Concurrencpp привносит силу параллельных задач в мир C ++, позволяя разработчикам легко и безопасно писать приложения, используя задачи, исполнителей и коратиков. Используя приложения condurrencpp могут разбить большие процедуры, которые необходимо асинхронно обработать в более мелкие задачи, которые работают одновременно и работают совместно для достижения желаемого результата. Concurrencpp также позволяет приложениям легко писать параллельные алгоритмы, используя параллельные коратики.
Основные преимущества condurrencpp:
std::thread и std::mutex .co_await .executor APIthread_pool_executor APImanual_executor APIresultresult APIlazy_result Typelazy_result APIresult_promise apiresult_promise Примерshared_result apishared_resultmake_ready_resultmake_exceptional_resultwhen_allwhen_anyresume_ontimer_queue apitimer APIgenerator APIgeneratorasync_lock APIscoped_async_lock apiasync_lock примерasync_condition_variable APIasync_condition_variable Примерruntime APItasktask APIConcurrencpp построен вокруг концепции одновременных задач. Задача - это асинхронная операция. Задачи предлагают более высокий уровень абстракции для одновременного кода, чем традиционные подходы, ориентированные на потоки. Задачи могут быть прикованы вместе, что означает, что задачи передают свой асинхронный результат от одного к другому, где результат одной задачи используется так, как если бы это был параметр или промежуточное значение другой текущей задачи. Задачи позволяют приложениям лучше использовать доступные аппаратные ресурсы и масштабировать гораздо больше, чем использование необработанных потоков, поскольку задачи могут быть приостановлены, ожидая другой задачи для получения результата, не блокируя лежащие в основе ОС. Задачи приносят гораздо большую производительность для разработчиков, позволяя им больше сосредоточиться на бизнес-логике и меньше на низкоуровневых концепциях, таких как управление потоками и межполосная синхронизация.
В то время как задачи указывают, какие действия должны быть выполнены, исполнители являются работниками-объектами, которые указывают , где и как выполнять задачи. Исполнители запасны приложения Утомительное управление пулами потоков и очередей задач. Исполнители также отделяют эти концепции от кода приложения, предоставляя унифицированный API для создания и планирования задач.
Задачи общаются друг с другом, используя объекты результата . Объект результата-это асинхронная труба, которая передает асинхронный результат одной задачи другой продолжающейся задаче. Результаты можно ожидать и разрешить не блокирующим манером.
Эти три понятия - задача, исполнитель и связанный результат - это строительные блоки coundencpp. Исполнители выполняют задачи, которые общаются друг с другом, отправляя результаты через результаты. Задачи, исполнители и объекты результатов совместно работают вместе для создания одновременного кода, который является быстрым и чистым.
Concurrencpp построен вокруг концепции RAII. Чтобы использовать задачи и исполнителей, приложения создают экземпляр runtime в начале main функции. Средство выполнения затем используется для приобретения существующих исполнителей и регистрации новых пользовательских исполнителей. Исполнители используются для создания и планирования задач для выполнения, и они могут вернуть объект result , который можно использовать для передачи асинхронного результата в другую задачу, которая действует как его потребитель. Когда время выполнения разрушено, оно отражается над каждым хранимым исполнителем и вызывает метод shutdown . Затем каждый исполнитель выходит изящно. Неопланированные задачи разрушены, и попытки создать новые задачи принесут исключение.
# include " concurrencpp/concurrencpp.h "
# include < iostream >
int main () {
concurrencpp::runtime runtime;
auto result = runtime. thread_executor ()-> submit ([] {
std::cout << " hello world " << std::endl;
});
result. get ();
return 0 ;
} В этом основном примере мы создали объект времени выполнения, затем мы приобрели исполнителя потока со временем выполнения. Мы использовали submit , чтобы пройти лямбду в качестве данного вызова. Эта лямбда возвращает void , следовательно, исполнитель возвращает result<void> объект, который передает асинхронный результат обратно в абонента. main вызовы get , какие блокируют основной резьбу, пока результат не станет готовым. Если исключение не было брошено, get void . Если было брошено исключение, get его. Асинхронно, thread_executor запускает новый поток выполнения и запускает данную лямбду. Это неявно co_return void , и задача завершена. main затем разблокируется.
# include " concurrencpp/concurrencpp.h "
# include < iostream >
# include < vector >
# include < algorithm >
# include < ctime >
using namespace concurrencpp ;
std::vector< int > make_random_vector () {
std::vector< int > vec ( 64 * 1'024 );
std::srand ( std::time ( nullptr ));
for ( auto & i : vec) {
i = :: rand ();
}
return vec;
}
result< size_t > count_even (std::shared_ptr<thread_pool_executor> tpe, const std::vector< int >& vector) {
const auto vecor_size = vector. size ();
const auto concurrency_level = tpe-> max_concurrency_level ();
const auto chunk_size = vecor_size / concurrency_level;
std::vector<result< size_t >> chunk_count;
for ( auto i = 0 ; i < concurrency_level; i++) {
const auto chunk_begin = i * chunk_size;
const auto chunk_end = chunk_begin + chunk_size;
auto result = tpe-> submit ([&vector, chunk_begin, chunk_end]() -> size_t {
return std::count_if (vector. begin () + chunk_begin, vector. begin () + chunk_end, []( auto i) {
return i % 2 == 0 ;
});
});
chunk_count. emplace_back ( std::move (result));
}
size_t total_count = 0 ;
for ( auto & result : chunk_count) {
total_count += co_await result;
}
co_return total_count;
}
int main () {
concurrencpp::runtime runtime;
const auto vector = make_random_vector ();
auto result = count_even (runtime. thread_pool_executor (), vector);
const auto total_count = result. get ();
std::cout << " there are " << total_count << " even numbers in the vector " << std::endl;
return 0 ;
} В этом примере мы запускаем программу, создав объект времени выполнения. Мы создаем вектор, заполненный случайными числами, затем приобретаем thread_pool_executor со временем выполнения и вызовы count_even . count_even - это коратика, которая порождает больше задач и co_await , чтобы они могли закончить внутри. max_concurrency_level возвращает максимальное количество работников, которые исполнитель поддерживает, в случае исполнителя Threadpool количество работников рассчитывается по количеству ядер. Затем мы разделяем массив, чтобы соответствовать количеству работников и отправляем каждый кусок, который обрабатывается в его собственной задаче. Асинхронно, рабочие считают, сколько даже чисел содержит каждый кусок, и co_return результат. count_even суммирует каждый результат, потянув счет, используя co_await , конечный результат затем co_return Ed. Основной поток, который был заблокирован вызовом get не блокируется, и общий счет возвращается. Основные отпечатки числа четных чисел, и программа изящно завершается.
Каждая большая или сложная операция может быть разбита на меньшие и цепные шаги. Задачи - это асинхронные операции, выполняющие эти вычислительные шаги. Задачи могут выполняться в любом месте с помощью исполнителей. В то время как задачи могут быть созданы из обычных вызовов (таких как функторы и лямбды), задачи в основном используются в коратиках, которые позволяют гладко подвеску и возобновление. В concurrencpp концепция задачи представлена concurrencpp::task Class. Хотя концепция задачи является центральной для Concurrenpp, приложения редко должны будут создавать и манипулировать объектами задачи, поскольку объекты задачи создаются и запланированы во время выполнения без внешней помощи.
CONCURRENCPP позволяет приложениям производить и потреблять CORUTINES в качестве основного способа создания задач. Concurrencpp поддерживает как нетерпеливые, так и ленивые задачи.
Желающие задачи начинают работать в тот момент, когда их вызывают. Этот тип исполнения рекомендуется, когда приложения должны запустить асинхронное действие и понадобиться его результат позже (пожар и потреблять позже) или полностью игнорировать асинхронный результат (пожар и забыть).
Желающие задачи могут вернуть result или null_result . Тип возврата result сообщает Coroutine передать возвращенное значение или исключение брошенного (огонь и потребление позже), в то время как тип возврата null_result говорит, что коратика бросится и игнорирует любой из них (огонь и забывайте).
Желающие коратики могут начать синхронно работать в потоке вызывающего абонента. Этот вид coroutines называется «обычные коратики». CONDARRENCPP AGER CORUTINES также может начать работать параллельно, внутри данного исполнителя этот вид коратиков называется «параллельными коратиками».
Ленивые задачи, с другой стороны, начинают работать только в том случае, когда co_await ed. Этот тип задач рекомендуется, когда результат задачи предназначен для использования сразу после создания задачи. Ленивые задачи, будучи отложенными, немного более оптимизированы для случая немедленного потребления, поскольку они не нуждаются в специальной синхронизации потоков, чтобы передать асинхронный результат обратно своему потребителю. Компилятор также может оптимизировать некоторые распределения памяти, необходимые для формирования основного обещания коратики. Невозможно запустить ленивую задачу и выполнить что-то еще-тем временем-стрельба из ленивой коратики обязательно означает приостановку абонента. Коротика вызывающего абонента будет возобновлен только после завершения коратины ленивого коллина. Ленивые задачи могут только вернуть lazy_result .
Ленивые задачи могут быть преобразованы в нетерпеливые задачи, позвонив lazy_result::run . Этот метод запускает ленивую задачу встроенной и возвращает объект result , который контролирует недавно началую задачу. Если разработчики не уверены, какой тип результата использовать, им рекомендуется использовать ленивые результаты, поскольку они могут быть преобразованы в регулярные (нетерпеливые) результаты, если это необходимо.
Когда функция возвращает любую из lazy_result , result или null_result и содержит хотя бы один co_await или co_return в своем теле, функция - это CoutingRencpppply Coroutine. Каждый действительный Concurrencpp Coroutine является действительной задачей. В нашем примере, даже примере, count_even -такая коратика. Сначала мы породили count_even , затем внутри него исполнитель Threadpool породил больше детских задач (которые создаются из обычных Callables), которые в конечном итоге были соединены с использованием co_await .
Исполнитель condurencpp - это объект, способный планировать и выполнять задачи. Исполнители упрощают работу по управлению ресурсами, такими как потоки, пулы потоков и очереди задач, отключая их от кода приложения. Исполнители предоставляют единый способ планирования и выполнения задач, поскольку все они расширяют concurrencpp::executor .
executor API class executor {
/*
Initializes a new executor and gives it a name.
*/
executor (std::string_view name);
/*
Destroys this executor.
*/
virtual ~executor () noexcept = default ;
/*
The name of the executor, used for logging and debugging.
*/
const std::string name;
/*
Schedules a task to run in this executor.
Throws concurrencpp::errors::runtime_shutdown exception if shutdown was called before.
*/
virtual void enqueue (concurrencpp::task task) = 0;
/*
Schedules a range of tasks to run in this executor.
Throws concurrencpp::errors::runtime_shutdown exception if shutdown was called before.
*/
virtual void enqueue (std::span<concurrencpp::task> tasks) = 0;
/*
Returns the maximum count of real OS threads this executor supports.
The actual count of threads this executor is running might be smaller than this number.
returns numeric_limits<int>::max if the executor does not have a limit for OS threads.
*/
virtual int max_concurrency_level () const noexcept = 0;
/*
Returns true if shutdown was called before, false otherwise.
*/
virtual bool shutdown_requested () const noexcept = 0;
/*
Shuts down the executor:
- Tells underlying threads to exit their work loop and joins them.
- Destroys unexecuted coroutines.
- Makes subsequent calls to enqueue, post, submit, bulk_post and
bulk_submit to throw concurrencpp::errors::runtime_shutdown exception.
- Makes shutdown_requested return true.
*/
virtual void shutdown () noexcept = 0;
/*
Turns a callable and its arguments into a task object and
schedules it to run in this executor using enqueue.
Arguments are passed to the task by decaying them first.
Throws errors::runtime_shutdown exception if shutdown has been called before.
*/
template < class callable_type , class ... argument_types>
void post (callable_type&& callable, argument_types&& ... arguments);
/*
Like post, but returns a result object that passes the asynchronous result.
Throws errors::runtime_shutdown exception if shutdown has been called before.
*/
template < class callable_type , class ... argument_types>
result<type> submit (callable_type&& callable, argument_types&& ... arguments);
/*
Turns an array of callables into an array of tasks and
schedules them to run in this executor using enqueue.
Throws errors::runtime_shutdown exception if shutdown has been called before.
*/
template < class callable_type >
void bulk_post (std::span<callable_type> callable_list);
/*
Like bulk_post, but returns an array of result objects that passes the asynchronous results.
Throws errors::runtime_shutdown exception if shutdown has been called before.
*/
template < class callable_type >
std::vector<concurrencpp::result<type>> bulk_submit (std::span<callable_type> callable_list);
};Как упомянуто выше, Concurrencpp предоставляет широко используемых исполнителей. Эти типы исполнителей:
Исполнитель пула потоков - исполнитель общего назначения, который поддерживает пул потоков. Исполнитель пула потоков подходит для коротких задач, связанных с процессором, которые не блокируются. Приложениям рекомендуется использовать этого исполнителя в качестве исполнителя по умолчанию для неблокирующих задач. Пул потоков concurrencpp обеспечивает динамическую инъекцию потока и динамическое баланс работы.
Исполнитель фон - исполнитель Threadpool с большим пулом потоков. Подходит для запуска коротких задач блокировки, таких как файл IO и DB -запросы. Важное примечание. При употреблении результатов этот исполнитель возвращался, вызовов submit и bulk_submit , важно переключить выполнение с помощью resume_on к исполнителю, связанному с процессором, чтобы предотвратить обработку задач, связанных с ЦП, для обработки внутри фонового_EXECUTOR.
пример:
auto result = background_executor.submit([] { /* some blocking action */ });
auto done_result = co_await result.resolve();
co_await resume_on (some_cpu_executor);
auto val = co_await done_result; // runs inside some_cpu_executorИсполнитель потока - исполнитель, который запускает каждую заполненную задачу для запуска в новом потоке выполнения. Темы не используются повторно. Этот исполнитель хорош для длительных задач, таких как объекты, которые запускают рабочую цикл, или длинные операции блокировки.
Исполнитель рабочего потока - один исполнитель по потоку, который поддерживает одну очередь задач. Подходит, когда приложения хотят выделенный поток, который выполняет много связанных задач.
Ручной исполнитель - исполнитель, который само по себе не выполняет. Код приложения может выполнять ранее включенные задачи путем вручную выпускать методы выполнения.
Произвольный исполнитель - базовый класс для пользователей, определенных исполнителями. Несмотря на то, что унаследование непосредственно от concurrencpp::executor возможно, derivable_executor использует шаблон CRTP , который предоставляет некоторые возможности оптимизации для компилятора.
Встроенный исполнитель - в основном используется для переопределения поведения других исполнителей. Внесение задачи эквивалентно вызов его встроенной.
Голый механизм исполнителя инкапсулируется в его метод enqueue . Этот метод выполняет задачу для выполнения и имеет две перегрузки: одна перегрузка получает один объект задачи в качестве аргумента, а другой, который получает диапазон объектов задачи. Вторая перегрузка используется для включения партии задач. Это позволяет лучше планировать эвристику и уменьшить споры.
Приложения не должны полагаться только на enqueue , concurrencpp::executor предоставляет API для планирования Callables, преобразуя их в объекты задачи за кулисами. Приложения могут попросить исполнителей вернуть объект результата, который передает асинхронный результат предоставленного вызова. Это делается по телефону executor::submit и executor::bulk_submit . submit получает вызов и возвращает объект результата. executor::bulk_submit получает span Callables и возвращает vector объектов результата аналогичным образом submit работы. Во многих случаях приложения не заинтересованы в асинхронной стоимости или исключении. В этом случае приложения могут использовать executor:::post и executor::bulk_post чтобы запланировать вызов или span обработков, которые будут выполнены, но также сообщают задачу отказаться от любого возвращаемого значения или исключения брошенного. Не проходить асинхронный результат быстрее, чем прохождение, но тогда у нас нет возможности узнать статус или результат продолжающейся задачи.
post , bulk_post , submit и bulk_submit Используйте enqueue за кулисами для базового механизма планирования.
thread_pool_executor API Помимо post , submit , bulk_post и bulk_submit , thread_pool_executor предоставляет эти дополнительные методы.
class thread_pool_executor {
/*
Returns the number of milliseconds each thread-pool worker
remains idle (lacks any task to execute) before exiting.
This constant can be set by passing a runtime_options object
to the constructor of the runtime class.
*/
std::chrono::milliseconds max_worker_idle_time () const noexcept ;
};manual_executor API Помимо post , submit , bulk_post и bulk_submit , manual_executor предоставляет эти дополнительные методы.
class manual_executor {
/*
Destructor. Equivalent to clear.
*/
~manual_executor () noexcept ;
/*
Returns the number of enqueued tasks at the moment of invocation.
This number can change quickly by the time the application handles it, it should be used as a hint.
This method is thread safe.
Might throw std::system_error if one of the underlying synchronization primitives throws.
*/
size_t size () const noexcept ;
/*
Queries whether the executor is empty from tasks at the moment of invocation.
This value can change quickly by the time the application handles it, it should be used as a hint.
This method is thread safe.
Might throw std::system_error if one of the underlying synchronization primitives throws.
*/
bool empty () const noexcept ;
/*
Clears the executor from any enqueued but yet to-be-executed tasks,
and returns the number of cleared tasks.
Tasks enqueued to this executor by (post_)submit method are resumed
and errors::broken_task exception is thrown inside them.
Ongoing tasks that are being executed by loop_once(_XXX) or loop(_XXX) are uneffected.
This method is thread safe.
Might throw std::system_error if one of the underlying synchronization primitives throws.
Throws errors::shutdown_exception if shutdown was called before.
*/
size_t clear ();
/*
Tries to execute a single task. If at the moment of invocation the executor
is empty, the method does nothing.
Returns true if a task was executed, false otherwise.
This method is thread safe.
Might throw std::system_error if one of the underlying synchronization primitives throws.
Throws errors::shutdown_exception if shutdown was called before.
*/
bool loop_once ();
/*
Tries to execute a single task.
This method returns when either a task was executed or max_waiting_time
(in milliseconds) has reached.
If max_waiting_time is 0, the method is equivalent to loop_once.
If shutdown is called from another thread, this method returns
and throws errors::shutdown_exception.
This method is thread safe.
Might throw std::system_error if one of the underlying synchronization primitives throws.
Throws errors::shutdown_exception if shutdown was called before.
*/
bool loop_once_for (std::chrono::milliseconds max_waiting_time);
/*
Tries to execute a single task.
This method returns when either a task was executed or timeout_time has reached.
If timeout_time has already expired, this method is equivalent to loop_once.
If shutdown is called from another thread, this method
returns and throws errors::shutdown_exception.
This method is thread safe.
Might throw std::system_error if one of the underlying synchronization primitives throws.
Throws errors::shutdown_exception if shutdown was called before.
*/
template < class clock_type , class duration_type >
bool loop_once_until (std::chrono::time_point<clock_type, duration_type> timeout_time);
/*
Tries to execute max_count enqueued tasks and returns the number of tasks that were executed.
This method does not wait: it returns when the executor
becomes empty from tasks or max_count tasks have been executed.
This method is thread safe.
Might throw std::system_error if one of the underlying synchronization primitives throws.
Throws errors::shutdown_exception if shutdown was called before.
*/
size_t loop ( size_t max_count);
/*
Tries to execute max_count tasks.
This method returns when either max_count tasks were executed or a
total amount of max_waiting_time has passed.
If max_waiting_time is 0, the method is equivalent to loop.
Returns the actual amount of tasks that were executed.
If shutdown is called from another thread, this method returns
and throws errors::shutdown_exception.
This method is thread safe.
Might throw std::system_error if one of the underlying synchronization primitives throws.
Throws errors::shutdown_exception if shutdown was called before.
*/
size_t loop_for ( size_t max_count, std::chrono::milliseconds max_waiting_time);
/*
Tries to execute max_count tasks.
This method returns when either max_count tasks were executed or timeout_time has reached.
If timeout_time has already expired, the method is equivalent to loop.
Returns the actual amount of tasks that were executed.
If shutdown is called from another thread, this method returns
and throws errors::shutdown_exception.
This method is thread safe.
Might throw std::system_error if one of the underlying synchronization primitives throws.
Throws errors::shutdown_exception if shutdown was called before.
*/
template < class clock_type , class duration_type >
size_t loop_until ( size_t max_count, std::chrono::time_point<clock_type, duration_type> timeout_time);
/*
Waits for at least one task to be available for execution.
This method should be used as a hint,
as other threads (calling loop, for example) might empty the executor,
before this thread has a chance to do something with the newly enqueued tasks.
If shutdown is called from another thread, this method returns
and throws errors::shutdown_exception.
This method is thread safe.
Might throw std::system_error if one of the underlying synchronization primitives throws.
Throws errors::shutdown_exception if shutdown was called before.
*/
void wait_for_task ();
/*
This method returns when one or more tasks are available for
execution or max_waiting_time has passed.
Returns true if at at least one task is available for execution, false otherwise.
This method should be used as a hint, as other threads (calling loop, for example)
might empty the executor, before this thread has a chance to do something
with the newly enqueued tasks.
If shutdown is called from another thread, this method
returns and throws errors::shutdown_exception.
This method is thread safe.
Might throw std::system_error if one of the underlying synchronization primitives throws.
Throws errors::shutdown_exception if shutdown was called before.
*/
bool wait_for_task_for (std::chrono::milliseconds max_waiting_time);
/*
This method returns when one or more tasks are available for execution or timeout_time has reached.
Returns true if at at least one task is available for execution, false otherwise.
This method should be used as a hint,
as other threads (calling loop, for example) might empty the executor,
before this thread has a chance to do something with the newly enqueued tasks.
If shutdown is called from another thread, this method
returns and throws errors::shutdown_exception.
This method is thread safe.
Might throw std::system_error if one of the underlying synchronization primitives throws.
Throws errors::shutdown_exception if shutdown was called before.
*/
template < class clock_type , class duration_type >
bool wait_for_task_until (std::chrono::time_point<clock_type, duration_type> timeout_time);
/*
This method returns when max_count or more tasks are available for execution.
This method should be used as a hint, as other threads
(calling loop, for example) might empty the executor,
before this thread has a chance to do something with the newly enqueued tasks.
If shutdown is called from another thread, this method returns
and throws errors::shutdown_exception.
This method is thread safe.
Might throw std::system_error if one of the underlying synchronization primitives throws.
Throws errors::shutdown_exception if shutdown was called before.
*/
void wait_for_tasks ( size_t max_count);
/*
This method returns when max_count or more tasks are available for execution
or max_waiting_time (in milliseconds) has passed.
Returns the number of tasks available for execution when the method returns.
This method should be used as a hint, as other
threads (calling loop, for example) might empty the executor,
before this thread has a chance to do something with the newly enqueued tasks.
If shutdown is called from another thread, this method returns
and throws errors::shutdown_exception.
This method is thread safe.
Might throw std::system_error if one of the underlying synchronization primitives throws.
Throws errors::shutdown_exception if shutdown was called before.
*/
size_t wait_for_tasks_for ( size_t count, std::chrono::milliseconds max_waiting_time);
/*
This method returns when max_count or more tasks are available for execution
or timeout_time is reached.
Returns the number of tasks available for execution when the method returns.
This method should be used as a hint, as other threads
(calling loop, for example) might empty the executor,
before this thread has a chance to do something with the newly enqueued tasks.
If shutdown is called from another thread, this method returns
and throws errors::shutdown_exception.
This method is thread safe.
Might throw std::system_error if one of the underlying synchronization primitives throws.
Throws errors::shutdown_exception if shutdown was called before.
*/
template < class clock_type , class duration_type >
size_t wait_for_tasks_until ( size_t count, std::chrono::time_point<clock_type, duration_type> timeout_time);
}; Асинхронные значения и исключения могут быть использованы с использованием объектов результатов condurencpp. Тип result представляет собой асинхронный результат нетерпеливой задачи, в то время как lazy_result представляет отложенный результат ленивой задачи.
Когда выполняется задача (нетерпеливая или ленивая), она либо возвращает допустимое значение, либо бросает исключение. В любом случае этот асинхронный результат передается потребителю объекта результата.
Объекты result образуют асимметричные коратики-выполнение Caller-Coroutine не осуществляется при выполнении Callee-Coroutine, обе Coroutines могут работать независимо. Только при употреблении результата Callee-Coroutine, Caller-Coroutine может быть подвешен в ожидании завершения Callee. Вплоть до этого момента оба коратики работают независимо. Callee-Coroutine работает независимо от того, используется ли его результат или нет.
lazy_result Objects образует симметричные коратики-выполнение Callee-Coroutine происходит только после подвески Caller-Coroutine. При ожидании ленивого результата нынешняя коратика приостановлена, и ленивая задача, связанная с ленивым результатом, начинает работать. После того, как Callee-Coroutine завершится и дает результат, возобновляется Caller-Coroutine. Если ленивый результат не потребляется, связанная с ним ленивая задача никогда не начинает работать.
Все объекты результатов являются типом только для перемещения, и поэтому их нельзя использовать после того, как их контент был перенесен в другой объект результата. В этом случае объект результата считается пустым, и пытается вызвать любой метод, кроме operator bool и operator = будет бросить исключение.
После того, как асинхронный результат был выведен из объекта результата (например, вызовом get или operator co_await ), объект результата становится пустым. Пустота может быть протестирована с помощью operator bool .
В ожидании результата означает приостановить текущую коратину до тех пор, пока объект результата не будет готов. Если из соответствующей задачи было возвращено допустимое значение, оно возвращается из объекта результата. Если связанная задача бросает исключение, оно повторно отображается. В момент ожидания, если результат уже готов, нынешняя коратика немедленно возобновляется. В противном случае он возобновится потоком, который устанавливает асинхронный результат или исключение.
Решение результата похоже на ожидание его. Разница в том, что выражение co_await вернет сам объект результата в не пустой форме в готовом состоянии. Асинхронный результат может быть получен с помощью get или co_await .
У каждого объекта результата есть статус, указывающий состояние асинхронного результата. Статус результата варьируется от result_status::idle (асинхронный результат или исключение еще не было получено) до result_status::value (связанная задача изящно изящно, возвращая допустимое значение) в result_status::exception (задача завершена путем выброса исключения). Статус может быть запрошен, позвонив (lazy_)result::status .
result Тип result представляет собой результат продолжающейся, асинхронной задачи, похожей на std::future .
Помимо ожидания и разрешения результатов-объектов, их также можно ждать, позвонив в любой из result::wait , result::wait_for , result::wait_until или result::get . В ожидании результата является операция блокировки (в случае асинхронного результата не готов), и приостановит весь поток выполнения, ожидая, когда асинхронный результат станет доступным. Операции ожидания, как правило, обескураживаются и разрешаются только в задачах уровня корня или в контекстах, которые позволяют это, например, блокирование основного потока, ожидая, чтобы остальная часть приложения закончилась изящно, или с использованием concurrencpp::blocking_executor или concurrencpp::thread_executor .
Ожидание объектов результата с помощью co_await (и при этом превращение текущей функции/задачи в кораку) также является предпочтительным способом потребления объектов результата, поскольку она не блокирует базовые потоки.
result API class result {
/*
Creates an empty result that isn't associated with any task.
*/
result () noexcept = default ;
/*
Destroys the result. Associated tasks are not cancelled.
The destructor does not block waiting for the asynchronous result to become ready.
*/
~result () noexcept = default ;
/*
Moves the content of rhs to *this. After this call, rhs is empty.
*/
result (result&& rhs) noexcept = default ;
/*
Moves the content of rhs to *this. After this call, rhs is empty. Returns *this.
*/
result& operator = (result&& rhs) noexcept = default ;
/*
Returns true if this is a non-empty result.
Applications must not use this object if this->operator bool() is false.
*/
explicit operator bool () const noexcept ;
/*
Queries the status of *this.
The returned value is any of result_status::idle, result_status::value or result_status::exception.
Throws errors::empty_result if *this is empty.
*/
result_status status () const ;
/*
Blocks the current thread of execution until this result is ready,
when status() != result_status::idle.
Throws errors::empty_result if *this is empty.
Might throw std::bad_alloc if fails to allocate memory.
Might throw std::system_error if one of the underlying synchronization primitives throws.
*/
void wait ();
/*
Blocks until this result is ready or duration has passed. Returns the status
of this result after unblocking.
Throws errors::empty_result if *this is empty.
Might throw std::bad_alloc if fails to allocate memory.
Might throw std::system_error if one of the underlying synchronization primitives throws.
*/
template < class duration_unit , class ratio >
result_status wait_for (std::chrono::duration<duration_unit, ratio> duration);
/*
Blocks until this result is ready or timeout_time has reached. Returns the status
of this result after unblocking.
Throws errors::empty_result if *this is empty.
Might throw std::bad_alloc if fails to allocate memory.
Might throw std::system_error if one of the underlying synchronization primitives throws.
*/
template < class clock , class duration >
result_status wait_until (std::chrono::time_point<clock, duration> timeout_time);
/*
Blocks the current thread of execution until this result is ready,
when status() != result_status::idle.
If the result is a valid value, it is returned, otherwise, get rethrows the asynchronous exception.
Throws errors::empty_result if *this is empty.
Might throw std::bad_alloc if fails to allocate memory.
Might throw std::system_error if one of the underlying synchronization primitives throws.
*/
type get ();
/*
Returns an awaitable used to await this result.
If the result is already ready - the current coroutine resumes
immediately in the calling thread of execution.
If the result is not ready yet, the current coroutine is suspended
and resumed when the asynchronous result is ready,
by the thread which had set the asynchronous value or exception.
In either way, after resuming, if the result is a valid value, it is returned.
Otherwise, operator co_await rethrows the asynchronous exception.
Throws errors::empty_result if *this is empty.
*/
auto operator co_await ();
/*
Returns an awaitable used to resolve this result.
After co_await expression finishes, *this is returned in a non-empty form, in a ready state.
Throws errors::empty_result if *this is empty.
*/
auto resolve ();
};lazy_result TypeЛенивый объект результата представляет собой результат отложенной ленивой задачи.
lazy_result несет ответственность за начать связанную ленивую задачу, так и передавать его отложенного результата своему потребителю. При ожидании или разрешении ленивый результат приостанавливает текущую коратину и запускает связанную ленивую задачу. Когда соответствующая задача выполняется, его асинхронное значение передается задаче вызывающего абонента, которая затем возобновляется.
Иногда API может вернуть ленивый результат, но приложениям нужна связанная с ним задача, чтобы работать с нетерпением (без приостановки задачи вызывающего абонента). В этом случае ленивые задачи могут быть преобразованы в нетерпеливые задачи, позвонив в run на связанном с ним ленивом результате. В этом случае соответствующая задача начнет работать в строке, не приостановив задачу вызывающего абонента. Первоначальный ленивый результат опустошен, и вместо этого будет возвращен действительный объект result , который отслеживает недавно началую задачу.
lazy_result API class lazy_result {
/*
Creates an empty lazy result that isn't associated with any task.
*/
lazy_result () noexcept = default ;
/*
Moves the content of rhs to *this. After this call, rhs is empty.
*/
lazy_result (lazy_result&& rhs) noexcept ;
/*
Destroys the result. If not empty, the destructor destroys the associated task without resuming it.
*/
~lazy_result () noexcept ;
/*
Moves the content of rhs to *this. After this call, rhs is empty. Returns *this.
If *this is not empty, then operator= destroys the associated task without resuming it.
*/
lazy_result& operator =(lazy_result&& rhs) noexcept ;
/*
Returns true if this is a non-empty result.
Applications must not use this object if this->operator bool() is false.
*/
explicit operator bool () const noexcept ;
/*
Queries the status of *this.
The returned value is any of result_status::idle, result_status::value or result_status::exception.
Throws errors::empty_result if *this is empty.
*/
result_status status () const ;
/*
Returns an awaitable used to start the associated task and await this result.
If the result is already ready - the current coroutine resumes immediately
in the calling thread of execution.
If the result is not ready yet, the current coroutine is suspended and
resumed when the asynchronous result is ready,
by the thread which had set the asynchronous value or exception.
In either way, after resuming, if the result is a valid value, it is returned.
Otherwise, operator co_await rethrows the asynchronous exception.
Throws errors::empty_result if *this is empty.
*/
auto operator co_await ();
/*
Returns an awaitable used to start the associated task and resolve this result.
If the result is already ready - the current coroutine resumes immediately
in the calling thread of execution.
If the result is not ready yet, the current coroutine is suspended and resumed
when the asynchronous result is ready, by the thread which
had set the asynchronous value or exception.
After co_await expression finishes, *this is returned in a non-empty form, in a ready state.
Throws errors::empty_result if *this is empty.
*/
auto resolve ();
/*
Runs the associated task inline and returns a result object that monitors the newly started task.
After this call, *this is empty.
Throws errors::empty_result if *this is empty.
Might throw std::bad_alloc if fails to allocate memory.
*/
result<type> run ();
};Регулярные нетерпеливые коратики начинают синхронно работать в вызовом потоке выполнения. Выполнение может перейти к другому потоку выполнения, если коратика подвергается перенсе, например, ожидая не готового объекта результата внутри него. Concurrencpp также предоставляет параллельные коратики, которые начинают работать внутри данного исполнителя, а не в призывном потоке выполнения. Этот стиль расписания CORUTINES особенно полезен при написании параллельных алгоритмов, рекурсивных алгоритмов и параллельных алгоритмов, которые используют модель вилки-младшего.
Каждая параллельная коратика должна соответствовать следующим предварительным условиям:
result / null_result .executor_tag в качестве своего первого аргумента.type* / type& / / std::shared_ptr<type> , где type является конкретным классом executor в качестве второго аргумента.co_await или co_return в его теле. Если все вышеперечисленное применяется, функция представляет собой параллельную Coroutine: Condurrencpp запустит приостановку Coroutine и немедленно перенесет ее для запуска в предоставленном исполнителе. concurrencpp::executor_tag - фиктивная заполнителя, чтобы сообщить о времени выполнения condurencpp, что эта функция не является обычной функцией, она должна начать работу внутри данного исполнителя. Если исполнитель перешел к параллельной коратике, нулевой, Coroutine не начнет работать, а исключение std::invalid_argument будет сброшено синхронно. Если все предварительные условия будут выполнены, приложения могут потреблять результат параллельной коратики, используя возвращенный объект результата.
В этом примере мы рассчитываем 30-го члена последовательности Фибоначчи параллельно. Мы начинаем запускать каждый шаг Fibonacci в собственной параллельной коратике. Первым аргументом является фиктивный executor_tag , а второй аргумент - исполнитель Threadpool. Каждый рекурсивный шаг вызывает новую параллельную кораку, которая работает параллельно. Каждый результат co_return своей родительской задачи и приобретен с помощью co_await .
Когда мы считаем, что ввод достаточно мал, чтобы рассчитывать синхронно (когда curr <= 10 ), мы прекращаем выполнять каждый рекурсивный шаг в его собственной задаче и просто синхронно решаем алгоритм.
# include " concurrencpp/concurrencpp.h "
# include < iostream >
using namespace concurrencpp ;
int fibonacci_sync ( int i) {
if (i == 0 ) {
return 0 ;
}
if (i == 1 ) {
return 1 ;
}
return fibonacci_sync (i - 1 ) + fibonacci_sync (i - 2 );
}
result< int > fibonacci (executor_tag, std::shared_ptr<thread_pool_executor> tpe, const int curr) {
if (curr <= 10 ) {
co_return fibonacci_sync (curr);
}
auto fib_1 = fibonacci ({}, tpe, curr - 1 );
auto fib_2 = fibonacci ({}, tpe, curr - 2 );
co_return co_await fib_1 + co_await fib_2;
}
int main () {
concurrencpp::runtime runtime;
auto fibb_30 = fibonacci ({}, runtime. thread_pool_executor (), 30 ). get ();
std::cout << " fibonacci(30) = " << fibb_30 << std::endl;
return 0 ;
} Для сравнения, так написан тот же код без использования параллельных столкновений, и не полагаясь на executor::submit в одиночку. Поскольку fibonacci возвращает result<int> , рекурсивно отправляя его через executor::submit result<result<int>> .
# include " concurrencpp/concurrencpp.h "
# include < iostream >
using namespace concurrencpp ;
int fibonacci_sync ( int i) {
if (i == 0 ) {
return 0 ;
}
if (i == 1 ) {
return 1 ;
}
return fibonacci_sync (i - 1 ) + fibonacci_sync (i - 2 );
}
result< int > fibonacci (std::shared_ptr<thread_pool_executor> tpe, const int curr) {
if (curr <= 10 ) {
co_return fibonacci_sync (curr);
}
auto fib_1 = tpe-> submit (fibonacci, tpe, curr - 1 );
auto fib_2 = tpe-> submit (fibonacci, tpe, curr - 2 );
co_return co_await co_await fib_1 +
co_await co_await fib_2;
}
int main () {
concurrencpp::runtime runtime;
auto fibb_30 = fibonacci (runtime. thread_pool_executor (), 30 ). get ();
std::cout << " fibonacci(30) = " << fibb_30 << std::endl;
return 0 ;
} Объекты результатов являются основным способом передачи данных между задачами в совместном образе, и мы видели, как исполнители и коратики создают такие объекты. Иногда мы хотим использовать возможности объектов результатов с не задачами, например, при использовании сторонней библиотеки. В этом случае мы можем завершить объект результата, используя result_promise . result_promise напоминает объект std::promise - приложения могут вручную устанавливать асинхронный результат или исключение и сделать связанный объект result готовым.
Так же, как объекты результатов, производители результатов-это только тип перемещения, который становится пустым после перемещения. Точно так же, после настройки результата или исключения, обещание результата также становится пустым. Если из-за того, что предложение результата выходит из области, и не было установлено никаких результатов/исключений, деструктор-результат устанавливает исключение concurrencpp::errors::broken_task Exception с использованием метода set_exception . Приостановленные и заблокированные задачи, ожидающие связанного объекта результата, возобновляются/разблокированы.
Обещания результатов могут преобразовать стиль кода обратного вызова в async/await style of кода: Всякий раз, когда компонент требуется обратный вызов для передачи асинхронного результата, мы можем передать обратный вызов, который вызывает set_result или set_exception (в зависимости от самого асинхронного результата) при передаче обещания результата и возвращает соответствующий результат.
result_promise api template < class type >
class result_promise {
/*
Constructs a valid result_promise.
Might throw std::bad_alloc if fails to allocate memory.
*/
result_promise ();
/*
Moves the content of rhs to *this. After this call, rhs is empty.
*/
result_promise (result_promise&& rhs) noexcept ;
/*
Destroys *this, possibly setting an errors::broken_task exception
by calling set_exception if *this is not empty at the time of destruction.
*/
~result_promise () noexcept ;
/*
Moves the content of rhs to *this. After this call, rhs is empty.
*/
result_promise& operator = (result_promise&& rhs) noexcept ;
/*
Returns true if this is a non-empty result-promise.
Applications must not use this object if this->operator bool() is false.
*/
explicit operator bool () const noexcept ;
/*
Sets a value by constructing <<type>> from arguments... in-place.
Makes the associated result object become ready - tasks waiting for it
to become ready are unblocked.
Suspended tasks are resumed inline.
After this call, *this becomes empty.
Throws errors::empty_result_promise exception If *this is empty.
Might throw any exception that the constructor
of type(std::forward<argument_types>(arguments)...) throws.
*/
template < class ... argument_types>
void set_result (argument_types&& ... arguments);
/*
Sets an exception.
Makes the associated result object become ready - tasks waiting for it
to become ready are unblocked.
Suspended tasks are resumed inline.
After this call, *this becomes empty.
Throws errors::empty_result_promise exception If *this is empty.
Throws std::invalid_argument exception if exception_ptr is null.
*/
void set_exception (std::exception_ptr exception_ptr);
/*
A convenience method that invokes a callable with arguments... and calls set_result
with the result of the invocation.
If an exception is thrown, the thrown exception is caught and set instead by calling set_exception.
After this call, *this becomes empty.
Throws errors::empty_result_promise exception If *this is empty.
Might throw any exception that callable(std::forward<argument_types>(arguments)...)
or the contructor of type(type&&) throw.
*/
template < class callable_type , class ... argument_types>
void set_from_function (callable_type&& callable, argument_types&& ... arguments);
/*
Gets the associated result object.
Throws errors::empty_result_promise exception If *this is empty.
Throws errors::result_already_retrieved exception if this method had been called before.
*/
result<type> get_result ();
};result_promise Пример: В этом примере result_promise используется для нажимания данных из одного потока, и его можно извлечь из связанного с ним объекта result из другого потока.
# include " concurrencpp/concurrencpp.h "
# include < iostream >
int main () {
concurrencpp::result_promise<std::string> promise;
auto result = promise. get_result ();
std::thread my_3_party_executor ([promise = std::move (promise)] () mutable {
std::this_thread::sleep_for ( std::chrono::seconds ( 1 )); // Imitate real work
promise. set_result ( " hello world " );
});
auto asynchronous_string = result. get ();
std::cout << " result promise returned string: " << asynchronous_string << std::endl;
my_3_party_executor. join ();
} В этом примере мы используем std::thread в качестве стороннего исполнителя. Это представляет собой сценарий, когда исполнитель, не являющийся Concurrencpp, используется как часть жизненного цикла приложения. Мы извлекаем объект результата, прежде чем передавать обещание, и блокируем основной резьбу, пока результат не станет готовым. В my_3_party_executor мы устанавливаем результат, как если бы мы его co_return .
Общие результаты - это особый вид объектов результатов, которые позволяют нескольким потребителям получить доступ к асинхронному результату, аналогично std::shared_future . Различные потребители из разных потоков могут вызывать функции, такие как await , get и resolve безопасным способом.
Общие результаты построены из обычных объектов результата и, в отличие от обычных объектов результата, они оба являются копируемыми и подвижными. Таким образом, shared_result ведет себя как тип std::shared_ptr . Если общий экземпляр результата перемещается в другой экземпляр, экземпляр становится пустым, и попытка получить доступ к нему вызовет исключение.
Чтобы поддержать нескольких потребителей, общие результаты возвращают ссылку на асинхронное значение вместо перемещения его (например, обычные результаты). Например, shared_result<int> возвращает int& of get , await и т. Д.. Если базовый тип shared_result является void или ссылочным типом (например, int& ), они возвращаются как обычно. Если асинхронный результат-это исключение брошенного, оно повторно.
Обратите внимание, что при получении асинхронного результата с использованием shared_result из нескольких потоков безопасно, фактическое значение не может быть безопасным потоком. Например, несколько потоков могут приобрести асинхронное целое число, получив свою ссылку ( int& ). Это не делает саму целое число безопасным. Можно мутировать асинхронное значение, если асинхронное значение уже безопасно. В качестве альтернативы, приложениям рекомендуется использовать const для начала (например, const int ) и приобретать постоянные ссылки (например, const int& ), которые предотвращают мутацию.
shared_result api class share_result {
/*
Creates an empty shared-result that isn't associated with any task.
*/
shared_result () noexcept = default ;
/*
Destroys the shared-result. Associated tasks are not cancelled.
The destructor does not block waiting for the asynchronous result to become ready.
*/
~shared_result () noexcept = default ;
/*
Converts a regular result object to a shared-result object.
After this call, rhs is empty.
Might throw std::bad_alloc if fails to allocate memory.
*/
shared_result (result<type> rhs);
/*
Copy constructor. Creates a copy of the shared result object that monitors the same task.
*/
shared_result ( const shared_result&) noexcept = default ;
/*
Move constructor. Moves rhs to *this. After this call, rhs is empty.
*/
shared_result (shared_result&& rhs) noexcept = default ;
/*
Copy assignment operator. Copies rhs to *this and monitors the same task that rhs monitors.
*/
shared_result& operator =( const shared_result& rhs) noexcept ;
/*
Move assignment operator. Moves rhs to *this. After this call, rhs is empty.
*/
shared_result& operator =(shared_result&& rhs) noexcept ;
/*
Returns true if this is a non-empty shared-result.
Applications must not use this object if this->operator bool() is false.
*/
explicit operator bool () const noexcept ;
/*
Queries the status of *this.
The return value is any of result_status::idle, result_status::value or result_status::exception.
Throws errors::empty_result if *this is empty.
*/
result_status status () const ;
/*
Blocks the current thread of execution until this shared-result is ready,
when status() != result_status::idle.
Throws errors::empty_result if *this is empty.
Might throw std::system_error if one of the underlying synchronization primitives throws.
*/
void wait ();
/*
Blocks until this shared-result is ready or duration has passed.
Returns the status of this shared-result after unblocking.
Throws errors::empty_result if *this is empty.
Might throw std::system_error if one of the underlying synchronization primitives throws.
*/
template < class duration_type , class ratio_type >
result_status wait_for (std::chrono::duration<duration_type, ratio_type> duration);
/*
Blocks until this shared-result is ready or timeout_time has reached.
Returns the status of this result after unblocking.
Throws errors::empty_result if *this is empty.
Might throw std::system_error if one of the underlying synchronization primitives throws.
*/
template < class clock_type , class duration_type >
result_status wait_until (std::chrono::time_point<clock_type, duration_type> timeout_time);
/*
Blocks the current thread of execution until this shared-result is ready,
when status() != result_status::idle.
If the result is a valid value, a reference to it is returned,
otherwise, get rethrows the asynchronous exception.
Throws errors::empty_result if *this is empty.
Might throw std::system_error if one of the underlying synchronization primitives throws.
*/
std:: add_lvalue_reference_t <type> get ();
/*
Returns an awaitable used to await this shared-result.
If the shared-result is already ready - the current coroutine resumes
immediately in the calling thread of execution.
If the shared-result is not ready yet, the current coroutine is
suspended and resumed when the asynchronous result is ready,
by the thread which had set the asynchronous value or exception.
In either way, after resuming, if the result is a valid value, a reference to it is returned.
Otherwise, operator co_await rethrows the asynchronous exception.
Throws errors::empty_result if *this is empty.
*/
auto operator co_await ();
/*
Returns an awaitable used to resolve this shared-result.
After co_await expression finishes, *this is returned in a non-empty form, in a ready state.
Throws errors::empty_result if *this is empty.
*/
auto resolve ();
};shared_result Пример: В этом примере объект result преобразуется в объект shared_result , а ссылка на асинхронный результат int получает многие задачи, порожденные thread_executor .
# include " concurrencpp/concurrencpp.h "
# include < iostream >
# include < chrono >
concurrencpp::result< void > consume_shared_result (concurrencpp::shared_result< int > shared_result,
std::shared_ptr<concurrencpp::executor> resume_executor) {
std::cout << " Awaiting shared_result to have a value " << std::endl;
const auto & async_value = co_await shared_result;
concurrencpp::resume_on (resume_executor);
std::cout << " In thread id " << std::this_thread::get_id () << " , got: " << async_value << " , memory address: " << &async_value << std::endl;
}
int main () {
concurrencpp::runtime runtime;
auto result = runtime. background_executor ()-> submit ([] {
std::this_thread::sleep_for ( std::chrono::seconds ( 1 ));
return 100 ;
});
concurrencpp::shared_result< int > shared_result ( std::move (result));
concurrencpp::result< void > results[ 8 ];
for ( size_t i = 0 ; i < 8 ; i++) {
results[i] = consume_shared_result (shared_result, runtime. thread_pool_executor ());
}
std::cout << " Main thread waiting for all consumers to finish " << std::endl;
auto tpe = runtime. thread_pool_executor ();
auto all_consumed = concurrencpp::when_all (tpe, std::begin (results), std::end (results)). run ();
all_consumed. get ();
std::cout << " All consumers are done, exiting " << std::endl;
return 0 ;
} Когда объект выполнения выходит из масштаба main , он отражает каждого хранимого исполнителя и вызывает свой метод shutdown . Попытка получить доступ к таймеру и любому исполнителю бросит исключение errors::runtime_shutdown . Когда исполнитель отключается, он очищает свои внутренние очереди задач, уничтожая незавершенные объекты task . Если объект задачи хранит совместную точку, эта коратика возобновляется встроенными, и внутрь исключение errors::broken_task в нем. В любом случае, когда выполняется исключение runtime_shutdown или broken_task , приложения должны как можно скорее прекратить свой текущий поток кода как можно скорее. Эти исключения не следует игнорировать. И runtime_shutdown , и broken_task наследуют от errors::interrupted_task Base Class, и этот тип также можно использовать в пункте catch для обработки завершения единым способом.
Многие асинхронные действия совместного использования требуют экземпляра исполнителя в качестве их исполнителя резюме . Когда асинхронное действие (осуществляемое в качестве коратики) может закончить синхронно, оно немедленно возобновляется в вызовом потоке выполнения. Если асинхронное действие не может закончить синхронно, оно будет возобновлено, когда оно закончится, внутри данного резюме-эмиктора. Например, when_any Utility Function требует экземпляра резюме-эмиктора в качестве первого аргумента. when_any возвращает lazy_result , который становится готовым, когда по крайней мере один данный результат становится готовым. Если один из результатов уже готов в момент вызова when_any , призывая Coroutine возобновляется синхронно в вызовом потоке выполнения. Если нет, то призывая Coroutine будет возобновлен, когда будет закончено, по крайней мере, результата, внутри данного резюме-эмиктора. Исполнители резюме важны, потому что они требуют, чтобы коратики возобновляются в тех случаях, когда неясно, где, как предполагается, возобновлена коратика (например, в случае when_any и when_all ), или в тех случаях, когда асинхронное действие обрабатывается внутри одного из работников совместного использования, которые используются только для обработки этого конкретного действия, а не кода применения.
make_ready_result функция make_ready_result создает готовую объект результата из данных аргументов. В ожидании такого результата приведет к немедленному возобновлению нынешней коратики. get и operator co_await вернет построенное значение.
/*
Creates a ready result object by building <<type>> from arguments&&... in-place.
Might throw any exception that the constructor
of type(std::forward<argument_types>(arguments)...) throws.
Might throw std::bad_alloc exception if fails to allocate memory.
*/
template < class type , class ... argument_types>
result<type> make_ready_result (argument_types&& ... arguments);
/*
An overload for void type.
Might throw std::bad_alloc exception if fails to allocate memory.
*/
result< void > make_ready_result ();make_exceptional_result функция make_exceptional_result создает готовую объект результата из данного исключения. В ожидании такого результата приведет к немедленному возобновлению нынешней коратики. get and operator co_await повторно переключает заданное исключение.
/*
Creates a ready result object from an exception pointer.
The returned result object will re-throw exception_ptr when calling get or await.
Throws std::invalid_argument if exception_ptr is null.
Might throw std::bad_alloc exception if fails to allocate memory.
*/
template < class type >
result<type> make_exceptional_result (std::exception_ptr exception_ptr);
/*
Overload. Similar to make_exceptional_result(std::exception_ptr),
but gets an exception object directly.
Might throw any exception that the constructor of exception_type(std::move(exception)) might throw.
Might throw std::bad_alloc exception if fails to allocate memory.
*/
template < class type , class exception_type >
result<type> make_exceptional_result (exception_type exception );when_all функция when_all - это функция утилиты, которая создает ленивый объект результата, который становится готовым, когда все результаты ввода завершены. В ожидании этого ленивого результата возвращает все объекты-входные объекты в готовом состоянии, готовый к употреблению.
when_all функция_ал поставляется с тремя ароматами - один, который принимает гетерогенный диапазон объектов результатов, другой, который получает пару итераторов в диапазон объектов результатов того же типа, и, наконец, перегрузка, которая вообще не принимает объектов результатов. В случае отсутствия объектов входного результата - функция возвращает готовый объект результата пустого кортежа.
Если один из переданных объектов результата пуст, будет выброшено исключение. В этом случае объекты-ввода-результата не зависят от функции и могут использоваться снова после обработки исключения. Если все объекты входного результата действительны, они опустошены этой функцией и возвращаются в действительном и готовом состоянии в качестве результата выходного вывода.
В настоящее время, when_all принимает только объекты result .
Все перегрузки принимают исполнителя резюме в качестве своего первого параметра. При ожидании результата, возвращаемого when_all , Caller Coroutine будет возобновлен данным исполнителем резюме.
/*
Creates a result object that becomes ready when all the input results become ready.
Passed result objects are emptied and returned as a tuple.
Throws std::invalid_argument if any of the passed result objects is empty.
Might throw an std::bad_alloc exception if no memory is available.
*/
template < class ... result_types>
lazy_result<std::tuple< typename std::decay<result_types>::type...>>
when_all (std::shared_ptr<executor_type> resume_executor,
result_types&& ... results);
/*
Overload. Similar to when_all(result_types&& ...) but receives a pair of iterators referencing a range.
Passed result objects are emptied and returned as a vector.
If begin == end, the function returns immediately with an empty vector.
Throws std::invalid_argument if any of the passed result objects is empty.
Might throw an std::bad_alloc exception if no memory is available.
*/
template < class iterator_type >
lazy_result<std::vector< typename std::iterator_traits<iterator_type>::value_type>>
when_all (std::shared_ptr<executor_type> resume_executor,
iterator_type begin, iterator_type end);
/*
Overload. Returns a ready result object that doesn't monitor any asynchronous result.
Might throw an std::bad_alloc exception if no memory is available.
*/
lazy_result<std::tuple<>> when_all (std::shared_ptr<executor_type> resume_executor);when_any Функция when_any - это функция утилиты, которая создает ленивый объект результата, который становится готовым, когда по крайней мере один входной результат завершен. В ожидании этого результата вернет вспомогательную структуру, содержащую все объекты-ввода, плюс индекс выполненной задачи. Может случиться так, что к моменту использования готового результата другие результаты могли бы уже завершить асинхронно. Приложения могут звонить, when_any многократно вызывает, чтобы потреблять готовые результаты по мере их завершения до тех пор, пока все результаты не будут использоваться.
when_any функция, которая поставляется только с двумя ароматами - один, который принимает гетерогенный диапазон объектов результатов, а другой, который получает пару итераторов в диапазоне объектов результатов одного и того же типа. В отличие от when_all , нет никакого значения в ожидании хотя бы одной задачи, чтобы закончить, когда диапазон результатов совершенно пуст. Следовательно, нет перегрузки без аргументов. Кроме того, перегрузка двух итераторов вызовет исключение, если эти итераторы ссылаются на пустой диапазон (когда begin == end ).
Если один из переданных объектов результата пуст, будет выброшено исключение. В любом случае, исключение, выдвигается, объекты, получающие вход, не зависят от функции и могут использоваться снова после обработки исключения. Если все объекты входного результата действительны, они опустошены этой функцией и возвращаются в действительное состояние в качестве результата выходного вывода.
В настоящее время, when_any принимает только объекты result .
Все перегрузки принимают исполнителя резюме в качестве своего первого параметра. При ожидании результата, возвращаемого when_any , Caller Coroutine будет возобновлен данным исполнителем резюме.
/*
Helper struct returned from when_any.
index is the position of the ready result in results sequence.
results is either an std::tuple or an std::vector of the results that were passed to when_any.
*/
template < class sequence_type >
struct when_any_result {
std:: size_t index;
sequence_type results;
};
/*
Creates a result object that becomes ready when at least one of the input results is ready.
Passed result objects are emptied and returned as a tuple.
Throws std::invalid_argument if any of the passed result objects is empty.
Might throw an std::bad_alloc exception if no memory is available.
*/
template < class ... result_types>
lazy_result<when_any_result<std::tuple<result_types...>>>
when_any (std::shared_ptr<executor_type> resume_executor,
result_types&& ... results);
/*
Overload. Similar to when_any(result_types&& ...) but receives a pair of iterators referencing a range.
Passed result objects are emptied and returned as a vector.
Throws std::invalid_argument if begin == end.
Throws std::invalid_argument if any of the passed result objects is empty.
Might throw an std::bad_alloc exception if no memory is available.
*/
template < class iterator_type >
lazy_result<when_any_result<std::vector< typename std::iterator_traits<iterator_type>::value_type>>>
when_any (std::shared_ptr<executor_type> resume_executor,
iterator_type begin, iterator_type end);resume_on resume_on возвращает ожидаемое, которое приостанавливает текущую кораку и возобновляет ее внутри, данный executor . Это важная функция, которая гарантирует, что в правильном исполнителе работает коратика. Например, приложения могут запланировать фоновую задачу, используя background_executor и ждать возвращаемого объекта результата. В этом случае ожидающая коратика будет возобновлена в фоновом исполнителе. Вызов resume_on с другим исполнителем, связанным с процессором, гарантирует, что связанные с ЦП строки кода не будут работать на фоновом исполнителе после завершения фоновой задачи. Если задача повторно запланирована, чтобы запустить другого исполнителя с помощью resume_on , но этот исполнитель выключен, прежде чем он сможет возобновить приостановленную задачу, эта задача возобновлена немедленно, и исключение erros::broken_task будет выбрано. В этом случае приложения должны довольно изящно.
/*
Returns an awaitable that suspends the current coroutine and resumes it inside executor.
Might throw any exception that executor_type::enqueue throws.
*/
template < class executor_type >
auto resume_on (std::shared_ptr<executor_type> executor);Concurrencpp также предоставляет таймеры и очереди таймер. Таймеры-это объекты, которые определяют асинхронные действия, выполняемые на исполнителе в четко определенном интервале времени. Существует три типа таймеров - обычные таймеры , онкот -таймеры и объекты задержки .
У обычных таймеров есть четыре свойства, которые их определяют:
Как и другие объекты в concurrencpp, таймеры - это только тип движения, который может быть пустым. Когда таймер разрушен или timer::cancel называется, таймер отменяет свои запланированные, но еще не выполненные задачи. Продолжающиеся задачи неэффективны. Таймер должен быть безопасным потоком. Рекомендуется установить свое время и частоту таймеров на гранулярность 50 миллисекунд.
Очередь таймера - это работник совместного использования, который управляет коллекцией таймеров и обрабатывает их только в одном потоке выполнения. Это также агент, используемый для создания новых таймеров. Когда крайний срок таймера (будь то время или частота таймера) достигнут, очередь таймера «запускает» таймер, планируя его вызов, чтобы запустить связанного исполнителя в качестве задачи.
Так же, как исполнители, очереди таймера также придерживаются концепции RAII. Когда объект выполнения выходит из области, он отключает очередь таймера, отменяя все ожидающие таймеры. После того, как очередь таймера будет выключена, любой последующий вызов make_timer , make_onshot_timer и make_delay_object принесет исключение errors::runtime_shutdown . Приложения не должны пытаться выключить очереди таймер самостоятельно.
timer_queue api: class timer_queue {
/*
Destroys this timer_queue.
*/
~timer_queue () noexcept ;
/*
Shuts down this timer_queue:
Tells the underlying thread of execution to quit and joins it.
Cancels all pending timers.
After this call, invocation of any method besides shutdown
and shutdown_requested will throw an errors::runtime_shutdown.
If shutdown had been called before, this method has no effect.
*/
void shutdown () noexcept ;
/*
Returns true if shutdown had been called before, false otherwise.
*/
bool shutdown_requested () const noexcept ;
/*
Creates a new running timer where *this is the associated timer_queue.
Throws std::invalid_argument if executor is null.
Throws errors::runtime_shutdown if shutdown had been called before.
Might throw std::bad_alloc if fails to allocate memory.
Might throw std::system_error if the one of the underlying synchronization primitives throws.
*/
template < class callable_type , class ... argumet_types>
timer make_timer (
std::chrono::milliseconds due_time,
std::chrono::milliseconds frequency,
std::shared_ptr<concurrencpp::executor> executor,
callable_type&& callable,
argumet_types&& ... arguments);
/*
Creates a new one-shot timer where *this is the associated timer_queue.
Throws std::invalid_argument if executor is null.
Throws errors::runtime_shutdown if shutdown had been called before.
Might throw std::bad_alloc if fails to allocate memory.
Might throw std::system_error if the one of the underlying synchronization primitives throws.
*/
template < class callable_type , class ... argumet_types>
timer make_one_shot_timer (
std::chrono::milliseconds due_time,
std::shared_ptr<concurrencpp::executor> executor,
callable_type&& callable,
argumet_types&& ... arguments);
/*
Creates a new delay object where *this is the associated timer_queue.
Throws std::invalid_argument if executor is null.
Throws errors::runtime_shutdown if shutdown had been called before.
Might throw std::bad_alloc if fails to allocate memory.
Might throw std::system_error if the one of the underlying synchronization primitives throws.
*/
result< void > make_delay_object (
std::chrono::milliseconds due_time,
std::shared_ptr<concurrencpp::executor> executor);
};timer API: class timer {
/*
Creates an empty timer.
*/
timer () noexcept = default ;
/*
Cancels the timer, if not empty.
*/
~timer () noexcept ;
/*
Moves the content of rhs to *this.
rhs is empty after this call.
*/
timer (timer&& rhs) noexcept = default ;
/*
Moves the content of rhs to *this.
rhs is empty after this call.
Returns *this.
*/
timer& operator = (timer&& rhs) noexcept ;
/*
Cancels this timer.
After this call, the associated timer_queue will not schedule *this
to run again and *this becomes empty.
Scheduled, but not yet executed tasks are cancelled.
Ongoing tasks are uneffected.
This method has no effect if *this is empty or the associated timer_queue has already expired.
Might throw std::system_error if one of the underlying synchronization primitives throws.
*/
void cancel ();
/*
Returns the associated executor of this timer.
Throws concurrencpp::errors::empty_timer is *this is empty.
*/
std::shared_ptr<executor> get_executor () const ;
/*
Returns the associated timer_queue of this timer.
Throws concurrencpp::errors::empty_timer is *this is empty.
*/
std::weak_ptr<timer_queue> get_timer_queue () const ;
/*
Returns the due time of this timer.
Throws concurrencpp::errors::empty_timer is *this is empty.
*/
std::chrono::milliseconds get_due_time () const ;
/*
Returns the frequency of this timer.
Throws concurrencpp::errors::empty_timer is *this is empty.
*/
std::chrono::milliseconds get_frequency () const ;
/*
Sets new frequency for this timer.
Callables already scheduled to run at the time of invocation are not affected.
Throws concurrencpp::errors::empty_timer is *this is empty.
*/
void set_frequency (std::chrono::milliseconds new_frequency);
/*
Returns true is *this is not an empty timer, false otherwise.
The timer should not be used if this->operator bool() is false.
*/
explicit operator bool () const noexcept ;
};В этом примере мы создаем обычный таймер, используя очередь таймера. Таймер планирует свой вызов, чтобы работать через 1,5 секунды, а затем стреляет каждую 2 секунды. Данный вызов запускается на исполнителе Threadpool.
# include " concurrencpp/concurrencpp.h "
# include < iostream >
using namespace std ::chrono_literals ;
int main () {
concurrencpp::runtime runtime;
std:: atomic_size_t counter = 1 ;
concurrencpp::timer timer = runtime. timer_queue ()-> make_timer (
1500ms,
2000ms,
runtime. thread_pool_executor (),
[&] {
const auto c = counter. fetch_add ( 1 );
std::cout << " timer was invoked for the " << c << " th time " << std::endl;
});
std::this_thread::sleep_for (12s);
return 0 ;
}Таймер OneShot - это единовременный таймер с учетом времени - после того, как он запланирует свой вызов, чтобы запустить, как только он никогда не перемещает его, чтобы снова работать.
В этом примере мы создаем таймер, который работает только один раз - через 3 секунды от его создания, таймер планирует свой вызов для запуска в новом потоке выполнения (используя thread_executor ).
# include " concurrencpp/concurrencpp.h "
# include < iostream >
using namespace std ::chrono_literals ;
int main () {
concurrencpp::runtime runtime;
concurrencpp::timer timer = runtime. timer_queue ()-> make_one_shot_timer (
3000ms,
runtime. thread_executor (),
[&] {
std::cout << " hello and goodbye " << std::endl;
});
std::this_thread::sleep_for (4s);
return 0 ;
} Объект задержки - это ленивый объект результата, который становится готовым, когда он является co_await Ed, и его срок достигается. Приложения могут co_await этот объект результата, чтобы задержать текущую коратину не блокировкой. Текущая коратика возобновится исполнителем, который был передан в make_delay_object .
В этом примере мы породим задачу (которая не возвращает никакого результата или исключения, который задерживается в цикле, вызывая co_await на объекте задержки.
# include " concurrencpp/concurrencpp.h "
# include < iostream >
using namespace std ::chrono_literals ;
concurrencpp::null_result delayed_task (
std::shared_ptr<concurrencpp::timer_queue> tq,
std::shared_ptr<concurrencpp::thread_pool_executor> ex) {
size_t counter = 1 ;
while ( true ) {
std::cout << " task was invoked " << counter << " times. " << std::endl;
counter++;
co_await tq-> make_delay_object (1500ms, ex);
}
}
int main () {
concurrencpp::runtime runtime;
delayed_task (runtime. timer_queue (), runtime. thread_pool_executor ());
std::this_thread::sleep_for (10s);
return 0 ;
} Генератор - это ленивая, синхронная коратика, способная создавать поток значений для потребления. Генераторы используют ключевое слово co_yield , чтобы получить значения обратно своим потребителям.
Генераторы предназначены для использования синхронно - они могут использовать только ключевое слово co_yield и не должны использовать ключевое слово co_await . Генератор будет продолжать создавать значения, пока называется ключевое слово co_yield . Если ключевое слово co_return называется (явно или неявно), то генератор перестанет создавать значения. Аналогичным образом, если исключение будет брошено, то генератор перестанет производить значения, а исключение брошенного будет повторно обтрантируется потребителю генератора.
Генераторы предназначены для использования в range-for цикла: генераторы неявно производят два итератора - begin и end , которые контролируют выполнение цикла for . Эти итераторы не должны обрабатывать или доступны вручную.
Когда генератор создается, он начинается как ленивая задача. Когда его метод begin называется, генератор возобновится в первый раз, и итератор возвращается. Ленивое задание возобновляется, вызывая operator++ на возвращенном итераторе. Возвращенный итератор будет равным итератору end , когда генератор заканчивает выполнение либо изящно, изящно или бросая исключение. Как упоминалось ранее, это происходит за кулисами внутренним механизмом петли и генератора, и его не следует вызывать напрямую.
Как и другие объекты в concurrencpp, генераторы являются типом только для перемещения. После того, как генератор был перемещен, он считается пустым, и пытается получить доступ к его внутренним методам (кроме operator bool ), вызовет исключение. Пустота генератора обычно не должна происходить - рекомендуется потреблять генераторы при их создании в цикле for цикла и не пытаться назвать их методы индивидуально.
generator API class generator {
/*
Move constructor. After this call, rhs is empty.
*/
generator (generator&& rhs) noexcept ;
/*
Destructor. Invalidates existing iterators.
*/
~generator () noexcept ;
generator ( const generator& rhs) = delete ;
generator& operator =(generator&& rhs) = delete ;
generator& operator =( const generator& rhs) = delete ;
/*
Returns true if this generator is not empty.
Applications must not use this object if this->operator bool() is false.
*/
explicit operator bool () const noexcept ;
/*
Starts running this generator and returns an iterator.
Throws errors::empty_generator if *this is empty.
Re-throws any exception that is thrown inside the generator code.
*/
iterator begin ();
/*
Returns an end iterator.
*/
static generator_end_iterator end () noexcept ;
};
class generator_iterator {
using value_type = std:: remove_reference_t <type>;
using reference = value_type&;
using pointer = value_type*;
using iterator_category = std::input_iterator_tag;
using difference_type = std:: ptrdiff_t ;
/*
Resumes the suspended generator and returns *this.
Re-throws any exception that was thrown inside the generator code.
*/
generator_iterator& operator ++();
/*
Post-increment version of operator++.
*/
void operator ++( int );
/*
Returns the latest value produced by the associated generator.
*/
reference operator *() const noexcept ;
/*
Returns a pointer to the latest value produced by the associated generator.
*/
pointer operator ->() const noexcept ;
/*
Comparision operators.
*/
friend bool operator ==( const generator_iterator& it0, const generator_iterator& it1) noexcept ;
friend bool operator ==( const generator_iterator& it, generator_end_iterator) noexcept ;
friend bool operator ==(generator_end_iterator end_it, const generator_iterator& it) noexcept ;
friend bool operator !=( const generator_iterator& it, generator_end_iterator end_it) noexcept ;
friend bool operator !=(generator_end_iterator end_it, const generator_iterator& it) noexcept ;
};generator : В этом примере мы напишем генератор, который дает n-й член последовательности S(n) = 1 + 2 + 3 + ... + n где n <= 100 :
concurrencpp::generator< int > sequence () {
int i = 1 ;
int sum = 0 ;
while (i <= 100 ) {
sum += i;
++i;
co_yield sum;
}
}
int main () {
for ( auto value : sequence ()) {
std::cout << value << std::end;
}
return 0 ;
} Регулярные синхронные замки не могут быть безопасно использоваться внутри задач по ряду причин:
std::mutex , будут заблокированы и разблокированы в том же потоке выполнения. Разблокировка синхронной блокировки в потоке, которая не заблокировала, это неопределенное поведение. Поскольку задачи могут быть приостановлены и возобновлены в любом потоке выполнения, синхронные блокировки будут ломаться при использовании внутри задач. concurrencpp::async_lock решает эти проблемы, предоставляя аналогичный API для std::mutex , с основным отличием, которое призывает к concurrencpp::async_lock вернет ленивый-результат, который можно безопасно co_awaited к задачам. Если одна задача пытается заблокировать асинкзалон и сбой, задача будет приостановлена и будет возобновлена, когда блокировка разблокирована и приобретена при приостановленной задаче. Это позволяет исполнителям обрабатывать огромное количество задач, ожидающих приобретения блокировки без дорогостоящего переключения контекста и дорогих вызовов ядра.
Подобно тому, как работает std::mutex , только одна задача может приобрести async_lock в любой момент времени, и в момент приобретения находится барьера для чтения . Выпуск асинхронной блокировки помещает барьера записи и позволяет следующей задаче приобрести его, создавая цепочку одного модификатора за раз, которые видят изменения, внесенные другие модификаторы, и публикует свои модификации для следующих модификаторов.
Как std::mutex , concurrencpp::async_lock не рекурсивный . Дополнительное внимание должно быть уделено при получении такой блокировки - блокировка не должна быть приобретена снова в задаче, которая была порождена другой задачей, которая уже приобрела замок. В таком случае произойдет неизбежный мертвый замк. В отличие от других объектов в concurrencpp, async_lock не является ни копированием, ни подвижным.
Как и стандартные блокировки, concurrencpp::async_lock предназначен для использования с обломами, которые используют C ++ raii Idiom, чтобы гарантировать, что блокировки всегда разблокируются при возврате функции или исключении. async_lock::lock возвращает ленивый-результат обломанной обертки, которая вызывает async_lock::unlock на разрушение. Необработанное использование async_lock::unlock обескуражено. concurrencpp::scoped_async_lock действует как обертка и предоставляет API, который почти идентичен std::unique_lock . concurrencpp::scoped_async_lock подвижен, но не копируется.
async_lock::lock и scoped_async_lock::lock требует резюме-экскутора в качестве их параметра. Призывая эти методы, если блокировка доступна для блокировки, то она заблокирована, и текущая задача возобновится немедленно. Если нет, то текущая задача приостановлена и будет возобновлена внутри данного резюме-эмиктора, когда блокировка наконец получена.
concurrencpp::scoped_async_lock завершает async_lock и убедитесь, что он правильно разблокирован. Как и std::unique_lock , есть случаи, что он не обертывает какую -либо блокировку, и в этом случае он считается пустым. Пустой scoped_async_lock может произойти, когда он по умолчанию построен, перемещается или используется scoped_async_lock::release . Пустое блокировка с асинхронизацией не разблокирует какую-либо блокировку на разрушении.
Даже если блокировка с асинксетом не является пустым, это не означает, что он владеет базовым асинкзалоком, и он разблокирует его при разрушении. Непустые и не принадлежащие блокировки Scoped-Async могут произойти, если был вызван scoped_async_lock::unlock или с использованием конструктора scoped_async_lock(async_lock&, std::defer_lock_t) .
async_lock API class async_lock {
/*
Constructs an async lock object.
*/
async_lock () noexcept ;
/*
Destructs an async lock object.
*this is not automatically unlocked at the moment of destruction.
*/
~async_lock () noexcept ;
/*
Asynchronously acquires the async lock.
If *this has already been locked by another non-parent task, the current task will be suspended
and will be resumed when *this is acquired, inside resume_executor.
If *this has not been locked by another task, then *this will be acquired and the current task will be resumed
immediately in the calling thread of execution.
If *this has already been locked by a parent task, then unavoidable dead-lock will occur.
Throws std::invalid_argument if resume_executor is null.
Throws std::system error if one of the underlying synhchronization primitives throws.
*/
lazy_result<scoped_async_lock> lock (std::shared_ptr<executor> resume_executor);
/*
Tries to acquire *this in the calling thread of execution.
Returns true if *this is acquired, false otherwise.
In any case, the current task is resumed immediately in the calling thread of execution.
Throws std::system error if one of the underlying synhchronization primitives throws.
*/
lazy_result< bool > try_lock ();
/*
Releases *this and allows other tasks (including suspended tasks waiting for *this) to acquire it.
Throws std::system error if *this is not locked at the moment of calling this method.
Throws std::system error if one of the underlying synhchronization primitives throws.
*/
void unlock ();
};scoped_async_lock api class scoped_async_lock {
/*
Constructs an async lock wrapper that does not wrap any async lock.
*/
scoped_async_lock () noexcept = default ;
/*
If *this wraps async_lock, this method releases the wrapped lock.
*/
~scoped_async_lock () noexcept ;
/*
Moves rhs to *this.
After this call, *rhs does not wrap any async lock.
*/
scoped_async_lock (scoped_async_lock&& rhs) noexcept ;
/*
Wrapps unlocked lock.
lock must not be in acquired mode when calling this method.
*/
scoped_async_lock (async_lock& lock, std:: defer_lock_t ) noexcept ;
/*
Wrapps locked lock.
lock must be already acquired when calling this method.
*/
scoped_async_lock (async_lock& lock, std:: adopt_lock_t ) noexcept ;
/*
Calls async_lock::lock on the wrapped locked, using resume_executor as a parameter.
Throws std::invalid_argument if resume_executor is nulll.
Throws std::system_error if *this does not wrap any lock.
Throws std::system_error if wrapped lock is already locked.
Throws any exception async_lock::lock throws.
*/
lazy_result< void > lock (std::shared_ptr<executor> resume_executor);
/*
Calls async_lock::try_lock on the wrapped lock.
Throws std::system_error if *this does not wrap any lock.
Throws std::system_error if wrapped lock is already locked.
Throws any exception async_lock::try_lock throws.
*/
lazy_result< bool > try_lock ();
/*
Calls async_lock::unlock on the wrapped lock.
If *this does not wrap any lock, this method does nothing.
Throws std::system_error if *this wraps a lock and it is not locked.
*/
void unlock ();
/*
Checks whether *this wraps a locked mutex or not.
Returns true if wrapped locked is in acquired state, false otherwise.
*/
bool owns_lock () const noexcept ;
/*
Equivalent to owns_lock.
*/
explicit operator bool () const noexcept ;
/*
Swaps the contents of *this and rhs.
*/
void swap (scoped_async_lock& rhs) noexcept ;
/*
Empties *this and returns a pointer to the previously wrapped lock.
After a call to this method, *this doesn't wrap any lock.
The previously wrapped lock is not released,
it must be released by either unlocking it manually through the returned pointer or by
capturing the pointer with another scoped_async_lock which will take ownerwhip over it.
*/
async_lock* release () noexcept ;
/*
Returns a pointer to the wrapped async_lock, or a null pointer if there is no wrapped async_lock.
*/
async_lock* mutex () const noexcept ;
};async_lock Пример: В этом примере мы одновременно подталкиваем 10 000 000 целых числа к объекту std::vector из разных задач, используя async_lock , чтобы убедиться, что раса данных не происходит, и правильность внутреннего состояния этого векторного объекта сохраняется.
# include " concurrencpp/concurrencpp.h "
# include < vector >
# include < iostream >
std::vector< size_t > numbers;
concurrencpp::async_lock lock;
concurrencpp::result< void > add_numbers (concurrencpp::executor_tag,
std::shared_ptr<concurrencpp::executor> executor,
size_t begin,
size_t end) {
for ( auto i = begin; i < end; i++) {
concurrencpp::scoped_async_lock raii_wrapper = co_await lock. lock (executor);
numbers. push_back (i);
}
}
int main () {
concurrencpp::runtime runtime;
constexpr size_t range = 10'000'000 ;
constexpr size_t sections = 4 ;
concurrencpp::result< void > results[sections];
for ( size_t i = 0 ; i < 4 ; i++) {
const auto range_start = i * range / sections;
const auto range_end = (i + 1 ) * range / sections;
results[i] = add_numbers ({}, runtime. thread_pool_executor (), range_start, range_end);
}
for ( auto & result : results) {
result. get ();
}
std::cout << " vector size is " << numbers. size () << std::endl;
// make sure the vector state has not been corrupted by unprotected concurrent accesses
std::sort (numbers. begin (), numbers. end ());
for ( size_t i = 0 ; i < range; i++) {
if (numbers[i] != i) {
std::cerr << " vector state is corrupted. " << std::endl;
return - 1 ;
}
}
std::cout << " succeeded pushing range [0 - 10,000,000] concurrently to the vector! " << std::endl;
return 0 ;
} async_condition_variable подражает стандартному condition_variable и может быть безопасно использоваться с задачами наряду с async_lock . async_condition_variable работает с async_lock , чтобы приостановить задачу, пока не изменится какая -то общая память (защищенная блокировкой). Задачи, которые хотят отслеживать изменения общей памяти, будут заблокировать экземпляр async_lock и вызовут async_condition_variable::await . Это будет атомно разблокировать блокировку и приостановить текущую задачу до тех пор, пока некоторые задачи модификатора не уведут переменную условия. Задача модификатора получает блокировку, изменяет общую память, разблокирует блокировку и вызовет либо notify_one , либо notify_all . Когда возобновляется приостановленная задача (используя исполнителя резюме, который был дан для await ), она снова заблокирует блокировку, позволяя задаче проходить с точки приостановки плавно. Как и async_lock , async_condition_variable не является ни подвижным, ни копируемым - он должен быть создан в одном месте и доступ к нескольким задачам.
async_condition_variable::await перегрузки требуют резюме-эмиктора, который будет использоваться для возобновления задачи, и заблокированная scoped_async_lock . async_condition_variable::await приходит с двумя перегрузками - одна, которая принимает предикат, а тот, который нет. Перегрузка, которая не принимает предикат, приостановит вызову сразу после вызова, пока оно не будет возобновлено вызовом, чтобы notify_* . Перегрузка, которая принимает предикат, работает, позволяя предикату осматривать общую память и неоднократно приостановить задачу, пока общая память не достигнет своего разыскиваемого состояния. Схематично это работает как звонок
while (!pred()) { // pred() inspects the shared memory and returns true or false
co_await await (resume_executor, lock); // suspend the current task until another task calls `notify_xxx`
} Как и стандартная переменная условия, приложениям рекомендуется использовать перегрузку предиката, поскольку это позволяет более мелкозернистый контроль над суспензиями и возобновлением. async_condition_variable может использоваться для написания одновременных коллекций и структур данных, таких как одновременные очереди и каналы.
Внутренне, async_condition_variable содержит подвеску, в которой задачи выполняют себя, когда они ждут уведомления переменной условия. Когда называются какие -либо методы notify_* , задача уведомления выполняет либо одну задачу или все задачи, в зависимости от вызываемого метода. Задачи выполняются из подвесной шейки в манере. Например, если задача A await вызовов, а затем await notify_one B. Задача B останется приостановленной до тех пор, пока не будет вызван другой вызов notify_one или notify_all . Если задача A и задача B приостановлены, а задача C вызывает notify_all , то обе задачи будут выполнены и возобновлены.
async_condition_variable API class async_condition_variable {
/*
Constructor.
*/
async_condition_variable () noexcept ;
/*
Atomically releases lock and suspends the current task by adding it to *this suspension-queue.
Throws std::invalid_argument if resume_executor is null.
Throws std::invalid_argument if lock is not locked at the moment of calling this method.
Might throw std::system_error if the underlying std::mutex throws.
*/
lazy_result< void > await (std::shared_ptr<executor> resume_executor, scoped_async_lock& lock);
/*
Equivalent to:
while (!pred()) {
co_await await(resume_executor, lock);
}
Might throw any exception that await(resume_executor, lock) might throw.
Might throw any exception that pred might throw.
*/
template < class predicate_type >
lazy_result< void > await (std::shared_ptr<executor> resume_executor, scoped_async_lock& lock, predicate_type pred);
/*
Dequeues one task from *this suspension-queue and resumes it, if any available at the moment of calling this method.
The suspended task is resumed by scheduling it to run on the executor given when await was called.
Might throw std::system_error if the underlying std::mutex throws.
*/
void notify_one ();
/*
Dequeues all tasks from *this suspension-queue and resumes them, if any available at the moment of calling this method.
The suspended tasks are resumed by scheduling them to run on the executors given when await was called.
Might throw std::system_error if the underlying std::mutex throws.
*/
void notify_all ();
};async_condition_variable Пример: В этом примере async_lock и async_condition_variable работают вместе, чтобы реализовать параллельную очередь, которую можно использовать для отправки данных (в этом примере, целых числах) между задачами. Обратите внимание, что некоторые методы возвращают result , в то время как еще один возврат lazy_result , показывая, как как нетерпеливые, так и ленивые задачи могут работать вместе.
# include " concurrencpp/concurrencpp.h "
# include < queue >
# include < iostream >
using namespace concurrencpp ;
class concurrent_queue {
private:
async_lock _lock;
async_condition_variable _cv;
std::queue< int > _queue;
bool _abort = false ;
public:
concurrent_queue () = default ;
result< void > shutdown (std::shared_ptr<executor> resume_executor) {
{
auto guard = co_await _lock. lock (resume_executor);
_abort = true ;
}
_cv. notify_all ();
}
lazy_result< void > push (std::shared_ptr<executor> resume_executor, int i) {
{
auto guard = co_await _lock. lock (resume_executor);
_queue. push (i);
}
_cv. notify_one ();
}
lazy_result< int > pop (std::shared_ptr<executor> resume_executor) {
auto guard = co_await _lock. lock (resume_executor);
co_await _cv. await (resume_executor, guard, [ this ] {
return _abort || !_queue. empty ();
});
if (!_queue. empty ()) {
auto result = _queue. front ();
_queue. pop ();
co_return result;
}
assert (_abort);
throw std::runtime_error ( " queue has been shut down. " );
}
};
result< void > producer_loop (executor_tag,
std::shared_ptr<thread_pool_executor> tpe,
concurrent_queue& queue,
int range_start,
int range_end) {
for (; range_start < range_end; ++range_start) {
co_await queue. push (tpe, range_start);
}
}
result< void > consumer_loop (executor_tag, std::shared_ptr<thread_pool_executor> tpe, concurrent_queue& queue) {
try {
while ( true ) {
std::cout << co_await queue. pop (tpe) << std::endl;
}
} catch ( const std:: exception & e) {
std::cerr << e. what () << std::endl;
}
}
int main () {
runtime runtime;
const auto thread_pool_executor = runtime. thread_pool_executor ();
concurrent_queue queue;
result< void > producers[ 4 ];
result< void > consumers[ 4 ];
for ( int i = 0 ; i < 4 ; i++) {
producers[i] = producer_loop ({}, thread_pool_executor, queue, i * 5 , (i + 1 ) * 5 );
}
for ( int i = 0 ; i < 4 ; i++) {
consumers[i] = consumer_loop ({}, thread_pool_executor, queue);
}
for ( int i = 0 ; i < 4 ; i++) {
producers[i]. get ();
}
queue. shutdown (thread_pool_executor). get ();
for ( int i = 0 ; i < 4 ; i++) {
consumers[i]. get ();
}
return 0 ;
} Объект выполнения Concurrencpp - это агент, используемый для приобретения, хранения и создания новых исполнителей.
Средство выполнения должно быть создано как тип значения, как только основная функция начинает работать. Когда время выполнения concurrencpp выходит из области, оно отражается над своими хранящимися исполнителями и отключает их один за другим, позвонив executor::shutdown . Затем исполнители выйдут из своего внутреннего рабочего цикла, и любую последующую попытку запланировать новую задачу, которая бросит исключение concurrencpp::runtime_shutdown . Средство выполнения также содержит глобальную очередь таймера, используемая для создания таймеров и задержки объектов. После разрушения хранящиеся исполнители разрушают неисполненные задачи и ждут, пока продолжающиеся задачи закончат. Если продолжающаяся задача пытается использовать исполнителя для создания новых задач или запланировать его собственное продолжение задачи - будет брошено исключение. В этом случае постоянные задачи должны уйти как можно скорее, что позволяет их базовым исполнителям уйти. Очередь таймера также будет выключена, отменив все время работы. С этим стилем кода Raii, перед созданием объекта выполнения не может быть обработана никаких задач, и в то время как/после того, как время выполнения выйдет из области. Это освобождает одновременные приложения от необходимости явно передавать сообщения о прекращении. Задачи являются исполнителями бесплатного использования, пока объект выполнения живой.
runtime API class runtime {
/*
Creates a runtime object with default options.
*/
runtime ();
/*
Creates a runtime object with user defined options.
*/
runtime ( const concurrencpp::runtime_options& options);
/*
Destroys this runtime object.
Calls executor::shutdown on each monitored executor.
Calls timer_queue::shutdown on the global timer queue.
*/
~runtime () noexcept ;
/*
Returns this runtime timer queue used to create new times.
*/
std::shared_ptr<concurrencpp::timer_queue> timer_queue () const noexcept ;
/*
Returns this runtime concurrencpp::inline_executor
*/
std::shared_ptr<concurrencpp::inline_executor> inline_executor () const noexcept ;
/*
Returns this runtime concurrencpp::thread_pool_executor
*/
std::shared_ptr<concurrencpp::thread_pool_executor> thread_pool_executor () const noexcept ;
/*
Returns this runtime concurrencpp::background_executor
*/
std::shared_ptr<concurrencpp::thread_pool_executor> background_executor () const noexcept ;
/*
Returns this runtime concurrencpp::thread_executor
*/
std::shared_ptr<concurrencpp::thread_executor> thread_executor () const noexcept ;
/*
Creates a new concurrencpp::worker_thread_executor and registers it in this runtime.
Might throw std::bad_alloc or std::system_error if any underlying memory or system resource could not have been acquired.
*/
std::shared_ptr<concurrencpp::worker_thread_executor> make_worker_thread_executor ();
/*
Creates a new concurrencpp::manual_executor and registers it in this runtime.
Might throw std::bad_alloc or std::system_error if any underlying memory or system resource could not have been acquired.
*/
std::shared_ptr<concurrencpp::manual_executor> make_manual_executor ();
/*
Creates a new user defined executor and registers it in this runtime.
executor_type must be a valid concrete class of concurrencpp::executor.
Might throw std::bad_alloc if no memory is available.
Might throw any exception that the constructor of <<executor_type>> might throw.
*/
template < class executor_type , class ... argument_types>
std::shared_ptr<executor_type> make_executor (argument_types&& ... arguments);
/*
returns the version of concurrencpp that the library was built with.
*/
static std::tuple< unsigned int , unsigned int , unsigned int > version () noexcept ;
}; В некоторых случаях приложения заинтересованы в мониторинге создания и прекращения потоков, например, некоторые распределители памяти требуют, чтобы новые потоки были зарегистрированы и незарегистрированы при их создании и прекращении. Среда выполнения concurrencpp позволяет установить обратный вызов создания потока и обратный вызов завершения потока. Эти обратные вызовы будут вызваны всякий раз, когда один из работников Concurrencpp создает новый поток и когда этот поток заканчивается. Эти обратные вызовы всегда вызываются изнутри созданного/завершающего потока, поэтому std::this_thread::get_id всегда вернет соответствующий идентификатор потока. Подписью этих обратных вызовов является void callback (std::string_view thread_name) . thread_name - это конкретный заголовок condurrencpp, который придается потоке и может быть замечен в некоторых отладчиках, которые представляют имя потока. Имя потока не гарантированно будет уникальным и должно использоваться для ведения журнала и отладки.
Чтобы установить обратный вызов по созданию потока и/или обратный вызов за прекращение потока, приложения могут установить участники thread_started_callback и/или thread_terminated_callback of runtime_options , который передается в конструктор времени выполнения. Поскольку эти обратные вызовы копируются каждому работнику condurencppp, который может создавать потоки, эти обратные вызовы должны быть копируемыми.
# include " concurrencpp/concurrencpp.h "
# include < iostream >
int main () {
concurrencpp::runtime_options options;
options. thread_started_callback = [](std::string_view thread_name) {
std::cout << " A new thread is starting to run, name: " << thread_name << " , thread id: " << std::this_thread::get_id ()
<< std::endl;
};
options. thread_terminated_callback = [](std::string_view thread_name) {
std::cout << " A thread is terminating, name: " << thread_name << " , thread id: " << std::this_thread::get_id () << std::endl;
};
concurrencpp::runtime runtime (options);
const auto timer_queue = runtime. timer_queue ();
const auto thread_pool_executor = runtime. thread_pool_executor ();
concurrencpp::timer timer =
timer_queue-> make_timer ( std::chrono::milliseconds ( 100 ), std::chrono::milliseconds ( 500 ), thread_pool_executor, [] {
std::cout << " A timer callable is executing " << std::endl;
});
std::this_thread::sleep_for ( std::chrono::seconds ( 3 ));
return 0 ;
}Возможный выход:
A new thread is starting to run, name: concurrencpp::timer_queue worker, thread id: 7496
A new thread is starting to run, name: concurrencpp::thread_pool_executor worker, thread id: 21620
A timer callable is executing
A timer callable is executing
A timer callable is executing
A timer callable is executing
A timer callable is executing
A timer callable is executing
A thread is terminating, name: concurrencpp::timer_queue worker, thread id: 7496
A thread is terminating, name: concurrencpp::thread_pool_executor worker, thread id: 21620
Приложения могут создавать свой собственный тип исполнителя, унаследовав класс derivable_executor . Есть несколько пунктов, которые следует учитывать при реализации пользовательских исполнителей: самое главное, чтобы помнить, что исполнители используются из нескольких потоков, поэтому реализованные методы должны быть защищены потоком.
Новые исполнители могут быть созданы с помощью runtime::make_executor . Приложения не должны создавать новых исполнителей с простым экземпляром (например, std::make_shared или rain new ), только с помощью runtime::make_executor . Кроме того, приложения не должны пытаться повторно устанавливать встроенных исполнителей concurrencpp, такие как thread_pool_executor или The thread_executor , эти исполнители должны доступны только через свои существующие экземпляры в объекте времени выполнения.
Еще один важный момент - правильно обработать выключение: shutdown , shutdown_requested и enqueue
shutdown должно сказать, что базовые потоки ушли, а затем присоединиться к ним.shutdown может быть вызвано несколько раз, и метод должен обрабатывать этот сценарий, игнорируя любые последующие вызовы после shutdown после первого вызова.enqueue должен бросить concurrencpp::errors::runtime_shutdown исключение, если shutdown было вызвано ранее. task Реализация исполнителей является одним из редких случаев, когда приложения должны напрямую работать с классом concurrencpp::task . concurrencpp::task - это std::function как объект, но с несколькими различиями. Как и std::function объект задачи хранит вызов, который действует как асинхронная операция. В отличие от std::function task - это только тип перемещения. При вызове объекты задачи не получают параметров и возвращайте void . Более того, каждый объект задачи может быть вызван только один раз. После первого вызова объект задачи становится пустым. Вызывание пустого объекта задачи эквивалентно вызове пустой лямбда ( []{} ) и не будет бросить никакого исключения. Объекты задачи получают их вызов в качестве ссылки на пересылку ( type&& где type , параметр шаблона), а не Copy (например std::function ). Строительство хранимого призывника происходит на месте. Это позволяет объектам задачи содержать Callables, которые используются только для перемещения (например, std::unique_ptr и concurrencpp::result ). Объекты задачи стараются использовать различные методы для оптимизации использования хранимых типов, например, объекты задачи применяют короткую буфера-оптимизацию (SBO) для регулярных, небольших приверженцев и будут встроить вызовы на std::coroutine_handle<void> , вызывая их непосредственно без виртуальной диспетчеры.
task API class task {
/*
Creates an empty task object.
*/
task () noexcept ;
/*
Creates a task object by moving the stored callable of rhs to *this.
If rhs is empty, then *this will also be empty after construction.
After this call, rhs is empty.
*/
task (task&& rhs) noexcept ;
/*
Creates a task object by storing callable in *this.
<<typename std::decay<callable_type>::type>> will be in-place-
constructed inside *this by perfect forwarding callable.
*/
template < class callable_type >
task (callable_type&& callable);
/*
Destroys stored callable, does nothing if empty.
*/
~task () noexcept ;
/*
If *this is empty, does nothing.
Invokes stored callable, and immediately destroys it.
After this call, *this is empty.
May throw any exception that the invoked callable may throw.
*/
void operator ()();
/*
Moves the stored callable of rhs to *this.
If rhs is empty, then *this will also be empty after this call.
If *this already contains a stored callable, operator = destroys it first.
*/
task& operator =(task&& rhs) noexcept ;
/*
If *this is not empty, task::clear destroys the stored callable and empties *this.
If *this is empty, clear does nothing.
*/
void clear () noexcept ;
/*
Returns true if *this stores a callable. false otherwise.
*/
explicit operator bool () const noexcept ;
/*
Returns true if *this stores a callable,
and that stored callable has the same type as <<typename std::decay<callable_type>::type>>
*/
template < class callable_type >
bool contains () const noexcept ;
}; При реализации пользовательских исполнителей она зависит от реализации для хранения объектов task (когда называется enqueue ) и выполнять их в соответствии с внутренним механизмом исполнителя.
В этом примере мы создаем исполнителя, который регистрирует действия, такие как задачи по внедрению или выполнение их. Мы реализуем интерфейс executor , и мы просим время выполнения для создания и хранения его экземпляра, позвонив runtime::make_executor . Остальная часть приложения ведет себя точно так же, как если бы мы использовали не определенных пользователей-исполнителей.
# include " concurrencpp/concurrencpp.h "
# include < iostream >
# include < queue >
# include < thread >
# include < mutex >
# include < condition_variable >
class logging_executor : public concurrencpp ::derivable_executor<logging_executor> {
private:
mutable std::mutex _lock;
std::queue<concurrencpp::task> _queue;
std::condition_variable _condition;
bool _shutdown_requested;
std::thread _thread;
const std::string _prefix;
void work_loop () {
while ( true ) {
std::unique_lock<std::mutex> lock (_lock);
if (_shutdown_requested) {
return ;
}
if (!_queue. empty ()) {
auto task = std::move (_queue. front ());
_queue. pop ();
lock. unlock ();
std::cout << _prefix << " A task is being executed " << std::endl;
task ();
continue ;
}
_condition. wait (lock, [ this ] {
return !_queue. empty () || _shutdown_requested;
});
}
}
public:
logging_executor (std::string_view prefix) :
derivable_executor<logging_executor>( " logging_executor " ),
_shutdown_requested ( false ),
_prefix (prefix) {
_thread = std::thread ([ this ] {
work_loop ();
});
}
void enqueue (concurrencpp::task task) override {
std::cout << _prefix << " A task is being enqueued! " << std::endl;
std::unique_lock<std::mutex> lock (_lock);
if (_shutdown_requested) {
throw concurrencpp::errors::runtime_shutdown ( " logging executor - executor was shutdown. " );
}
_queue. emplace ( std::move (task));
_condition. notify_one ();
}
void enqueue (std::span<concurrencpp::task> tasks) override {
std::cout << _prefix << tasks. size () << " tasks are being enqueued! " << std::endl;
std::unique_lock<std::mutex> lock (_lock);
if (_shutdown_requested) {
throw concurrencpp::errors::runtime_shutdown ( " logging executor - executor was shutdown. " );
}
for ( auto & task : tasks) {
_queue. emplace ( std::move (task));
}
_condition. notify_one ();
}
int max_concurrency_level () const noexcept override {
return 1 ;
}
bool shutdown_requested () const noexcept override {
std::unique_lock<std::mutex> lock (_lock);
return _shutdown_requested;
}
void shutdown () noexcept override {
std::cout << _prefix << " shutdown requested " << std::endl;
std::unique_lock<std::mutex> lock (_lock);
if (_shutdown_requested) return ; // nothing to do.
_shutdown_requested = true ;
lock. unlock ();
_condition. notify_one ();
_thread. join ();
}
};
int main () {
concurrencpp::runtime runtime;
auto logging_ex = runtime. make_executor <logging_executor>( " Session #1234 " );
for ( size_t i = 0 ; i < 10 ; i++) {
logging_ex-> post ([] {
std::cout << " hello world " << std::endl;
});
}
std::getchar ();
return 0 ;
}$ git clone https://github.com/David-Haim/concurrencpp.git
$ cd concurrencpp
$ cmake -S . -B build /lib
$ cmake -- build build /lib --config Release$ git clone https://github.com/David-Haim/concurrencpp.git
$ cd concurrencpp
$ cmake -S test -B build / test
$ cmake -- build build / test
< # for release mode: cmake --build build/test --config Release #>
$ cd build / test
$ ctest . -V -C Debug
< # for release mode: ctest . -V -C Release #> $ git clone https://github.com/David-Haim/concurrencpp.git
$ cd concurrencpp
$ cmake -DCMAKE_BUILD_TYPE=Release -S . -B build /lib
$ cmake -- build build /lib
#optional, install the library: sudo cmake --install build/lib С Clang и GCC также можно запустить тесты с поддержкой TSAN (Thread Sanitizer).
$ git clone https://github.com/David-Haim/concurrencpp.git
$ cd concurrencpp
$ cmake -S test -B build / test
#for release mode: cmake -DCMAKE_BUILD_TYPE=Release -S test -B build/test
#for TSAN mode: cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_THREAD_SANITIZER=Yes -S test -B build/test
$ cmake -- build build / test
$ cd build / test
$ ctest . -V При компиляции на Linux библиотека пытается использовать libstdc++ по умолчанию. Если вы намереваетесь использовать libc++ в качестве стандартной реализации библиотеки, флаг CMAKE_TOOLCHAIN_FILE должен быть указан, как показано ниже:
$ cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/libc++.cmake -DCMAKE_BUILD_TYPE=Release -S . -B build /libВ качестве альтернативы для создания и установки библиотеки вручную, разработчики могут получить стабильные выпуски COMPURRENCPP через менеджеры пакетов VCPKG и CONAN:
VCPKG:
$ vcpkg install concurrencppКонан: совместно на Conancenter
Concurrencpp поставляется со встроенной программой песочницы, которая может изменить и экспериментировать, не устанавливая или связывают скомпилированную библиотеку с другой кодовой базой. Чтобы сыграть с песочницей, разработчики могут изменить sandbox/main.cpp и составить приложение, используя следующие команды:
$ cmake -S sandbox -B build /sandbox
$ cmake -- build build /sandbox
< # for release mode: cmake --build build/sandbox --config Release #>
$ ./ build /sandbox < # runs the sandbox> $ cmake -S sandbox -B build /sandbox
#for release mode: cmake -DCMAKE_BUILD_TYPE=Release -S sandbox -B build/sandbox
$ cmake -- build build /sandbox
$ ./ build /sandbox #runs the sandbox