Я изучаю многопоточность и обнаружил, что async()
предпочтительнее использовать с io_context
. Но я не совсем понимаю, зачем они нужны. Я перечитал несколько документации и посмотрел много лекций на эту тему, но так и не смог понять, в чем разница, например, в этом коде:
boost::asio::io_context io;
io.post([](){
do_some_work()
});
std::thread thread([&io]() {
io.run();
});
из этого:
std::thread thread([&io]() {
do_some_work();
});
Я также видел много примеров использования io_context
с async
во взаимодействии клиент-сервер с так называемыми обработчиками. Но я просто не понимаю их смысла. Зачем они нужны, если можно просто писать методы подряд:
socket();
connect();
read();
handle();
Зачем использовать сложную конструкцию async
с io_context
, если можно написать всё структурно (как я сделал выше) с помощью future
/promise
и будет читабельно и понятно?
При синхронных вызовах сокетов вы можете обрабатывать только одно соединение на поток. Как правило, отправка и получение данных по сети занимает гораздо больше времени, чем обработка данных, это приводит к тому, что поток проводит большую часть своего времени в режиме ожидания в ожидании операций. завершить. На сервере, обрабатывающем множество соединений, создание нового потока для каждого соединения может оказаться неэффективным, поскольку создание и обслуживание потоков обычно обходится относительно дорого.
Асинхронный код помогает решить эту проблему: поток может выполнять обработку соединения, а затем асинхронно отправлять результаты, пока он ожидает завершения отправки, поток может перейти и обработать другое соединение. Это означает, что вы можете обрабатывать множество соединений одновременно, используя лишь небольшой пул потоков.
io_context
— это asio-реализация этого пула потоков. В вашем тривиальном примере да, его использование бесполезно, но более реалистичным примером будет:
boost::asio::io_context io;
void do_some_work(){
// Do some asynchronous operation
// Run the callback for it
io.post(async_complete());
}
for (int i = 0; i< 10; i++)
{
io.post([](){
do_some_work()
});
}
std::thread thread([&io]() {
io.run();
});
io_context
asio-реализация пула потоков?! Я почти уверен, что boost::asio::thread_pool
— это реализация пула потоков. Это во многом говорит об этом в названии. Фактически, весь смысл io.run()
в том, что io_context
нужен поток выполнения, чтобы действительно что-то сделать; у него нет своего.
@MSalters, ок, точнее, это рабочая очередь, которая затем может использоваться пулом потоков для выполнения задач, я больше пытался объяснить основные концепции асинхронного кода, а не технические детали asio
Хорошо, но разве мы не можем написать то же самое, просто создав поток и зациклившись в нем? Вот так: std::thread thread([&io]() { for (int i = 0; i< 10; i++) { do_some_work() } async_complete() });
Или основная цель использования io_context — выполнение операций в очереди?
он предназначен для выполнения операций в очереди, ваш цикл будет выполнять всю работу, а затем вызывать обратный вызов, версия asio будет выполнять каждую часть работы (возможно, все одновременно), а затем вызывать обратный вызов для каждой части работы, и все это в одном потоке
Чтобы ответить на последнюю часть вопроса: почему бы и нет future/promise
? Это потому, что это не сработает. Чтобы что-то сделать, вам понадобится std::async
вместо std::promise
. И это не имеет прямого контроля над потоками выполнения. std::launch::async
требуются новые темы или, по крайней мере, темы, которые выглядят новыми (локальные значения потоков очищены)
Более логичное решение вашего шаблона
socket();
connect();
while(...) {
read();
handle();
}
было бы
co_await socket();
co_await connect();
while(...) {
co_yield read();
}
(Использование сопрограмм C++20 - co_yield позволяет вызывающей стороне обрабатывать каждое возвращаемое значение)
Потоки — довольно сложная вещь для создания, а отдельные операции, запланированные в этом потоке, обходятся дешевле по сравнению с накладными расходами на создание потока. Кроме того, асинхронные операции для нескольких цепочек выполнения могут выполняться не по порядку и с пакетными результатами. Есть также вещи, которые вы не можете сделать с синхронным API, например, настроить буферы приема настолько заранее, что сетевая карта может фактически использовать их почти напрямую в качестве цели, без необходимости пробуждения вашего процесса.