I know how to do the basics: I can setup a Task<void> to be executed after an arbitrary delay; I can setup a continuation of that task to do my scheduled work; I can setup a continuation of that task to schedule another invocation of this chain at the next scheduled time point.
I'm struggling with how to make this cancelable in a thread-safe manner. Mostly, I'm struggling to reassure myself that I've properly dealt with all the races that occur in trying to do this safely.
Other than the exceptions my own DoWork() function might throw, are there exceptions I'd see in the catch(...) block that can actually be handled meaningfully, or should I just rethrow them?
Did I cover all the possible races (again, ignoring for the moment what DoWork() does) that can happen with cancellation-vs-timer?
Jason
I'm struggling with how to make this cancelable in a thread-safe manner. Mostly, I'm struggling to reassure myself that I've properly dealt with all the races that occur in trying to do this safely.
#include <pplx/pplx.h>
#include <pplx/pplxtasks.h>
#include <pplx/threadpool.h>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <mutex>
class MyWork
{
MyWork(int delay);
~MyWork();
void Start();
void Cancel();
private:
int _delay;
std::mutex _mutex;
pplx::cancellation_token_source _cts;
pplx::task_completion_event<void> *_event;
boost::asio::deadline_timer _timer;
pplx::task<void> _worktask;
void TimerHandler(const boost::system::error_code& error);
void DoWork();
};
MyWork::MyWork(int msec) : _delay(msec), _event(nullptr), _timer(crossplat::threadpool::shared_instance().service()) {}
void MyWork::Start()
{
std::lock_guard<std::mutex> lock(_mutex);
delete _event; // Destroy previous event (already signalled, in general)
_event = new pplx::task_completion_event<void>();
_worktask = pplx::task<void>(*_event).then([this](pplx::task<void> t){
try {
t.get(); // ?? Or should I just call .wait() ?
if (pplx::is_task_cancellation_requested()) {
std::lock_guard<std::mutex> lock(_mutex);
delete _event;
_event = nullptr;
pplx::cancel_current_task();
} else {
Start(); // Schedule the next go'round
DoWork(); // The regularly-scheduled stuff we care about
}
}
catch (...) {
// Don't call Start()
}
}, _cts.get_token());
// Changing timer expiration cancels all previous async_waiters
_timer.expires_from_now(boost::posix_time::milliseconds(_delay));
_timer.async_wait(boost::bind(&MyWork::TimerHandler, this, boost::asio::placeholders::error));
}
void MyWork::Cancel()
{
std::lock_guard<std::mutex> lock(_mutex);
_cts.cancel();
_timer.cancel();
if (_event) {
_event->set();
}
}
void MyWork::TimerHandler(const boost::system::error_code& error)
{
if (!error) { // Timer popped; signal the first task in the chain
_event->set();
} else {
// Handle errors
}
}
Is there any point to calling t.get(), since the "previous task" was just waiting on the task_completion_event, is not itself cancelled, etc?Other than the exceptions my own DoWork() function might throw, are there exceptions I'd see in the catch(...) block that can actually be handled meaningfully, or should I just rethrow them?
Did I cover all the possible races (again, ignoring for the moment what DoWork() does) that can happen with cancellation-vs-timer?
Jason