O Concurrencpp traz o poder das tarefas simultâneas ao mundo C ++, permitindo que os desenvolvedores escrevam aplicativos altamente concorrentes com facilidade e segurança usando tarefas, executores e coroutinas. Ao usar aplicativos Concurrencpp, pode dividir grandes procedimentos que precisam ser processados de forma assíncrona em tarefas menores que funcionam simultaneamente e funcionam de maneira cooperativa para alcançar o resultado desejado. O Concurrencpp também permite que os aplicativos escrevam algoritmos paralelos facilmente usando coroutinas paralelas.
As principais vantagens do Concurrencpp são:
std::thread e std::mutex .co_await .executorthread_pool_executormanual_executor APIresultresultlazy_result TIPOlazy_resultresult_promiseresult_promise Exemploshared_result APIshared_resultmake_ready_resultmake_exceptional_resultwhen_allwhen_anyresume_ontimer_queue APItimergeneratorgeneratorasync_lockscoped_async_lockasync_lockasync_condition_variableasync_condition_variableruntimetasktaskConcurrencpp é construído em torno do conceito de tarefas simultâneas. Uma tarefa é uma operação assíncrona. As tarefas oferecem um nível mais alto de abstração para o código simultâneo do que as abordagens tradicionais centradas no fio. As tarefas podem ser acorrentadas, o que significa que as tarefas passam seu resultado assíncrono de um para outro, onde o resultado de uma tarefa é usado como se fosse um parâmetro ou um valor intermediário de outra tarefa em andamento. As tarefas permitem que os aplicativos utilizem melhor os recursos de hardware disponíveis e escalem muito mais do que o uso de threads brutos, pois as tarefas podem ser suspensas, aguardando outra tarefa para produzir um resultado, sem bloquear os threads subjacentes do OS. As tarefas trazem muito mais produtividade aos desenvolvedores, permitindo que eles se concentrem mais na lógica de negócios e menos em conceitos de baixo nível, como gerenciamento de threads e sincronização entre thread.
Embora as tarefas especifiquem quais ações devem ser executadas, os executores são objetos de trabalhador que especificam onde e como executar tarefas. Executores pouparam aplicativos o gerenciamento tedioso de pools de threads e filas de tarefas. Os executores também desacoplam esses conceitos do código do aplicativo, fornecendo uma API unificada para criar e agendar tarefas.
As tarefas se comunicam usando objetos de resultado . Um objeto de resultado é um tubo assíncrono que passa o resultado assíncrono de uma tarefa para outra tarefa contínua. Os resultados podem ser aguardados e resolvidos de maneira não bloqueada.
Esses três conceitos - a tarefa, o executor e o resultado associado são os blocos de construção do Concurrencpp. Os executores executam tarefas que se comunicam, enviando resultados por meio de objeto de resultados. Tarefas, executores e objetos de resultado trabalham juntos simbioticamente para produzir código simultâneo que é rápido e limpo.
Concurrencpp é construído em torno do conceito RAII. Para usar tarefas e executores, os aplicativos criam uma instância runtime no início da função main . O tempo de execução é usado para adquirir executores existentes e registrar novos executores definidos pelo usuário. Os executores são usados para criar e agendar tarefas para executar, e podem retornar um objeto result que pode ser usado para passar o resultado assíncrono para outra tarefa que atua como consumidor. Quando o tempo de execução é destruído, ele itera sobre todos os executores armazenados e chama seu método shutdown . Todo executor sai graciosamente. As tarefas não programadas são destruídas e as tentativas de criar novas tarefas lançarão uma exceção.
# 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 ;
} Neste exemplo básico, criamos um objeto de tempo de execução e adquirimos o executor do thread do tempo de execução. Usamos submit para passar um lambda como nosso indicável. Esse lambda retorna void , portanto, o executor retorna um objeto result<void> que passa o resultado assíncrono de volta ao chamador. As main chamadas get quais bloqueia o fio principal até que o resultado se prepare. Se nenhuma exceção foi lançada, get as void . Se uma exceção foi lançada, get reutilizada. Assíncrono, thread_executor lança um novo thread de execução e executa o lambda fornecido. Ele implicitamente co_return void e a tarefa está concluída. main é então desbloqueado.
# 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 ;
} Neste exemplo, iniciamos o programa criando um objeto de tempo de execução. Criamos um vetor cheio de números aleatórios e adquirimos o thread_pool_executor do tempo de execução e chamamos count_even . count_even é uma corotação que gera mais tarefas e co_await s para que eles terminem dentro. max_concurrency_level retorna a quantidade máxima de trabalhadores que o executor suporta, no caso do executor Threadpool, o número de trabalhadores é calculado a partir do número de núcleos. Em seguida, particionamos a matriz para corresponder ao número de trabalhadores e enviarmos todos os pedaços a serem processados em sua própria tarefa. Assíncrono, os trabalhadores contam quantos números de pares contêm e co_return o resultado. count_even resume todos os resultados puxando a contagem usando co_await , o resultado final é então co_return ed. O encadeamento principal, que foi bloqueado pelo get não está bloqueado e a contagem total é retornada. A principal imprime o número de números pares e o programa termina graciosamente.
Toda operação grande ou complexa pode ser dividida em etapas menores e em cadeia. As tarefas são operações assíncronas que implementam essas etapas computacionais. As tarefas podem ser executadas em qualquer lugar com a ajuda dos executores. Embora as tarefas possam ser criadas a partir de chamáveis regulares (como functores e lambdas), as tarefas são usadas principalmente com coroutinas, que permitem suspensão e retomada suaves. No Concurrencpp, o conceito de tarefa é representado pela classe concurrencpp::task . Embora o conceito de tarefa seja central para a Concurrenpp, os aplicativos raramente precisam criar e manipular objetos de tarefas, pois os objetos de tarefas são criados e agendados pelo tempo de execução sem ajuda externa.
O Concurrencpp permite que os aplicativos produzam e consumam coroutinas como a principal maneira de criar tarefas. A Concurrencpp suporta tarefas ansiosas e preguiçosas.
As tarefas ansiosas começam a executar o momento em que são invocadas. Esse tipo de execução é recomendado quando as aplicações precisam disparar uma ação assíncrona e consumir seu resultado posteriormente (incêndio e consumir posteriormente) ou ignorar completamente o resultado assíncrono (fogo e esquecer).
Tarefas ansiosas podem retornar result ou null_result . O tipo de devolução result diz à coroutina que passa o valor retornado ou a exceção arremessada (incêndio e consumo mais tarde), enquanto o tipo de retorno null_result diz à coroutina cair e ignorar qualquer um deles (fogo e esquecer).
Coroutines ansiosos podem começar a ser executados de maneira síncrona, no thread de chamadas. Esse tipo de coroutina é chamado de "coroutinas regulares". As coroutinas ansiosas do Concurrencpp também podem começar a funcionar em paralelo, dentro de um determinado executor, esse tipo de corootina é chamado de "coroutinas paralelas".
Tarefas preguiçosas, por outro lado, começam a ser executadas apenas quando co_await ed. Esse tipo de tarefas é recomendado quando o resultado da tarefa deve ser consumido imediatamente após a criação da tarefa. Tarefas preguiçosas, sendo adiadas, são um pouco mais otimizadas para o caso de consumo imediato, pois não precisam de sincronização especial de roscas para passar o resultado assíncrono de volta ao seu consumidor. O compilador também pode otimizar algumas alocações de memória necessárias para formar a promessa subjacente à coroagem. Não é possível demitir uma tarefa preguiçosa e executar algo mais-o disparo de uma corotagem preguiçosa de calendas significa necessariamente a suspensão da corota-corota. A coroutina de chamadas só será retomada quando a corotagem preguiçosa for concluída. Tarefas preguiçosas podem retornar apenas lazy_result .
Tarefas preguiçosas podem ser convertidas para tarefas ansiosas chamando lazy_result::run . Este método executa a tarefa preguiçosa em linha e retorna um objeto result que monitora a tarefa recém -iniciada. Se os desenvolvedores não tiverem certeza de qual tipo de resultado usar, eles são incentivados a usar resultados preguiçosos, pois podem ser convertidos em resultados regulares (ansiosos), se necessário.
Quando uma função retorna qualquer um dos lazy_result , result ou null_result e contém pelo menos um co_await ou co_return em seu corpo, a função é uma coroutina concorrente. Cada coroutina simultaneira válida é uma tarefa válida. Em nosso exemplo, o exemplo acima, count_even é uma corotagem. Primeiro, geramos count_even , depois dentro dele, o executor Threadpool gerou mais tarefas de crianças (que são criadas a partir de callables regulares), que acabaram sendo unidas usando co_await .
Um executor do Concurrencpp é um objeto capaz de agendar e executar tarefas. Os executores simplificam o trabalho de gerenciar recursos, como threads, threads e filas de tarefas, dissociando -os do código do aplicativo. Os executores fornecem uma maneira unificada de agendar e executar tarefas, pois todos estendem concurrencpp::executor .
executor 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);
};Como mencionado acima, o Concurrencpp fornece executores comumente usados. Esses tipos de executor são:
Executor do pool de threads - um executor de uso geral que mantém um pool de threads. O executor do pool de threads é adequado para tarefas curtas ligadas à CPU que não bloqueiam. Os aplicativos são incentivados a usar esse executor como executor padrão para tarefas não bloqueadoras. O pool de threads Concurrencpp fornece injeção dinâmica de roscas e equilíbrio dinâmico de trabalho.
Executor de segundo plano - um executor do Threadpool com um pool maior de threads. Adequado para lançar tarefas curtas de bloqueio, como consultas de IO e banco de dados. NOTA IMPORTANTE: Ao consumir resultados, esse executor retornou ao ligar submit e bulk_submit , é importante alternar a execução usando resume_on para um executor ligado à CPU, a fim de impedir que as tarefas ligadas à CPU sejam processadas dentro do Background_executor.
exemplo:
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_executorExecutor do thread - Um executor que inicia cada tarefa anexada para executar em um novo thread de execução. Os tópicos não são reutilizados. Este executor é bom para tarefas de longa execução, como objetos que executam um loop de trabalho ou operações de bloqueio longo.
Executor do thread do trabalhador - um único executor de thread que mantém uma única fila de tarefas. Adequado quando os aplicativos desejam um thread dedicado que execute muitas tarefas relacionadas.
Executor manual - um executor que não executa coroutinas por si só. O código do aplicativo pode executar tarefas previamente inseridas, invocando manualmente seus métodos de execução.
Executor derivável - uma classe base para executores definidos pelo usuário. Embora seja possível herdar diretamente do concurrencpp::executor , derivable_executor usa o padrão CRTP que oferece algumas oportunidades de otimização para o compilador.
Executor embutido - usado principalmente para substituir o comportamento de outros executores. A envolvimento de uma tarefa é equivalente a invocá -la em linha.
O mecanismo nu de um executor é encapsulado em seu método enqueue . Esse método envolve uma tarefa para execução e possui duas sobrecargas: uma sobrecarga recebe um único objeto de tarefa como um argumento e outro que recebe um período de objetos de tarefa. A segunda sobrecarga é usada para envolver um lote de tarefas. Isso permite uma melhor programação de heurísticas e diminuição da disputa.
Os aplicativos não precisam confiar apenas no enqueue , concurrencpp::executor fornece uma API para agendar chamadas de usuários, convertendo -os em objetos de tarefa nos bastidores. Os aplicativos podem solicitar aos executores que retornem um objeto de resultado que passa o resultado assíncrono do chamável fornecido. Isso é feito ligando para executor::submit e executor::bulk_submit . submit recebe um chamável e retorna um objeto de resultado. executor::bulk_submit recebe um span de callables e retorna um vector de objetos de resultado de uma maneira semelhante, submit obras. Em muitos casos, os aplicativos não estão interessados no valor ou exceção assíncrona. Nesse caso, os aplicativos podem usar executor:::post e executor::bulk_post para agendar um chamável ou um span de callables a serem executados, mas também diz a tarefa para abandonar qualquer valor retornado ou exceção lançada. Não passar o resultado assíncrono é mais rápido que a passagem, mas não temos como conhecer o status ou o resultado da tarefa em andamento.
post , bulk_post , submit e bulk_submit Use enqueue nos bastidores para o mecanismo de agendamento subjacente.
thread_pool_executor Além das post , submit , bulk_post e bulk_submit , o thread_pool_executor fornece esses métodos adicionais.
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 Além de post , submit , bulk_post e bulk_submit , o manual_executor fornece esses métodos adicionais.
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);
}; Valores e exceções assíncronos podem ser consumidos usando objetos de resultado do Concurrencpp. O tipo result representa o resultado assíncrono de uma tarefa ansiosa, enquanto lazy_result representa o resultado diferido de uma tarefa preguiçosa.
Quando uma tarefa (ansiosa ou preguiçosa) é concluída, ele retorna um valor válido ou lança uma exceção. Em ambos os casos, esse resultado assíncrono é passado para o consumidor do objeto de resultado.
Os objetos result formam coroutinas assimétricas-a execução de uma corota-coroutina não é efetuada pela execução de uma callee-coroutina, ambas as coroutinas podem ser executadas de forma independente. Somente ao consumir o resultado da Callee-coroutine, a corota-corotação pode ser suspensa aguardando o callee para concluir. Até aquele momento, ambas as coroutinas funcionam de forma independente. O Callee-coroutine executa se seu resultado é consumido ou não.
lazy_result Objetos formam coroutinas simétricas-a execução de uma coroartina de callee ocorre somente após a suspensão da coroagem-corotação. Ao aguardar um resultado preguiçoso, a coroutina atual é suspensa e a tarefa preguiçosa associada ao resultado preguiçoso começa a ser executado. Depois que a Callee-Coroutine conclui e produz um resultado, a corota-corotação é retomada. Se um resultado preguiçoso não for consumido, sua tarefa preguiçosa associada nunca começa a ser executada.
Todos os objetos de resultado são um tipo de movimento e, como tal, eles não podem ser usados após a transferência de seu conteúdo para outro objeto de resultado. Nesse caso, o objeto de resultado é considerado vazio e tenta chamar qualquer método que não seja operator bool e operator = lançará uma exceção.
Depois que o resultado assíncrono foi retirado do objeto Resultado (por exemplo, chamando get ou operator co_await ), o objeto de resultado fica vazio. O vazio pode ser testado com operator bool .
Aguardando um resultado significa suspender a coroutina atual até que o objeto de resultado esteja pronto. Se um valor válido foi retornado da tarefa associada, ele será retornado do objeto Resultado. Se a tarefa associada lançar uma exceção, ela será re-arruinada. No momento de aguardar, se o resultado já estiver pronto, a coroutina atual retoma imediatamente. Caso contrário, é retomado pelo encadeamento que define o resultado ou exceção assíncrona.
Resolver um resultado é semelhante a aguardá -lo. A diferença é que a expressão co_await retornará o próprio objeto de resultado, em uma forma não vazia, em um estado pronto. O resultado assíncrono pode ser puxado usando get ou co_await .
Todo objeto de resultado tem um status indicando o estado do resultado assíncrono. O status de resultado varia de result_status::idle (o resultado assíncrono ou a exceção ainda não foi produzido) para result_status::value (a tarefa associada terminou graciosamente ao retornar um valor válido) para result_status::exception (a tarefa encerrada por uma exceção). O status pode ser consultado chamando (lazy_)result::status .
result O tipo result representa o resultado de uma tarefa contínua e assíncrona, semelhante à std::future .
Além de aguardar e resolver objetos de resultados, eles também podem ser esperados chamando qualquer result::wait , result::wait_for , result::wait_until ou result::get . Esperar que um resultado termine é uma operação de bloqueio (no caso, o resultado assíncrono não está pronto) e suspenderá todo o segmento de execução aguardando o resultado assíncrono estar disponível. As operações de espera são geralmente desencorajadas e permitidas apenas em tarefas de nível de raiz ou em contextos que permitem, como bloquear o segmento principal que aguarda o restante do aplicativo terminar graciosamente ou usar concurrencpp::blocking_executor ou concurrencpp::thread_executor .
Aguardando objetos de resultado usando co_await (e, ao fazer isso, transformar também a função/tarefa atual em uma coroutina) é a maneira preferida de consumir objetos de resultado, pois não bloqueia threads subjacentes.
result 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 TIPOUm objeto de resultado preguiçoso representa o resultado de uma tarefa preguiçosa diferida.
lazy_result tem a responsabilidade de iniciar a tarefa preguiçosa associada e passar seu resultado diferido de volta ao seu consumidor. Quando aguardado ou resolvido, o resultado preguiçoso suspende a coroutina atual e inicia a tarefa preguiçosa associada. Quando a tarefa associada é concluída, seu valor assíncrono é passado para a tarefa de chamadas, que é retomada.
Às vezes, uma API pode retornar um resultado preguiçoso, mas os aplicativos precisam de sua tarefa associada para executar ansiosamente (sem suspender a tarefa do chamador). Nesse caso, tarefas preguiçosas podem ser convertidas em tarefas ansiosas, chamando run de seu resultado preguiçoso associado. Nesse caso, a tarefa associada começará a ser executada em linha, sem suspender a tarefa do chamador. O resultado preguiçoso original é esvaziado e um objeto result válido que monitora a tarefa recém -iniciada será retornada.
lazy_result 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 ();
};Coroutinas ansiosas regulares começam a correr de forma síncrona no encadeamento de execução. A execução pode mudar para outro thread de execução se uma coroutina sofrer um reagendamento, por exemplo, aguardando um objeto de resultado não pronto dentro dele. O Concurrencpp também fornece coroutinas paralelas, que começam a ser executadas dentro de um determinado executor, não no encadeamento de execução. Esse estilo de programação de coroutinas é especialmente útil ao escrever algoritmos paralelos, algoritmos recursivos e algoritmos simultâneos que usam o modelo de joio de garfo.
Toda coroutina paralela deve atender às seguintes pré -condições:
result / null_result .executor_tag como seu primeiro argumento.type* / type& / std::shared_ptr<type> , onde type é uma classe concreta de executor como seu segundo argumento.co_await ou co_return em seu corpo. Se tudo acima se aplicar, a função é uma coroutina paralela: o Concurrencpp iniciará a coroutina suspensa e a reagendará imediatamente para executar no executor fornecido. concurrencpp::executor_tag é um espaço reservado dummy para dizer ao tempo de execução do Concurrencpp que essa função não é uma função regular, ele precisa começar a ser executado dentro do executor especificado. Se o executor passou para a coroutina paralela for nulo, a coroutina não começará a ser executada e uma exceção std::invalid_argument será lançada de maneira síncrona. Se todas as pré -condições forem atendidas, os aplicativos poderão consumir o resultado da coroutina paralela usando o objeto de resultado retornado.
Neste exemplo, calculamos o 30º membro da sequência de Fibonacci de maneira paralela. Começamos a lançar cada etapa de Fibonacci em sua própria corotina paralela. O primeiro argumento é um dummy executor_tag e o segundo argumento é o executor Threadpool. Toda etapa recursiva chama uma nova coroutina paralela que é executada em paralelo. Cada resultado é co_return para sua tarefa pai e adquirido usando co_await .
Quando consideramos a entrada pequena o suficiente para ser calculada de maneira síncrona (quando curr <= 10 ), paramos de executar cada etapa recursiva em sua própria tarefa e apenas resolver o algoritmo de maneira síncrona.
# 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 ;
} Para comparar, é assim que o mesmo código é escrito sem o uso de coroutinas paralelas e confiando no executor::submit sozinho. Como fibonacci retorna um result<int> , enviá -lo recursivamente via executor::submit resultará em um 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 ;
} Os objetos de resultado são a principal maneira de passar dados entre tarefas no Concurrencpp e vimos como executores e coroutinas produzem esses objetos. Às vezes, queremos usar os recursos dos objetos de resultado com não tarefas, por exemplo, ao usar uma biblioteca de terceiros. Nesse caso, podemos concluir um objeto de resultado usando um result_promise . result_promise se assemelha a um objeto std::promise - os aplicativos podem definir manualmente o resultado ou exceção assíncrona e tornar o objeto result associado pronto.
Assim como os objetos de resultado, as promessas de resultados são um tipo de movimento que fica vazio após o movimento. Da mesma forma, após definir um resultado ou uma exceção, a promessa de resultado também se torna vazia. Se uma promessa de resultado sair do escopo e nenhum resultado/exceção foi definido, o destro-promotor de resultados define um concurrencpp::errors::broken_task Excection usando o método set_exception . As tarefas suspensas e bloqueadas aguardando o objeto de resultado associado são retomadas/desbloqueadas.
As promessas de resultado podem converter o estilo de retorno de chamada em async/await o estilo de código: sempre que um componente exigir um retorno de chamada para passar no resultado assíncrono, podemos passar um retorno de chamada que chama set_result ou set_exception (dependendo do resultado assíncrono em si) da promessa de resultados aprovados e devolver o resultado associado.
result_promise 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 Exemplo: Neste exemplo, result_promise é usado para empurrar dados de um thread e pode ser retirado de seu objeto result associado de outro thread.
# 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 ();
} Neste exemplo, usamos std::thread como executor de terceiros. Isso representa um cenário quando um executor não concorrente é usado como parte do ciclo de vida do aplicativo. Extraímos o objeto de resultado antes de passarmos a promessa e bloquear o segmento principal até que o resultado se prepare. Em my_3_party_executor , definimos um resultado como se o co_return .
Os resultados compartilhados são um tipo especial de objetos de resultado que permitem que vários consumidores acessem o resultado assíncrono, semelhante ao std::shared_future . Diferentes consumidores de diferentes encadeamentos podem chamar funções como await , get e resolve de maneira segura.
Os resultados compartilhados são construídos a partir de objetos de resultados regulares e, diferentemente dos objetos de resultados regulares, eles são copiáveis e móveis. Como tal, shared_result se comporta como std::shared_ptr Type. Se uma instância de resultado compartilhada for movida para outra instância, a instância ficará vazia e tentar acessá -la lançará uma exceção.
Para apoiar vários consumidores, os resultados compartilhados retornam uma referência ao valor assíncrono em vez de movê -lo (como resultados regulares). Por exemplo, um shared_result<int> retorna um int& quando get , await etc. são chamados. Se o tipo subjacente do shared_result for void ou um tipo de referência (como int& ), eles serão devolvidos como de costume. Se o resultado assíncrono é uma exceção arremessada, ele é re-arestido.
Observe que, ao adquirir o resultado assíncrono usando shared_result de vários threads, é seguro, o valor real pode não ser seguro. Por exemplo, vários threads podem adquirir um número inteiro assíncrono recebendo sua referência ( int& ). Não torna o próprio número inteiro seguro. Não há problema em sofrer mutações o valor assíncrono se o valor assíncrono já estiver seguro. Como alternativa, as aplicações são incentivadas a usar tipos const para começar (como const int ) e adquirir referências constantes (como const int& ) que impedem a mutação.
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 : Neste exemplo, um objeto result é convertido em um objeto shared_result e uma referência a um resultado assíncrono int é adquirida por muitas tarefas geradas com 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 ;
} Quando o objeto de tempo de execução fica fora do escopo do main , ele itera cada executor armazenado e chama seu método shutdown . Tentando acessar o timer-quere ou qualquer executor lançará uma exceção errors::runtime_shutdown . Quando um executor desliga, ele limpa suas filas de tarefas internas, destruindo objetos task não executados. Se um objeto de tarefa armazenar uma corota concorrente, essa coroutina será retomada em linha e uma exceção errors::broken_task será lançada dentro dele. Em qualquer caso em que seja lançada uma exceção de execução runtime_shutdown ou broken_task , os aplicativos devem encerrar seu fluxo de código atual graciosamente o mais rápido possível. Essas exceções não devem ser ignoradas. runtime_shutdown e broken_task herdam de errors::interrupted_task Classe, e esse tipo também pode ser usado em uma cláusula catch para lidar com a terminação de maneira unificada.
Muitos ações assíncronas concurspp exigem uma instância de um executor como executor de currículo . Quando uma ação assíncrona (implementada como coroutina) pode terminar de forma síncrona, ela retoma imediatamente o encadeamento de execução. Se a ação assíncrona não puder terminar de forma síncrona, ela será retomada quando terminar, dentro do currículo-executor. Por exemplo, when_any a função utilitária requer uma instância de um executivo de currículo como seu primeiro argumento. when_any retorna um lazy_result que fica pronto quando pelo menos um resultado dado se preparar. Se um dos resultados já estiver pronto no momento de chamar when_any , a Calling Coroutine será retomada de forma síncrona no encadeamento de execução. Caso contrário, a coroutina de chamada será retomada quando pelo menos o resultado for concluído, dentro do currículo-executor dado. Os executores de retomar são importantes porque exigem onde as coroutinas são retomadas nos casos em que não está claro onde uma coroutina deve ser retomada (por exemplo, no caso de when_any e when_all ), ou nos casos em que a ação assíncrona é processada dentro de um dos trabalhadores do Concurrencpp, que são usados apenas para processar a ação específica e não o código de aplicação.
make_ready_result make_ready_result cria um objeto de resultado pronto a partir de argumentos determinados. Aguardando esse resultado fará com que a corota atual seja retomada imediatamente. get e operator co_await retornará o valor construído.
/*
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 cria um objeto de resultado pronto a partir de uma determinada exceção. Aguardando esse resultado fará com que a corota atual seja retomada imediatamente. get e operator co_await irá reverter a exceção fornecida.
/*
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 função when_all é uma função de utilidade que cria um objeto de resultado preguiçoso que fica pronto quando todos os resultados de entrada forem concluídos. Aguardando esse resultado preguiçoso retorna todos os objetos de resumo de entrada em um estado pronto, pronto para serem consumidos.
when_all Function vem com três sabores - um que aceita uma gama heterogênea de objetos de resultado, outro que leva um par de iteradores a uma variedade de objetos de resultado do mesmo tipo e, finalmente, uma sobrecarga que não aceita objetos de resultados. No caso de nenhum objetos de resultado de entrada - a função retorna um objeto de resultado pronto de uma tupla vazia.
Se um dos objetos de resultados passados estiver vazio, uma exceção será lançada. Nesse caso, os objetos de entrada de entrada não são afetados pela função e podem ser usados novamente após a manipulação da exceção. Se todos os objetos de resultado de entrada forem válidos, eles serão esvaziados por essa função e retornados em um estado válido e pronto como resultado de saída.
Atualmente, when_all aceita apenas objetos result .
Todas as sobrecargas aceitam um executor de currículo como seu primeiro parâmetro. Ao aguardar um resultado devolvido por when_all , a corotina de chamadas será retomada pelo executor do currículo fornecido.
/*
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 função when_any é uma função de utilidade que cria um objeto de resultado preguiçoso que se prepara quando pelo menos um resultado de entrada for concluído. Aguardando esse resultado retornará uma estrutura auxiliar contendo todos os objetos de entrada de entrada mais o índice da tarefa preenchida. Pode ser que, no momento do consumo do resultado pronto, outros resultados já tenham concluído de forma assíncrona. Os aplicativos podem ligar when_any repetidamente para consumir resultados prontos à medida que concluírem até que todos os resultados sejam consumidos.
when_any Função vem com apenas dois sabores - um que aceita uma gama heterogênea de objetos de resultado e outro que leva um par de iteradores a uma variedade de objetos de resultados do mesmo tipo. Ao contrário de when_all , não há significado em aguardar pelo menos uma tarefa para finalizar quando o intervalo de resultados está completamente vazio. Portanto, não há sobrecarga sem argumentos. Além disso, a sobrecarga de dois iteradores lançará uma exceção se esses iteradores referirem um intervalo vazio (quando begin == end ).
Se um dos objetos de resultados passados estiver vazio, uma exceção será lançada. De qualquer forma, uma exceção é lançada, os objetos de resulto de entrada não são afetados pela função e podem ser usados novamente após a manipulação da exceção. Se todos os objetos de resultado de entrada forem válidos, eles serão esvaziados por essa função e retornados em um estado válido como resultado de saída.
Atualmente, when_any aceita apenas objetos result .
Todas as sobrecargas aceitam um executor de currículo como seu primeiro parâmetro. Ao aguardar um resultado retornado por when_any , a corotagem de chamadas será retomada pelo executor do currículo fornecido.
/*
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 Retorna um aguardável que suspenda a coroutina atual e o retoma dentro de dentro do executor . Esta é uma função importante que garante que uma coroutina esteja em execução correta. Por exemplo, os aplicativos podem agendar uma tarefa em segundo plano usando o background_executor e aguardar o objeto de resultado retornado. Nesse caso, a coroutina aguardando será retomada dentro do executor em segundo plano. Uma chamada para resume_on com outro executor ligado à CPU garante que as linhas de código ligadas à CPU não sejam executadas no executor em segundo plano assim que a tarefa em segundo plano for concluída. Se uma tarefa for remarcada para executar em outro executor usando resume_on , mas esse executor será desligado antes que possa retomar a tarefa suspensa, essa tarefa é retomada imediatamente e uma exceção erros::broken_task será lançada. Nesse caso, as aplicações precisam graciosamente.
/*
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);O Concurrencpp também fornece timers e filas de timer. Os temporizadores são objetos que definem ações assíncronas em execução em um executor em um intervalo de tempo bem definido. Existem três tipos de temporizadores - temporizadores regulares , timers onshot e objetos de atraso .
Timers regulares têm quatro propriedades que os definem:
Como outros objetos no Concurrencpp, os temporizadores são um tipo de movimento que pode estar vazio. Quando um cronômetro é destruído ou timer::cancel é chamado, o timer cancela suas tarefas agendadas, mas ainda não executadas. Tarefas em andamento não são efetivas. O chamável do timer deve ser seguro. Recomenda -se definir o devido tempo e a frequência dos temporizadores a uma granularidade de 50 milissegundos.
Uma fila de timer é um trabalhador do Concurrencpp que gerencia uma coleção de temporizadores e os processa em apenas um tópico de execução. É também o agente usado para criar novos temporizadores. Quando um prazo para o timer (seja o tempo de devido tempo ou frequência do timer) atingiu, a fila do timer "dispara" o temporizador, agendo seu chamável para executar no executor associado como uma tarefa.
Assim como os executores, as filas do timer também aderem ao conceito RAII. Quando o objeto de tempo de execução fica fora do escopo, ele desliga a fila do timer, cancelando todos os temporizadores pendentes. Depois que uma fila de timer foi desligada, qualquer chamada subsequente para make_timer , make_onshot_timer e make_delay_object lançará uma exceção errors::runtime_shutdown . As aplicações não devem tentar desligar as filas do timer por si mesmas.
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 : 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 ;
};Neste exemplo, criamos um cronômetro regular usando a fila do timer. O timer agenda seu chamável para ser executado após 1,5 segundos e depois dispara seu chamável a cada 2 segundos. O chamável dado é executado no executor do 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 ;
}Um cronômetro OneShot é um cronômetro único, com apenas um tempo - depois de agendar seu chamável para ser executado assim que nunca o reagende para ser executado novamente.
Neste exemplo, criamos um cronômetro que é executado apenas uma vez - após 3 segundos de sua criação, o temporizador agendará seu chamável para executar em um novo thread de execução (usando 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 ;
} Um objeto de atraso é um objeto de resultado preguiçoso que se prepara quando é co_await ed e seu devido tempo é atingido. Os aplicativos podem co_await esse objeto de resultado para adiar a corotação atual de uma maneira não bloqueadora. A coroutina atual é retomada pelo executor que foi passado para make_delay_object .
Neste exemplo, geramos uma tarefa (que não retorna nenhum resultado ou a exceção lançada), que atrasa -se em um loop chamando co_await em um objeto de atraso.
# 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 ;
} Um gerador é uma coroutina síncrona preguiçosa que é capaz de produzir um fluxo de valores para consumir. Os geradores usam a palavra -chave co_yield para produzir valores de volta aos seus consumidores.
Os geradores devem ser usados de forma síncrona - eles só podem usar a palavra -chave co_yield e não devem usar a palavra -chave co_await . Um gerador continuará a produzir valores enquanto a palavra -chave co_yield for chamada. Se a palavra -chave co_return for chamada (explícita ou implicitamente), o gerador parará de produzir valores. Da mesma forma, se uma exceção for lançada, o gerador parará de produzir valores e a exceção arremessada será re-coroada ao consumidor do gerador.
Os geradores devem ser usados em um range-for o loop: os geradores produzem implicitamente dois iteradores - begin e end que controlam a execução do loop for . Esses iteradores não devem ser manuseados ou acessados manualmente.
Quando um gerador é criado, ele começa como uma tarefa preguiçosa. Quando seu método begin é chamado, o gerador é retomado pela primeira vez e um iterador é retornado. A tarefa preguiçosa é retomada repetidamente ligando para operator++ no iterador retornado. O iterador devolvido será igual ao end do iterador quando o gerador terminar a execução, saindo graciosamente ou lançando uma exceção. Como mencionado anteriormente, isso acontece nos bastidores pelo mecanismo interno do loop e pelo gerador e não deve ser chamado diretamente.
Como outros objetos no Concurrencpp, os geradores são do tipo apenas de movimento. Depois que um gerador foi movido, ele é considerado vazio e tentando acessar seus métodos internos (exceto operator bool ) lançará uma exceção. O vazio de um gerador geralmente não deve ocorrer - é aconselhável consumir geradores após sua criação em um loop for e não tentar chamar seus métodos individualmente.
generator 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 : Neste exemplo, escreveremos um gerador que produz o n -th Membro da sequência S(n) = 1 + 2 + 3 + ... + n onde 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 ;
} Bloqueios síncronos regulares não podem ser usados com segurança nas tarefas por vários motivos:
std::mutex , devem ser bloqueados e desbloqueados no mesmo thread de execução. Desbloqueando uma trava síncrona em um encadeamento que não travou, é um comportamento indefinido. Como as tarefas podem ser suspensas e retomadas em qualquer encadeamento de execução, os bloqueios síncronos serão interrompidos quando usados nas tarefas internas. concurrencpp::async_lock resolve esses problemas, fornecendo uma API semelhante a std::mutex , com a principal diferença que exige o concurrencpp::async_lock retornará um recente preguiçoso que pode ser co_awaited com segurança nas tarefas. Se uma tarefa tentar bloquear um bloqueio de assíncrona e falhar, a tarefa será suspensa e será retomada quando o bloqueio for desbloqueado e adquirido pela tarefa suspensa. Isso permite que os executores processem uma enorme quantidade de tarefas esperando para adquirir uma fechadura sem troca de contexto cara e chamadas caras de kernel.
Semelhante à forma como std::mutex funciona, apenas uma tarefa pode adquirir async_lock a qualquer momento, e uma barreira de leitura está no momento da aquisição. A liberação de um bloqueio assíncrono coloca uma barreira de gravação e permite que a próxima tarefa a adquira, criando uma cadeia de um modificador em um momento que vê as mudanças que outros modificadores fizeram e publica suas modificações para os próximos modificadores ver.
Como std::mutex , concurrencpp::async_lock não é recursivo . Atenção extra deve ser dada ao adquirir esse bloqueio - um bloqueio não deve ser adquirido novamente em uma tarefa que foi gerada por outra tarefa que já havia adquirido o bloqueio. Nesse caso, ocorrerá uma trava morta inevitável. Ao contrário de outros objetos no Concurrencpp, async_lock não são copiáveis nem móveis.
Como os bloqueios padrão, concurrencpp::async_lock deve ser usado com embalagens com escopo que alavancam o idioma C ++ RAII para garantir que os bloqueios sejam sempre desbloqueados no retorno da função ou na exceção. async_lock::lock retorna um recente preguiçoso de um invólucro com escopo que chama async_lock::unlock a destruição. Os usos brutos do async_lock::unlock são desencorajados. concurrencpp::scoped_async_lock atua como o invólucro com escopo e fornece uma API que é quase idêntica ao std::unique_lock . concurrencpp::scoped_async_lock é móvel, mas não copiável.
async_lock::lock e scoped_async_lock::lock requerem um executivo de currículo como seu parâmetro. Ao chamar esses métodos, se o bloqueio estiver disponível para bloqueio, ele estará bloqueado e a tarefa atual será retomada imediatamente. Caso contrário, a tarefa atual será suspensa e será retomada dentro do executivo do currículo fornecido quando o bloqueio for finalmente adquirido.
concurrencpp::scoped_async_lock envolve um async_lock e verifique se ele é desbloqueado corretamente. Como std::unique_lock , há casos em que ele não envolve nenhum bloqueio e, neste caso, é considerado vazio. Um scoped_async_lock vazio pode acontecer quando for construído, movido ou scoped_async_lock::release é chamado. Um bloqueio vazio de escopo não desbloqueará nenhum bloqueio na destruição.
Mesmo que o bloqueio-asincrono de escopo não esteja vazio, isso não significa que ele possua o Async-Lock subjacente e o desbloqueará na destruição. Os bloqueios de scop-async não vazios e não proprietários podem acontecer se scoped_async_lock::unlock foi chamado ou o escopo-async-lock foi construído usando o construtor scoped_async_lock(async_lock&, std::defer_lock_t) .
async_lock 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 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 : Neste exemplo, levamos 10.000.000 inteiros a um objeto std::vector de diferentes tarefas simultaneamente, enquanto usamos async_lock para garantir que nenhuma corrida de dados ocorra e a correção do estado interno desse objeto vetorial seja preservado.
# 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 imita o padrão condition_variable e pode ser usado com segurança com tarefas juntamente com async_lock . async_condition_variable trabalha com async_lock para suspender uma tarefa até que alguma memória compartilhada (protegida pelo bloqueio) mude. As tarefas que desejam monitorar as alterações de memória compartilhada bloquearão uma instância do async_lock e chamam async_condition_variable::await . Isso desbloqueia atomicamente o bloqueio e suspenderá a tarefa atual até que alguma tarefa modificadora notifique a variável de condição. Uma tarefa modificadora adquire o bloqueio, modifica a memória compartilhada, desbloqueia o bloqueio e ligue para notify_one ou notify_all . Quando uma tarefa suspensa é retomada (usando o executor do currículo que foi dado para await ), ele trava a trava novamente, permitindo que a tarefa continue a partir do ponto de suspensão sem problemas. Como async_lock , async_condition_variable não são móveis ou copiáveis - ele deve ser criado em um só lugar e acessado por várias tarefas.
async_condition_variable::await sobrecarga requerem um currículo-executor, que será usado para retomar a tarefa e um scoped_async_lock bloqueado_async_lock. async_condition_variable::await vem com duas sobrecargas - uma que aceita um predicado e que não. A sobrecarga que não aceita um predicado suspenderá a tarefa de chamada imediatamente após a invocação até que seja retomada por uma chamada para notify_* . A sobrecarga que aceita um predicado funciona, permitindo que o predicado inspecione a memória compartilhada e suspenda a tarefa repetidamente até que a memória compartilhada atinja seu estado desejado. esquematicamente, funciona como ligar
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`
} Assim como a variável de condição padrão, os aplicativos são incentivados a usar a carga de predicados, pois permite um controle mais refinado sobre suspensões e reposições. async_condition_variable podem ser usados para gravar coleções e estruturas de dados simultâneas, como filas e canais simultâneos.
Internamente, async_condition_variable mantém uma operação de suspensão, na qual as tarefas se envolvem quando aguardam a variável de condição a ser notificada. Quando qualquer um dos métodos notify_* é chamado, a tarefa notificada dequea uma tarefa ou todas as tarefas, dependendo do método invocado. As tarefas são descentadas a partir da operação da suspensão de maneira FIFO. Por exemplo, se a tarefa A await e, em seguida, a Tarefa B chamadas await , a tarefa C chama notify_one , então a tarefa A interna será desquedada e retomada. A Tarefa B permanecerá suspensa até que outra chamada para notify_one ou notify_all seja chamada. Se a Tarefa A e a Tarefa B estiverem suspensas e as chamadas da Tarefa C notify_all , as duas tarefas serão descentadas e retomadas.
async_condition_variable 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 : Neste exemplo, async_lock e async_condition_variable trabalham juntos para implementar uma fila simultânea que pode ser usada para enviar dados (neste exemplo, números inteiros) entre tarefas. Observe que alguns métodos retornam um result enquanto outro retorna lazy_result , mostrando como tarefas ansiosas e preguiçosas podem funcionar juntas.
# 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 ;
} O objeto de tempo de execução Concurrencpp é o agente usado para adquirir, armazenar e criar novos executores.
O tempo de execução deve ser criado como um tipo de valor assim que a função principal começar a ser executada. Quando o tempo de execução do Concurrencpp sai do escopo, ele itera sobre seus executores armazenados e os desliga um por um chamando executor::shutdown . Os executores saem de seu loop de trabalho interno e qualquer tentativa subsequente de agendar uma nova tarefa lançará uma exceção concurrencpp::runtime_shutdown . O tempo de execução também contém a fila global do timer usada para criar timers e atrasar objetos. Após a destruição, os executores armazenados destroem tarefas não executadas e aguardam o término das tarefas em andamento. Se uma tarefa em andamento tentar usar um executor para gerar novas tarefas ou agendar sua própria continuação de tarefas - uma exceção será lançada. Nesse caso, as tarefas contínuas precisam sair o mais rápido possível, permitindo que seus executores subjacentes desistissem. A fila do timer também será fechada, cancelando todos os temporizadores em execução. Com esse estilo de código RAII, nenhuma tarefa pode ser processada antes da criação do objeto de tempo de execução e, enquanto/após o tempo de execução sair do escopo. Isso libera aplicativos simultâneos da necessidade de comunicar mensagens de rescisão explicitamente. As tarefas são executores de uso livre, desde que o objeto de tempo de execução esteja vivo.
runtime 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 ;
}; Em alguns casos, os aplicativos estão interessados em monitorar a criação e o término da linha, por exemplo, alguns alocadores de memória exigem que novos threads sejam registrados e não registrados em sua criação e rescisão. O tempo de execução do Concurrencpp permite definir um retorno de chamada de criação de thread e um retorno de chamada de terminação de thread. Esses retornos de chamada serão chamados sempre que um dos trabalhadores do Concurrencpp criar um novo thread e quando esse thread estiver terminando. Esses retornos de chamada são sempre chamados de dentro do thread criado/término, então std::this_thread::get_id sempre retornará o ID do thread relevante. A assinatura desses retornos de chamada é void callback (std::string_view thread_name) . thread_name é um título específico do Concurrencpp que é fornecido ao thread e pode ser observado em alguns depuradores que apresentam o nome do thread. O nome do thread não é garantido para ser único e deve ser usado para registro e depuração.
Para definir um retorno de chamada de criação de thread e/ou um retorno de chamada de terminação de thread, os aplicativos podem definir os membros do thread_started_callback e/ou thread_terminated_callback do runtime_options que são passados para o construtor de tempo de execução. Como esses retornos de chamada são copiados para cada trabalhador do Concurrencpp que pode criar threads, esses retornos de chamada precisam ser copiáveis.
# 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 ;
}Saída possível:
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
Os aplicativos podem criar seu próprio tipo de executor personalizado, herdando a classe derivable_executor . Há alguns pontos a serem considerados ao implementar executores definidos pelo usuário: o mais importante é lembrar que os executores são usados em vários threads, para que os métodos implementados devem ser seguros para threads.
Os novos executores podem ser criados usando runtime::make_executor . Os aplicativos não devem criar novos executores com instanciação simples (como std::make_shared ou simples new ), apenas usando runtime::make_executor . Além disso, os aplicativos não devem tentar reinstalar os executores internos do Concurrencpp, como o thread_pool_executor ou o thread_executor , esses executores devem ser acessados apenas através de suas instâncias existentes no objeto de tempo de execução.
Outro ponto importante é lidar com o desligamento corretamente: shutdown , shutdown_requested e enqueue deve monitorar o estado do executor e se comportar de acordo quando invocados:
shutdown deve informar os threads subjacentes para sair e depois se juntar a eles.shutdown pode ser chamado várias vezes, e o método deve lidar com esse cenário ignorando as chamadas subsequentes para shutdown após a primeira invocação.enqueue deve lançar uma exceção concurrencpp::errors::runtime_shutdown se shutdown já tivesse sido chamado antes. task A implementação dos executores é um dos casos raros em que os aplicativos precisam trabalhar com a classe concurrencpp::task diretamente. concurrencpp::task é uma std::function como objeto, mas com algumas diferenças. Como std::function , o objeto de tarefas armazena um chamável que atua como a operação assíncrona. Ao contrário std::function , task é um tipo de movimentação. Na invocação, os objetos de tarefa não recebem parâmetros e retornam void . Além disso, todo objeto de tarefas pode ser chamado apenas uma vez. Após a primeira invocação, o objeto de tarefas fica vazio. Invocar um objeto de tarefa vazio é equivalente a invocar um lambda vazio ( []{} ) e não lançará nenhuma exceção. Os objetos de tarefa recebem seu chamável como referência de encaminhamento ( type&& onde type é um parâmetro de modelo), e não por cópia (como std::function ). A construção do chamável armazenado acontece no local. Isso permite que os objetos de tarefas conterão as callables que são apenas o tipo de movimentação (como std::unique_ptr e concurrencpp::result ). Os objetos de tarefa tentam usar métodos diferentes para otimizar o uso dos tipos armazenados, por exemplo, os objetos de tarefas aplicam a otimização de curta duração (SBO) para chamadas pequenas e regulares, e encerram as chamadas para std::coroutine_handle<void> chamando-os diretamente sem o despacho virtual.
task 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 ;
}; Ao implementar os executores definidos pelo usuário, cabe à implementação armazenar objetos task (quando enqueue é chamado) e executá-los de acordo com o mecanismo interno do executor.
Neste exemplo, criamos um executor que registra ações como envolver tarefas ou executá -las. Implementamos a interface executor e solicitamos o tempo de execução para criar e armazenar uma instância ligando para runtime::make_executor . O restante do aplicativo se comporta exatamente o mesmo que se usássemos executores não definidos pelo usuário.
# 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 Com o CLANG e o GCC, também é possível executar os testes com o suporte do TSAN (SHANILIGADOR DO THREAGEM).
$ 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 Ao compilar no Linux, a biblioteca tenta usar libstdc++ por padrão. Se você pretende usar libc++ como sua implementação padrão da biblioteca, o sinalizador CMAKE_TOOLCHAIN_FILE deve ser especificado como abaixo:
$ cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/libc++.cmake -DCMAKE_BUILD_TYPE=Release -S . -B build /libAlternativamente, para construir e instalar a biblioteca manualmente, os desenvolvedores podem obter lançamentos estáveis do Concurrencpp por meio dos gerentes de pacotes VCPKG e Conan:
vcpkg:
$ vcpkg install concurrencppConan: Concurrencpp em Conancenter
O Concurrencpp vem com um programa de sandbox embutido que os desenvolvedores podem modificar e experimentar, sem precisar instalar ou vincular a biblioteca compilada a uma base de código diferente. Para brincar com a caixa de areia, os desenvolvedores podem modificar sandbox/main.cpp e compilar o aplicativo usando os seguintes comandos:
$ 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