Darren Cook
Darren Cook

Reputation: 28913

asio/strand: why is behaviour with timer different?

I've been following along with this excellent asio tutorial, but have got confused with exactly what strands do. My understanding was that they worked like a queue for a set of handlers (callbacks) such that the handlers in such a queue would execute in order. But some experiments suggest I am wrong. Can someone explain what they really are?

I'm starting with example 6c It executes PrintNum(1) to PrintNum(5), each with a 1 second delay, before triggering the timer. (Unintuitively, this also happens if I move the PrintNum calls to after starting the timer! Then I realized the request to call TimerHandler does not get on to the strand queue until the timer triggers.)

My first variation was to remove the strand references on just the timer, but leave them on PrintNum (see full code on gist):

strand->post( boost::bind( &PrintNum, 1 ) );
strand->post( boost::bind( &PrintNum, 2 ) );
strand->post( boost::bind( &PrintNum, 3 ) );
strand->post( boost::bind( &PrintNum, 4 ) );
strand->post( boost::bind( &PrintNum, 5 ) );

boost::shared_ptr< boost::asio::deadline_timer > timer(
        new boost::asio::deadline_timer( *io_service )
);
timer->expires_from_now( boost::posix_time::seconds( 1 ) );
timer->async_wait( boost::bind( &TimerHandler, _1, timer ) );

Now the timer runs independently of the PrintNum calls. I get the output I expected.

My question comes with my second variation (see gist), where I removed the strand calls for PrintNum, but kept them on the timer:

io_service->post( boost::bind( &PrintNum, 1 ) );
io_service->post( boost::bind( &PrintNum, 2 ) );
io_service->post( boost::bind( &PrintNum, 3 ) );
io_service->post( boost::bind( &PrintNum, 4 ) );
io_service->post( boost::bind( &PrintNum, 5 ) );

boost::shared_ptr< boost::asio::deadline_timer > timer(
        new boost::asio::deadline_timer( *io_service )
);
timer->expires_from_now( boost::posix_time::milliseconds( 1000 ) );
timer->async_wait(
        strand->wrap( boost::bind( &TimerHandler, _1, timer, strand ) )
);

(You'll see in the gist code that I'm shuffling it up a bit, but the behaviour is basically the same.)

What I expected here is that the strand would basically do nothing: I only ever one handler (TimerHandler) in the strand queue at a time. I therefore expected the timer to tick away independently of the PrintNum calls. But what I see is that the PrintNum calls still get priority: all 5 have to finish before the TimerHandler is allowed to execute.

(It is worth pointing out that example 6c in Drew Benton's tutorial was all about ensuring that neither of TimerHandler and PrintNum would run at the same time. My variations deliberately remove that guarantee; my starting point was wanting to understand the problem that example 6c is the solution to.)

Upvotes: 0

Views: 2058

Answers (1)

Darren Cook
Darren Cook

Reputation: 28913

I think I can answer myself now. In the second example, the problem is nothing to do with the use of strand; behaviour is (kind of) identical if the strand->wrap is removed. The problem is the threads are all busy! Increase the number of threads in the thread pool to 6 and we get the expected behaviour: the timer triggers 1 second after it is created. When there are 5 or fewer threads, then TimerHandler will not get called until the first of the PrintNum calls finish.

The other important thing to understanding this example, as I'd already noted in the question, is that it is strand->wrap() that gets triggered by the timer, not TimerHandler. When the timer goes off it is at that point that TimerHandler gets added to the queue. But all the PrintNum requests have already been called, and all the threads are busy!

I created another gist that puts the TimerHandler in its own io_service, with its own dedicated thread.

boost::shared_ptr< boost::asio::io_service > io_service2(
        new boost::asio::io_service
);
boost::shared_ptr< boost::asio::io_service::work > work2(
        new boost::asio::io_service::work( *io_service2 )
);
boost::shared_ptr< boost::asio::io_service::strand > strand2(
        new boost::asio::io_service::strand( *io_service2 )
);

...

boost::thread_group worker_threads;
for( int x = 0; x < 2; ++x )
{
        worker_threads.create_thread( boost::bind( &WorkerThread, x==0? io_service2 : io_service ) );
}

...

boost::shared_ptr< boost::asio::deadline_timer > timer(
        new boost::asio::deadline_timer( *io_service2 )
);
timer->expires_from_now( boost::posix_time::milliseconds( 1000 ) );
timer->async_wait(
        strand2->wrap( boost::bind( &TimerHandler, _1, timer, strand2 ) )
);

Now the timer will run reliably however much of a thread hog the PrintNum calls are. (To emphasize that point, I only give one thread for all the PrintNum calls to share, forcing them to run in serial.)

Upvotes: 0

Related Questions