Canceling a deadline_timer

We have already seen how to set an ASIO deadline_timer. No big deal, actually. It's just a matter of specifying when the timer should expire and what to do at that moment. Here we are about to expand a bit this topic, seeing how we could cancel the request we made.

The final result of setting a Deadline Timer is a call to an action specified by the user. In the previous example we used a functor. Here we use instead a free function:
void doJob(const boost::system::error_code& ec, const char* msg) // 1.
if(!ec) // 2.
std::cout << "Message: " << msg << std::endl;

if(ec == boost::asio::error::operation_aborted) // 3.
std::cout << "Never mind" << std::endl;
std::cout << ec.message() << std::endl;

1. The function must have an error_code among its parameters, but there could be more. Here we want a c-string to be passed too.
2. If the timeout expired correctly, we output the passed c-string to the console.
3. If the user canceled the timer, ASIO would signal the fact calling our function with error code set to operation_aborted.

Using ASIO I/O Service

As usual, we are going to run an ASIO I/O service object that would take care of orchestrating the execution details on our behalf:
boost::asio::io_service ios;

// the specific code would go here;

Normal usage

Usually we would do something like that:
boost::asio::deadline_timer dt1 = boost::asio::deadline_timer(ios); // 1.
dt1.expires_from_now(boost::posix_time::seconds(1)); // 2.
dt1.async_wait(boost::bind(&doJob, _1, "Just say hello")); // 3.

1. Create a Deadline Timer object associated to our I/O Service.
2. Specify when we want the timer to fire off. Here we are saying: "in one second".
3. Tell ASIO what to do when the wait (asychronously) expires. Notice that here we are using boost::bind() to let the compiler knows how to use the passed function in this context. I could translate it like this: "Let async_wait() uses doJob(), passing to it as first parameter what async_wait() already knows it should pass (i.e.: the error code) and as second parameter the c-string I provide here".

Canceling the request

If after we have created and set our deadline_timer, and before run() is called on the I/O Service object, we find out that there is no use anymore for this timer, we can simply cancel its execution:
boost::asio::deadline_timer dt2 = boost::asio::deadline_timer(ios);
dt2.async_wait(boost::bind(&doJob, _1, "Don't say hello"));

The result of this code is that the doJob() function is called immediately, with the error code set to boost::asio::error::operation_aborted.

Implicit canceling /1

When a deadline_timer object is destroyed, it implicitly cancels itself, calling the set action with the operation_aborted error code. So, this code does not behave like someone could expect:
boost::asio::deadline_timer dt3 = boost::asio::deadline_timer(ios);

dt3.async_wait(boost::bind(&doJob, _1, "This hello is out of scope"));

When this deadline_timer object goes out of scope, it is destroyed, and an implicit call to cancel() is performed. As a result we will get immediately a "Never mind" message from doJob(), and not the three seconds delayed hello message.

Implicit canceling /2

We could get the same result as the previous example using a variable on the heap:
boost::asio::deadline_timer* dt4a = new boost::asio::deadline_timer(ios);

dt4a->async_wait(boost::bind(&doJob, _1, "Doomed hello form heap"));
delete dt4a;

Since delete on the Deadline Timer is called before run() is called on the I/O Service object, we will never see the hello message, the doJob() function would be called instead with the error code set to operation_aborted.

Hello on the heap - without canceling

So, if we want the doJob() function to be called normally (no error set and after the timer expires), we have to call delete for the Deadline Timer after the run() method is executed on the I/O Service object:
boost::asio::deadline_timer* dt4b = new boost::asio::deadline_timer(ios);

dt4b->async_wait(boost::bind(&doJob, _1, "Correct hello form heap"));

// ...;

// memory cleanup
delete dt4b;

The deleting of dt4b is quite a sensitive matter. It has to be done after the call, but when ios is still alive.
Besides, we have all the normal worries associated to the usage of raw pointers in C/C++. So, it is not a good idea writing code like that, if you can avoid it. Better using a smart pointer, as we see next.

Smart hello on the heap - without canceling

Using the good old std::auto_ptr we could rewrite the previous code in this way:
std::auto_ptr dt5(new boost::asio::deadline_timer(ios));
dt5->async_wait(boost::bind(&doJob, _1, "Smart hello form heap"));

Better to stress that we still have to be careful considering the relation between the I/O Service and the Deadline Timer: the latter has to be destroyed after is called, but before ios goes out of scope.

1 comment: