O que é um Executor
Um executor em C++ é um objeto ou mecanismo que é responsável por gerenciar o agendamento e a execução de tarefas ou operações em uma ou mais threads. Ele pode ser usado para gerenciar a concorrência e a sincronização entre as tarefas, garantindo que elas sejam executadas de forma eficiente e consistente.
Um executor em C++ pode ser implementado de várias maneiras diferentes, dependendo das necessidades da aplicação. Algumas possibilidades incluem:
-
Usando threads: um executor pode ser implementado como um conjunto de threads que são responsáveis por executar as tarefas adicionadas a ele. Isso pode ser útil se a aplicação precisa de muitas tarefas sendo executadas concorrentemente e se houver recursos suficientes para suportar essas threads.
-
Usando um pool de threads: um executor pode ser implementado como um pool de threads que são compartilhados por todas as tarefas adicionadas a ele. Isso pode ajudar a gerenciar o uso de recursos, permitindo que mais tarefas sejam executadas concorrentemente, mas sem precisar criar novas threads para cada tarefa.
-
Usando um event loop: um executor pode ser implementado usando um event loop para gerenciar a execução de tarefas. Isso pode ser útil se a aplicação precisa lidar com múltiplos eventos simultâneos e se as tarefas só precisam ser executadas quando um evento ocorre.
O event loop é uma estrutura de loop de execução que é usada para gerenciar a entrada e a saída de eventos em uma aplicação. Ele é comumente usado em aplicações que precisam lidar com múltiplos eventos simultâneos, como entrada do usuário, atualizações de redes ou operações de tempo de espera. Em C++, um event loop pode ser implementado de várias maneiras diferentes, dependendo das necessidades da aplicação. Algumas possibilidades incluem:
-
Usando um loop infinito: um event loop pode ser implementado como um loop infinito que verifica periodicamente por eventos. Isso pode ser útil se a aplicação precisa verificar por eventos com frequência, mas não precisa de uma resposta muito rápida.
-
Usando notificações assíncronas: um event loop pode ser implementado usando notificações assíncronas para ser avisado quando um evento ocorre. Isso pode ser útil se a aplicação precisa de uma resposta rápida aos eventos e se houver muitos eventos ocorrendo com pouco tempo de espera entre eles.
Resumidamente, um executor pode ser usado em conjunto com um event loop para gerenciar a execução de tarefas que são disparadas por eventos. Por exemplo, se o event loop detecta um evento de entrada de usuário, ele pode adicionar uma tarefa ao executor para ser executada em uma thread separada, permitindo que a aplicação continue processando outros eventos enquanto a tarefa é executada. Isso pode ajudar a garantir que a aplicação responda de forma rápida e responsiva, mesmo quando há muitas tarefas a serem executadas.
Surgimento do assunto em torno do C++ STL
A proposta P2300, oficialmente denominada std::execution, introduz um modelo de programação assíncrona baseado em "senders" e "receivers" (remetentes e receptores). Ela foi projetada para ser uma alternativa genérica e componível aos callbacks e futures/promises.
Status atual: A proposta P2300 (std::execution) foi aceita para o C++26 (aprovada em 2024). Ela representa um modelo diferente dos executores do Asio — baseado em sender/receiver em vez de callbacks diretos.
O modelo sender/receiver de P2300 funciona assim:
- Um sender descreve um trabalho que ainda não foi iniciado
- Um receiver é o destino do resultado desse trabalho
- Um scheduler determina onde/quando o trabalho será executado
Este modelo é diferente dos executores do Asio, mas as duas abordagens são complementares. O Asio continua sendo a escolha prática para programação de redes assíncronas em C++ hoje.
A proposta anterior P0443R14 ("The Unified Executor for C++") foi o precursor do P2300 e influenciou muito o design atual.
Por quê Executores?
C++ sempre careceu de infraestrutura de programação simultânea disponível, e a infraestrutura recém-introduzida desde C++11, bem como a melhoria de bibliotecas de terceiros, como boost e folly, têm mais ou menos problemas e certas limitações.
std::async não é assíncrono
std::async é uma função do C++ Standard Library que é usada para iniciar uma tarefa assíncrona em um ponto específico no tempo. Ela é usada para criar uma tarefa assíncrona que será executada em uma thread separada e retorna um objeto std::future que pode ser usado para obter o resultado da tarefa quando ela for concluída.
Quando chamamos std::async, ela pode ou não iniciar a execução imediatamente, dependendo da política de execução fornecida como argumento (std::launch::async ou std::launch::defer). No entanto, a função sempre retorna um objeto std::future imediatamente, independentemente de a computação ter sido concluída ou não.
O objeto std::future representa um valor que pode não estar disponível imediatamente. Ao chamar um método em um objeto std::future, como std::future::get(), ele pode bloquear a thread atual até que o valor esteja pronto. Isso significa que, se chamarmos std::future::get() imediatamente após std::async, a chamada pode bloquear até que a computação seja concluída.
Diferentemente dostd::async, a expressão co_await [C++20] é usada para suspender a avaliação de uma função assíncrona (coroutine) enquanto aguarda a conclusão de uma computação representada pela expressão operando.
A ideia por trás da função std::async e std::future é permitir que o programador inicie uma tarefa assíncrona e continue a trabalhar em outras partes do código enquanto aguarda o resultado. No entanto, o programador precisa estar ciente de que o comportamento de bloqueio pode ocorrer se ele chamar std::future::get() imediatamente após std::async.
Apesar disso, std::async pode ser útil em situações em que é necessário iniciar uma tarefa assíncrona de forma fácil e rápida. Ele é especialmente útil quando é necessário obter o resultado da tarefa assíncrona de forma síncrona, usando a sintaxe de await do C++20 ou esperando pelo objeto std::future retornado por std::async.
A seguir, um exemplo de uso da função std::async para iniciar uma tarefa assíncrona e obter o resultado da tarefa de forma síncrona:
#include <iostream>
#include <future>
// Função que será executada assincronamente
int long_running_task(int x, int y) {
// Simulando um processamento demorado
std::this_thread::sleep_for(std::chrono::seconds(2));
return x + y;
}
int main() {
// Iniciando a tarefa assíncrona com std::async
std::future<int> result = std::async(long_running_task, 10, 20);
// Obtendo o resultado da tarefa síncronamente com std::future::get
int sum = result.get();
std::cout << "Resultado da tarefa assíncrona: " << sum << std::endl;
return 0;
}
Neste exemplo, a função long_running_task é iniciada de forma assíncrona com std::async e o resultado da tarefa é obtido síncronamente com std::future::get. Isso significa que o código que chama std::async será bloqueado até que a tarefa seja concluída e o resultado esteja disponível.
Observe que, apesar de usarmos std::async para iniciar a tarefa assíncrona, o código não pode ser escrito de forma assíncrona usando a sintaxe de await do C++20. Para escrever código assíncrono de forma mais simples e clara, é recomendável usar outras bibliotecas de tempo de execução, como Asio ou Libunifex.
Em resumo, std::async é uma função do C++ Standard Library que é usada para iniciar uma tarefa assíncrona em uma thread separada. Ela não é uma função assíncrona no sentido tradicional da palavra e não pode ser usada com a sintaxe de await do C++20, mas pode ser útil em situações em que é necessário iniciar uma tarefa assíncrona de forma fácil e rápida.
Modelo de Evolução do Future/Promise
No C++11, o modelo future/promise é um meio de permitir que uma thread espere por um valor a ser produzido por outra thread de maneira assíncrona. Ele é composto pelos seguintes elementos:
-
Promise: um objeto que permite que um valor seja definido em algum momento no futuro. A promessa possui um método setValue para definir o valor e um métodosetExceptionpara definir uma exceção a ser lançada quando o valor for solicitado. -
Future: um objeto que permite que uma thread espere por um valor produzido por outra thread. O futuro possui um método wait que faz a thread que o chama esperar até que o valor esteja disponível. Além disso, o futuro possui métodos comotheneonErrorque permitem que ações sejam executadas quando o valor estiver disponível ou uma exceção for lançada.
A implementação de um Future/Promise típico em C++ é mostrada na figura abaixo:

Para usar o modelo de futuros e promessas, é preciso criar um objeto promessa e obter um objeto futuro a partir dele. Em seguida, a thread que produzirá o valor deve definir o valor na promessa usando o método setValue ou setException. A thread que estiver esperando pelo valor pode usar o método wait do futuro para esperar até que o valor esteja disponível.
Aqui está um exemplo de como usar o modelo de futuros e promessas em C++:
#include <future>
#include <iostream>
int main() {
// Cria uma promessa e um futuro
std::promise<int> promise;
std::future<int> future = promise.get_future();
// Define o valor da promessa em uma thread separada
std::thread([&promise] {
std::this_thread::sleep_for(std::chrono::seconds(1));
promise.set_value(42);
}).detach();
// Espera pelo valor a ser definido e imprime-o
std::cout << future.get() << std::endl;
return 0;
}
Este código cria um objeto std::promise e um objeto std::future, e define o valor da std::promise em uma thread separada usando um std::thread. O objeto std::future é então usado para esperar pelo valor a ser definido, e o valor é impresso no console.
Essas classes são mais básicas do que as oferecidas pelas bibliotecas folly e asio, mas são parte da Biblioteca Padrão de C++ e, portanto, estão disponíveis em qualquer compilador C++ padrão.
As classes std::future e std::promise fornecem um conjunto similar de funcionalidades às classes folly::Future e folly::Promise da biblioteca folly, mas são parte da Biblioteca Padrão de C++ e não exigem dependências adicionais.
Executores do Asio
Os executores são componentes do asio que definem o contexto de execução de uma função ou um bloco de código. Eles podem ser usados para controlar como e quando uma função ou um bloco de código é executado, e permitem que você aproveite os recursos de concorrência fornecidos pelo asio para executar tarefas de forma assíncrona e concorrente.
O asio fornece várias formas de se trabalhar com executores, incluindo a possibilidade de especificar o contexto de execução de uma função ou um bloco de código usando o template asio::execution, ou usando funções como asio::post, que permitem agendar a execução de uma função ou um bloco de código em um determinado contexto de execução.
asio::execution é um modelo C++ que representa um contexto de execução, ou um objeto que define como uma função ou um bloco de código deve ser executado. É usado como um parâmetro de tipo em vários componentes asio, como asio::strand e asio::spawn, para especificar o contexto de execução no qual uma função ou um bloco de código deve ser executado.
Existem vários tipos de executores que podem ser usados com asio::execution. Estes incluem:
-
asio::io_context::executor_type: Este é o tipo de executor padrão paraasio::io_contexte representa o contexto de execução fornecido por um objetoasio::io_context. As funções ou blocos de código executados usando este executor serão executados no contexto do loop de eventos do io_context. -
asio::strand<Executor>: Este é um executor decorador que envolve outro executor, Executor, e garante que as funções ou blocos de código executados com ele sejam executados de forma serializada, ou seja, apenas um de cada vez. Isso pode ser útil para sincronizar o acesso a recursos compartilhados. -
asio::system_executor: Este é um tipo de executor especial que representa o contexto de execução fornecido pelo sistema operacional. As funções ou blocos de código executados usando este executor serão executados no contexto da thread pool do sistema operacional. -
asio::thread_pool: Este é um tipo de executor que representa um pool (grupo) de threads que podem serem usados para executar funções ou blocos de código, ou também para executar tarefas de forma concorrente em múltiplas threads. -
asio::post: Esta é uma função que leva uma função ou um bloco de código e um executor e agenda a função ou o bloco de código para ser executado no contexto do executor especificado.
Em geral, asio::execution é um conceito poderoso que permite especificar o contexto de execução no qual uma função ou um bloco de código deve ser executado e aproveitar os vários tipos de executores fornecidos por asio para controlar a execução do seu código.
Asio executores em comparação com outras alternativas
std::execution (C++26)
O namespace std::execution (aprovado para C++26 via P2300) fornece um modelo de programação assíncrona baseado em sender/receiver. É diferente dos executores de política de execução do C++17 (std::execution::sequenced_policy, std::execution::parallel_policy) — esses últimos controlam apenas algoritmos paralelos como std::sort, não operações de rede.
O modelo P2300 funciona com schedulers, senders e receivers:
// Exemplo conceitual com std::execution (C++26 / implementações experimentais)
// Nota: requer implementação como libunifex ou stdexec
namespace ex = std::execution;
auto result = ex::sync_wait(
ex::schedule(scheduler)
| ex::then([] { return 42; })
);
Importante:
std::execution::executenão existe como função padrão neste modelo. O P2300 usaex::start()e composição de senders. Não confundir com os executores do Asio, que têm API diferente.
O Asio continua sendo a escolha prática para programação de redes assíncronas em C++, independente do P2300. As duas abordagens são complementares.
Libunifex
Libunifex e Asio são duas bibliotecas diferentes que são utilizadas para criar aplicações de redes de forma assíncrona em C++.
ASIO é uma biblioteca de tempo de execução que oferece suporte para a comunicação assíncrona entre sistemas de computador. Ela é amplamente utilizada para a criação de aplicações de redes, como servidores de rede e clientes de rede. Asio fornece uma série de recursos, como sockets de rede, temporizadores e sinais de interrupção, que podem ser usados para criar aplicações de rede de forma assíncrona.
Por outro lado, Libunifex é uma biblioteca de executores para C++ que oferece uma interface uniforme para a execução de tarefas assíncronas em diferentes plataformas e bibliotecas de tempo de execução, como Asio Standalone e Boost.ASIO. Ela permite que você escreva código assíncrono de forma mais portável, pois você pode usar a mesma interface para trabalhar com diferentes bibliotecas de tempo de execução sem precisar se preocupar com as diferenças entre elas.
Cppcoro
Cppcoro foi uma biblioteca pioneira de corrotinas para C++ criada por Lewis Baker. Ela fornecia primitivas para escrever código assíncrono usando corrotinas do C++20.
Atenção: O cppcoro está largamente sem manutenção desde 2021. Não é recomendado para novos projetos. Alternativas ativas incluem:
Em resumo, o Asio é a biblioteca mais completa e ativa para criar aplicações de rede assíncronas em C++, com suporte tanto a callbacks quanto a corrotinas C++20. O Libunifex é experimental e voltado à pesquisa do modelo sender/receiver do P2300.