I've created a wrapper around boost::asio::io_service to handle asynchronous tasks on the GUI thread of an OpenGL application.
Tasks might be created from other threads so boost::asio
seems ideal for this purpose and means I don't need to write my own task queue with associated mutexes and locking. I want to keep the work done on each frame below an acceptable threshold (e.g. 5ms) so I'm calling poll_one
until the desired budget is exceeded, rather than calling run
. As far as I can tell this requires me to call reset
whenever new tasks are posted, which seems to be working well.
Since it's short, here's the whole thing, sans #include
:
typedef std::function<void(void)> VoidFunc;
typedef std::shared_ptr<class UiTaskQueue> UiTaskQueueRef;
class UiTaskQueue {
public:
static UiTaskQueueRef create()
{
return UiTaskQueueRef( new UiTaskQueue() );
}
~UiTaskQueue() {}
// normally just hand off the results of std/boost::bind to this function:
void pushTask( VoidFunc f )
{
mService.post( f );
mService.reset();
}
// called from UI thread; defaults to ~5ms budget (but always does one call)
void update( const float &budgetSeconds = 0.005f )
{
// getElapsedSeconds is a utility function from the GUI lib I'm using
const float t = getElapsedSeconds();
while ( mService.poll_one() && getElapsedSeconds() - t < budgetSeconds );
}
private:
UiTaskQueue() {}
boost::asio::io_service mService;
};
I keep an instance of UiTaskQueueRef in my main app class and call mUiTaskQueue->update()
from within my app's animation loop.
I'd like to extend the functionality of this class to allow a task to be canceled. My previous implementation (using almost the same interface) returned a numeric ID for each task and allowed tasks to be canceled using this ID. But now the management of the queue and associated locking is handled by boost::asio
I'm not sure how best to do this.
I've made an attempt by wrapping any tasks I might want to cancel in a shared_ptr
and making a wrapper object that stores a weak_ptr
to the task and implements the ()
operator so it can be passed to the io_service
. It looks like this:
struct CancelableTask {
CancelableTask( std::weak_ptr<VoidFunc> f ): mFunc(f) {}
void operator()(void) const {
std::shared_ptr<VoidFunc> f = mFunc.lock();
if (f) {
(*f)();
}
}
std::weak_ptr<VoidFunc> mFunc;
};
I then have an overload of my pushTask
method that looks like this:
void pushTask( std::weak_ptr<VoidFunc> f )
{
mService.post( CancelableTask(f) );
mService.reset();
}
I then post cancelable tasks to the queue using:
std::function<void(void)> *task = new std::function<void(void)>( boost::bind(&MyApp::doUiTask, this) );
mTask = std::shared_ptr< std::function<void(void)> >( task );
mUiTaskQueue->pushTask( std::weak_ptr< std::function<void(void)> >( mTask ) );
Or with the VoidFunc
typedef if you prefer:
VoidFunc *task = new VoidFunc( std::bind(&MyApp::doUiTask, this) );
mTask = std::shared_ptr<VoidFunc>( task );
mUiTaskQueue->pushTask( std::weak_ptr<VoidFunc>( mTask ) );
So long as I keep the shared_ptr
to mTask
around then the io_service
will execute the task. If I call reset
on mTask
then the weak_ptr
can't lock and the task is skipped as desired.
My question is really one of confidence with all these new tools: is new std::function<void(void)>( std::bind( ... ) )
an OK thing to be doing, and a safe thing to manage with a shared_ptr
?
Yes, this is safe.
For the code:
VoidFunc *task = new VoidFunc( std::bind(&MyApp::doUiTask, this) );
mTask = std::shared_ptr<VoidFunc>( task );
Just do:
mTask.reset(new VoidFunc( std::bind(&MyApp::doUiTask, this) ) );
(and elsewhere).
Bear in mind that you need to deal with the race condition where a tread might be getting a lock on the weak_ptr just before you reset the shared_ptr keeping the callback alive, and as a result you will occasionally see callbacks even though you went down the code path resetting the callback shared_ptr.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With