Ответ 1
Наблюдаемое поведение хорошо определено и ожидается, но не следует ожидать, что оно произойдет часто.
Asio имеет ограниченный набор реализаций строк, а стратегия распределения по умолчанию для цепочек - хеширование. Если произойдет хеш-столкновение, две нити будут использовать одну и ту же реализацию. Когда происходит хеш-столкновение, пример упрощается до следующей демонстрации:
#include <cassert>
#include <boost/asio.hpp>
int main()
{
boost::asio::io_service io_service;
boost::asio::io_service::strand strand1(io_service);
// Have strand2 use the same implementation as strand1.
boost::asio::io_service::strand strand2(strand1);
int value = 0;
auto handler1 = [&value, &strand1, &strand2]() {
assert(strand1.running_in_this_thread());
assert(strand2.running_in_this_thread());
value = 1;
// handler2 is queued into strand and never invoked.
auto handler2 = [&value]() { assert(false); };
strand2.post(handler2);
// handler3 is immediately executed.
auto handler3 = [&value]() { value = 3; };
strand2.dispatch(handler3);
assert(value == 3);
};
// Enqueue handler1.
strand1.post(handler1);
// Run the event processing loop, executing handler1.
assert(io_service.poll_one() == 1);
}
В приведенном выше примере:
-
io_service.poll_one()
выполняет один готовый обработчик (handler1
) -
handler2
никогда не вызывается -
handler3
вызывается непосредственно вstrand2.dispatch()
, посколькуstrand2.dispatch()
вызывается из обработчика, гдеstrand2.running_in_this_thread()
возвращаетtrue
Существуют различные детали, способствующие наблюдаемому поведению:
-
io_service::poll_one()
будет запускать цикл событийio_service
и без блокировки, он будет выполнять не более одного готового к запуску обработчика. Обработчики, выполняемые непосредственно в контекстеdispatch()
, никогда не помещаются в очередьio_service
и не подлежат ограничениюpoll_one()
вызова одного обработчика. -
Перегрузка
boost::asio::spawn(strand, function)
запускает стековый сопроцессор as-if черезstrand.dispatch()
:- Если
strand.running_in_this_thread()
возвращаетfalse
для вызывающего, тогда сопрограмма будет отправлена вstrand
для отложенного вызова - если
strand.running_in_this_thread()
возвращаетtrue
для вызывающего, тогда сопрограмма будет выполнена немедленно
- Если
-
Дискретные объекты
strand
, которые используют одну и ту же реализацию, все еще сохраняют гарантии нити. А именно, одновременное выполнение не будет происходить, и порядок обращения обработчика будет определен. Когда дискретные объектыstrand
используют дискретные реализации, а несколько потоков запускаютio_service
, тогда можно наблюдать одновременное выполнение отдельных цепей. Однако, когда дискретные объектыstrand
используют одну и ту же реализацию, не будут наблюдаться concurrency, даже если несколько потоков работают сio_service
. Это поведение задокументировано:Реализация не гарантирует, что обработчики, отправленные или отправленные через разные объекты strand, будут вызываться одновременно.
-
Asio имеет ограниченный набор реализаций строк. Текущее значение по умолчанию -
193
, и его можно контролировать, определяяBOOST_ASIO_STRAND_IMPLEMENTATIONS
на нужный номер. Эта функция отмечена в примечаниях к выпуску Boost.Asio 1.48Изменилось количество реализаций строк, задав
BOOST_ASIO_STRAND_IMPLEMENTATIONS
на нужный номер.Уменьшая размер пула, вы увеличиваете вероятность того, что две дискретные нити будут использовать одну и ту же реализацию. Если исходный код должен был установить размер пула
1
, тоstrand1
иstrand2
всегда будут использовать ту же реализацию, в результатеval
всегда будет3
(демо). -
Стратегия по умолчанию для реализации реализаций строк заключается в использовании хеша с золотым соотношением. Поскольку используется алгоритм хэширования, существует потенциал для коллизий, в результате чего одна и та же реализация используется для нескольких дискретных объектов
strand
. ОпределивBOOST_ASIO_ENABLE_SEQUENTIAL_STRAND_ALLOCATION
, можно изменить стратегию распределения на раунд-робин, предотвращая возникновение конфликта до тех пор, пока не произойдет выделение строкBOOST_ASIO_STRAND_IMPLEMENTATIONS + 1
. Эта функция отмечена в примечаниях к выпуску Boost.Asio 1.48:Добавлена поддержка нового флага
BOOST_ASIO_ENABLE_SEQUENTIAL_STRAND_ALLOCATION
, который переключает распределение реализаций строк для использования циклического подхода, а не хеширования.
Учитывая приведенные выше детали, происходит следующее, когда 1
наблюдается в исходном коде:
-
strand1
иstrand2
имеют дискретные реализации -
io_service::poll_one()
выполняет одиночный обработчик, который был отправлен непосредственно вstrand1
- обработчик, который был отправлен в
strand1
, устанавливаетval
в1
- обработчик, отправленный в
strand2
, находится в очереди и никогда не вызывается -
создание сопрограммы отложено, так как
strand
порядок гарантии обращения предотвращает создание сопрограммы, пока не выполнит предыдущий обработчик, который был отправлен вstrand2
:задан объект strand
s
, еслиs.post(a)
произойдет доs.dispatch(b)
, где последний выполняется за пределами строки, тогдаasio_handler_invoke(a1, &a1)
произойдет доasio_handler_invoke(b1, &b1)
.
С другой стороны, когда 3
наблюдается:
- происходит хеш-столкновение для
strand1
иstrand2
, в результате чего они используют ту же реализацию базовой строки -
io_service::poll_one()
выполняет одиночный обработчик, который был отправлен непосредственно вstrand1
- обработчик, который был отправлен в
strand1
, устанавливаетval
в1
- обработчик, отправленный в
strand2
, находится в очереди и никогда не вызывается - coroutine немедленно создается и вызывается внутри
boost::asio::spawn()
, устанавливаяval
в3
, так какstrand2
может безопасно выполнять сопрограмму, сохраняя при этом гарантию неконкурентного выполнения и порядка вызова обработчика