Discussion:
[asio-users] deadline_timer and handler lifetimes
Jason Aubrey
2011-12-30 23:24:05 UTC
Permalink
Overview:

I'm trying to use a deadline_timer in an RAII context as shown below.



Details:

When a deadline_timer goes out of scope, it would be nice (at least I think so) if it encapsulated the cleanup logic by implicitly canceling queued handlers and ensuring pending handlers are completed. Since this is not the case, I need to explicitly synchronize the cleanup as shown below.



//--------------------------------------------------------------------------------

struct foo
{
foo(boost::asio::io_service& io_service)

: m_timer(io_service)

{

schedule_timer();
}
~foo()
{
m_timer.cancel();

m_event.wait();
}



void schedule_timer()

{

m_timer.expires_at(boost::posix_time::microsecond_clock() + boost::posix_time::seconds(1));
m_timer.async_wait(boost::bind(&foo::on_timer, this, boost::asio::placeholders::error));

}



void on_timer(const boost::system::error_code& code)

{

if(code == boost::asio::error::operation_aborted)

{

m_event.signal();

return;

}

// TODO: Handle spurious wake-ups (omitted for brevity)

// TODO: Modify this instance (omitted for brevity)

schedule_timer();
}

boost::asio::deadline_timer m_timer;

manual_reset_event m_event; // An abstraction based on condition_variable, mutex, and a bool
};

//--------------------------------------------------------------------------------



Analysis:

Reasons I don't like the code above:

*) Not sure it is correct - any idea?

*) Prone to a deadlock if an exception is thrown in on_timer before schedule_timer is called again

*) Need an event for every timer to ensure cleanup



The only alternatives I've come across are:

1) Use static functions for the handlers to avoid lifetime issues

2) Have the handler self-manage its lifetime (e.g. have it call 'delete this')

3) Couple the timer and io_service lifetimes so the io_service is stopped while the handler is still in scope



A problem I have with each alternative:

1) Even if the handler is static, the handler is modifying an instance which has the same lifetime issues

2) If I need multiple timers in foo then I don't see how 'delete this' is possible where 'this' is an instance of foo.

3) Doesn't fit the situation where timers with independent lifetimes share the same io_service



Please show me something obvious I've missed here!



Thanks,

Jason Aubrey
Gruenke, Matt
2011-12-31 02:05:28 UTC
Permalink
You can get something that behaves how I think you want, but it will take a bit more work. Ultimately, you're fighting the usage model that the Asio async interface was designed to support, which is to *de*-couple the state machine from your thread's stack!

You will need:

1.
A shared_ptr<> to a mutex
2.
A shared_ptr<> to the deadline_timer
3.
A shared_ptr<> to the data on which the normal timeout handler operates.



The timeout handler should be a static function that takes items 1, 2, and a weak_ptr<> to #3. When your struct is destroyed, it should: obtain the mutex, reset #3, release the lock, and cancel the timer. Do not wait on the timer. Be sure to hold the mutex until after #3 is reset (meaning you can't just grab it inside the destructor of #3's target). Note that cancelling the timer is just an optimization to get the resources freed up sooner.

When the timer handler executes (now a static or independent function), it should: obtain the mutex, lock the pointer to #3, call the real timeout handler if the #3 pointer is valid, then release the lock on the mutex. You can pretty much ignore the error code.


The other way get the RAII behavior you want is to have a long-lived deadline_timer that calls handlers for the specific timeouts that you can atomically schedule & unschedule on it. A std::map< timeout, handler_fn > is a good way to keep them sorted, and a mutex makes it atomic Your RAII wrapper holds an iterator to the corresponding timeout & a pointer to the timer wrapper. Cancel is only used when a new timeout is scheduled earlier than the previous earliest one (which never happens if you're only using one timeout interval every time) or the timer is unscheduled. Since it's pretty straight-forward, I won't go further into the details.


The usage model for which Asio is designed is to let the timer state machine own (or at least hold a refcount on) the data on which it operates, rather than tying it to a thread scope. To cancel the timer, post an event to the strand on which its handler runs that cancels it.


I hope this helps. Maybe there's a simpler way to do RAII with deadline timers, but the two I've described are the only ways I'm certain will work as desired.


Mat


________________________________

From: Jason Aubrey [mailto:***@alphaworkscapital.com]
Sent: Fri 12/30/2011 6:24 PM
To: asio-***@lists.sourceforge.net
Subject: [asio-users] deadline_timer and handler lifetimes



Overview:

I'm trying to use a deadline_timer in an RAII context as shown below.



Details:

When a deadline_timer goes out of scope, it would be nice (at least I think so) if it encapsulated the cleanup logic by implicitly canceling queued handlers and ensuring pending handlers are completed. Since this is not the case, I need to explicitly synchronize the cleanup as shown below.



//--------------------------------------------------------------------------------

struct foo
{
foo(boost::asio::io_service& io_service)

: m_timer(io_service)

{

schedule_timer();
}
~foo()
{
m_timer.cancel();

m_event.wait();
}



void schedule_timer()

{

m_timer.expires_at(boost::posix_time::microsecond_clock() + boost::posix_time::seconds(1));
m_timer.async_wait(boost::bind(&foo::on_timer, this, boost::asio::placeholders::error));

}



void on_timer(const boost::system::error_code& code)

{

if(code == boost::asio::error::operation_aborted)

{

m_event.signal();

return;

}

// TODO: Handle spurious wake-ups (omitted for brevity)

// TODO: Modify this instance (omitted for brevity)

schedule_timer();
}


boost::asio::deadline_timer m_timer;

manual_reset_event m_event; // An abstraction based on condition_variable, mutex, and a bool
};

//--------------------------------------------------------------------------------



Analysis:

Reasons I don't like the code above:

*) Not sure it is correct - any idea?

*) Prone to a deadlock if an exception is thrown in on_timer before schedule_timer is called again

*) Need an event for every timer to ensure cleanup



The only alternatives I've come across are:

1) Use static functions for the handlers to avoid lifetime issues

2) Have the handler self-manage its lifetime (e.g. have it call 'delete this')

3) Couple the timer and io_service lifetimes so the io_service is stopped while the handler is still in scope



A problem I have with each alternative:

1) Even if the handler is static, the handler is modifying an instance which has the same lifetime issues

2) If I need multiple timers in foo then I don't see how 'delete this' is possible where 'this' is an instance of foo.

3) Doesn't fit the situation where timers with independent lifetimes share the same io_service



Please show me something obvious I've missed here!



Thanks,

Jason Aubrey
Marsh Ray
2011-12-31 02:08:17 UTC
Permalink
queued handlers and ensuring pending handlers are completed. Since this
is not the case, I need to explicitly synchronize the cleanup as shown
below.
I would have the outstanding timer pass a shared_pointer (or other
counted reference) via the bind() to on the instance of foo. That way
foo's dtor doesn't actually run until the timer routine is finished with it.

Now it may be that foo's destructor does other things that need to
happen deterministically. But in general, allowing outstanding
asynchronous callbacks to an object means you no longer have have total
control over the lifetime of the object. So if that's the case you
probably need to split foo into multiple classes.

- Marsh
//--------------------------------------------------------------------------------
struct foo
{
foo(boost::asio::io_service& io_service)
: m_timer(io_service)
{
schedule_timer();
}
~foo()
{
m_timer.cancel();
m_event.wait();
}
void schedule_timer()
{
m_timer.expires_at(boost::posix_time::microsecond_clock() +
boost::posix_time::seconds(1));
m_timer.async_wait(boost::bind(&foo::on_timer, this,
boost::asio::placeholders::error));
}
void on_timer(const boost::system::error_code& code)
{
if(code == boost::asio::error::operation_aborted)
{
m_event.signal();
return;
}
// TODO: Handle spurious wake-ups (omitted for brevity)
// TODO: Modify this instance (omitted for brevity)
schedule_timer();
}
boost::asio::deadline_timerm_timer;
manual_reset_eventm_event; // An abstraction based on
condition_variable, mutex, and a bool
};
//--------------------------------------------------------------------------------
*) Not sure it is correct - any idea?
*) Prone to a deadlock if an exception is thrown in on_timerbefore
schedule_timeris called again
*) Need an event for every timer to ensure cleanup
1) Use static functions for the handlers to avoid lifetime issues
2) Have the handler self-manage its lifetime (e.g. have it call 'delete this')
3) Couple the timer and io_servicelifetimes so the io_serviceis stopped
while the handler is still in scope
1) Even if the handler is static, the handler is modifying an instance
which has the same lifetime issues
2) If I need multiple timers in foothen I don't see how 'delete this' is
possible where 'this' is an instance of foo.
3) Doesn't fit the situation where timers with independent lifetimes
share the same io_service
Please show me something obvious I've missed here!
Thanks,
Jason Aubrey
------------------------------------------------------------------------------
Ridiculously easy VDI. With Citrix VDI-in-a-Box, you don't need a complex
infrastructure or vast IT resources to deliver seamless, secure access to
virtual desktops. With this all-in-one solution, easily deploy virtual
desktops for less than the cost of PCs and save 60% on VDI infrastructure
costs. Try it free! http://p.sf.net/sfu/Citrix-VDIinabox
_______________________________________________
asio-users mailing list
https://lists.sourceforge.net/lists/listinfo/asio-users
_______________________________________________
Using Asio? List your project at
http://think-async.com/Asio/WhoIsUsingAsio
Jason Aubrey
2011-12-31 19:39:42 UTC
Permalink
Thanks for the suggestions guys. I now see how the asio design intends to decouple the lifetimes from the thread's stack. This is the thing I was missing. This design is different from what I'm used to but I see how this could have advantages. By forcing the coupling things get complicated in several ways. My current class has multiple timers, but I'll consider the asio intent as well as decomposition and see where that leads me. Thanks for the help!

Jason
Post by Marsh Ray
queued handlers and ensuring pending handlers are completed. Since this
is not the case, I need to explicitly synchronize the cleanup as shown
below.
I would have the outstanding timer pass a shared_pointer (or other
counted reference) via the bind() to on the instance of foo. That way
foo's dtor doesn't actually run until the timer routine is finished with it.
Now it may be that foo's destructor does other things that need to
happen deterministically. But in general, allowing outstanding
asynchronous callbacks to an object means you no longer have have total
control over the lifetime of the object. So if that's the case you
probably need to split foo into multiple classes.
- Marsh
//--------------------------------------------------------------------------------
struct foo
{
foo(boost::asio::io_service& io_service)
: m_timer(io_service)
{
schedule_timer();
}
~foo()
{
m_timer.cancel();
m_event.wait();
}
void schedule_timer()
{
m_timer.expires_at(boost::posix_time::microsecond_clock() +
boost::posix_time::seconds(1));
m_timer.async_wait(boost::bind(&foo::on_timer, this,
boost::asio::placeholders::error));
}
void on_timer(const boost::system::error_code& code)
{
if(code == boost::asio::error::operation_aborted)
{
m_event.signal();
return;
}
// TODO: Handle spurious wake-ups (omitted for brevity)
// TODO: Modify this instance (omitted for brevity)
schedule_timer();
}
boost::asio::deadline_timerm_timer;
manual_reset_eventm_event; // An abstraction based on
condition_variable, mutex, and a bool
};
//--------------------------------------------------------------------------------
*) Not sure it is correct - any idea?
*) Prone to a deadlock if an exception is thrown in on_timerbefore
schedule_timeris called again
*) Need an event for every timer to ensure cleanup
1) Use static functions for the handlers to avoid lifetime issues
2) Have the handler self-manage its lifetime (e.g. have it call 'delete this')
3) Couple the timer and io_servicelifetimes so the io_serviceis stopped
while the handler is still in scope
1) Even if the handler is static, the handler is modifying an instance
which has the same lifetime issues
2) If I need multiple timers in foothen I don't see how 'delete this' is
possible where 'this' is an instance of foo.
3) Doesn't fit the situation where timers with independent lifetimes
share the same io_service
Please show me something obvious I've missed here!
Thanks,
Jason Aubrey
------------------------------------------------------------------------------
Ridiculously easy VDI. With Citrix VDI-in-a-Box, you don't need a complex
infrastructure or vast IT resources to deliver seamless, secure access to
virtual desktops. With this all-in-one solution, easily deploy virtual
desktops for less than the cost of PCs and save 60% on VDI infrastructure
costs. Try it free! http://p.sf.net/sfu/Citrix-VDIinabox
_______________________________________________
asio-users mailing list
https://lists.sourceforge.net/lists/listinfo/asio-users
_______________________________________________
Using Asio? List your project at
http://think-async.com/Asio/WhoIsUsin
Gruenke, Matt
2011-12-31 20:41:18 UTC
Permalink
It's a conceptual leap I also missed, at first. In fact, like you, I
also made a RAII timer wrapper in my early work with Asio.

One way to use Asio effectively is to think of your design as a set of
state machines, where each transition maps to a callback from one of the
async methods. You can wrap & chain these to construct higher-level
state machines.

On the practical side of things, I recommend dedicating a strand to each
state machine. Boost::bind(), shared_ptr<>, weak_ptr<>, and
shared_from_this<> are all valuable tools for managing object lifetimes.
This can be tricky, however, and there's probably no substitute for
experience.


Matt


-----Original Message-----
From: Jason Aubrey [mailto:***@alphaworkscapital.com]
Sent: Saturday, December 31, 2011 14:40
To: Marsh Ray
Cc: asio-***@lists.sourceforge.net
Subject: Re: [asio-users] deadline_timer and handler lifetimes

Thanks for the suggestions guys. I now see how the asio design intends
to decouple the lifetimes from the thread's stack. This is the thing I
was missing. This design is different from what I'm used to but I see
how this could have advantages. By forcing the coupling things get
complicated in several ways. My current class has multiple timers, but
I'll consider the asio intent as well as decomposition and see where
that leads me. Thanks for the help!

Jason

Loading...