11

I am attempting to understand Boost.Asio, with the intention of potentially implementing a signaling system using condition variables in conjunction with Boost.Asio.

I have seen the other StackOverflow questions boost asio asynchronously waiting on a condition variable, boost::asio async condition, and boost condition variable issue, but none of these questions/answers have satisfactorily touched on an essential question that I have: Is it true that, and/or is there a fundamental reason why, Boost.Asio is not applicable to, or a natural fit with, condition variables?

My thinking is that condition variables are internally implemented using operating-system level synchronization objects (for example, boost::thread::condition_variable on Windows uses a Windows OS semaphore). Because, to my current understanding, boost::asio::io_service is intended to encapsulate OS-level synchronization objects, condition variables would therefore seem to be a natural fit.

It is true that unlike file operations and socket operations, there is typically never a callback function at the operating system level associated with a signaled condition (I think - I am not sure about this). However, it would seem simple enough to implement such a callback handler within Boost.Asio by simply requiring the user to provide a callback function that is to be called when a condition variable is signaled - just as users must provide a completion handler routine for other boost::asio::io_service services.

For example (this is just a quick thought, not a complete prototype - it does not include sufficient parameters to deal with notify_one() vs. notify_all(), doesn't indicate how the service knows when to exit, and likely has other glaring omissions or flaws):

void condition_handler_function() {}
boost::asio::io_service service;
boost::mutex mut;
boost::condition_variable cond;

// The following class is **made up by me** - would such a class be a good idea?
boost::asio::io_service::condition_service
             condserv(service, cond, mut, condition_handler_function); 

condserv.async_wait_on_signal();

service.run(); // when condition variable is signaled by notify_one(),
               // 'handler_function()' would be called


// ... in some other thread, later:
cond.notify_one(); // This would trigger 'handler_function()'
                   // in this theoretical code

Perhaps, if I tried to fill in the missing details noted above the code snippet, it would become clear to me that this could not work in a clean way. However, this effort is non-trivial.

Therefore, I would like to post the question here. Is there a good reason why condition variables are not supported by Boost.Asio?

ADDENDUM

I have changed the title of the post to reference "Event-based interface", since Tanner's answer, below, has clarified to me that it is really an Event-based interface that I am asking about (not really condition variables).

einpoklum
  • 118,144
  • 57
  • 340
  • 684
Dan Nissenbaum
  • 13,558
  • 21
  • 105
  • 181

2 Answers2

24

Boost.Asio is a C++ library for network and low-level I/O programming. As such, OS-level synchronization objects, such as condition variables, are outside of the scope of the library, and a much better fit for Boost.Thread. The Boost.Asio author often presents the boost::asio::io_service as the bridge or link between the application and the OS. While this may be an over simplification, it is within the context of the OS's I/O services.

Asynchronous programming already has an innate complexity due to the separation in time and space between operation initiation and completion. Strands provided a fairly clean solution to provide strict sequential invocation of handlers, without the need of explicit locking. As the locking is both implicit and thread-safe, application code can use strands without the fear of deadlocking. On the other hand, having boost::asio::io_service::condition_service perform implicit synchronization on an externally provided object may turn a complex library into a complicated one. It may not be clear to the application developer what mutex on which the handler was synchronized, and the state of the mutex. Additionally, it introduces the ability for applications to more easily deadlock the event processing loop due to the implicit locking.


If event-based handler invocation needs to occur, then one fairly simple alternative is use the same approach Boost.Asio's timeout server example uses: boost::asio::deadline_timer. A deadline_timer's expiry time can be set to posix_time::pos_infin, causing an async_wait's handler to only be invoked once the timer has been canceled:

  • cancel() could function as notify_all(), where all outstanding handlers are queued for invocation.
  • cancel_one() could function as notify_one(), where a max of one outstanding handler is queued for invocation.

A simple example, ignoring error code handling, is as follows:

#include <iostream>

#include <boost/asio.hpp>
#include <boost/thread.hpp>

class event
{
public:
  explicit
  event(boost::asio::io_service& io_service) 
    : timer_(io_service)
  {
    // Setting expiration to infinity will cause handlers to
    // wait on the timer until cancelled.
    timer_.expires_at(boost::posix_time::pos_infin);
  }

  template <typename WaitHandler>
  void async_wait(WaitHandler handler)
  {
    // bind is used to adapt the user provided handler to the deadline 
    // timer's wait handler type requirement.
    timer_.async_wait(boost::bind(handler));
  }

  void notify_one() { timer_.cancel_one(); }
  void notify_all() { timer_.cancel();     }

private:
  boost::asio::deadline_timer timer_;
};

void on_event() { std::cout << "on_event" << std::endl; }

int main()
{
  boost::asio::io_service io_service;
  event event(io_service);

  // Add work to service.
  event.async_wait(&on_event);

  // Run io_service.
  boost::thread thread(boost::bind(&boost::asio::io_service::run,
                       &io_service));

  // Trigger event, causing the on_event handler to run.
  event.notify_one();

  thread.join();  
}
Tanner Sansbury
  • 51,153
  • 9
  • 112
  • 169
  • Very good answer, thanks. You are right that I was seeking an "Event" interface in Boost.Asio, rather than a condition variable interface. Had such an Event interface been available, I would have immediately used it. Do you think it is well within the intended scope of Boost.Asio to incorporate an Event interface? If so, I wonder if that would be a good suggestion for the author or maintainers. – Dan Nissenbaum Jun 11 '13 at 17:53
  • I have changed the title of the post to include the concept/phrase "Event-based interface". – Dan Nissenbaum Jun 11 '13 at 17:56
  • @DanNissenbaum: Hmmm. It may be within the scope. I suppose it could not hurt to make a suggestion/request. A cleaner interface would definitely make it much more explicit, as using `boost::asio::deadline_timer` to obtain the desired behavior is slightly obscure. – Tanner Sansbury Jun 11 '13 at 21:54
  • Very nice answer. I was able to seamlessly change my condition variable logic using this technique. – Tarc Aug 09 '16 at 22:30
  • And today, on the umpteenth reading of this example, suddenly the cleverness of _"Bind is used to adapt the user provided handler to the deadline timer's wait handler type requirement."_ clicked in my mind. – sehe Mar 17 '18 at 13:13
  • @TannerSansbury I am trying to figure out how I can call pass the error code (_1) and some user defined parameters to the template function WaitHandler() which takes no parameters, I get tons of errors when I try to change event.async_wait(&on_event) to a member fn with a few parameters including ec – johnco3 Jun 26 '18 at 06:04
4

Condition variables are a synchronous concept: they block one thread until something happens in another thread. Boost.Asio is an asynchronous framework: it provides multiplexing of events in a non-blocking fashion. These two don't seem very compatible. If you want asynchronous event notification, look at eventfd on Linux, which should be usable with Boost.Asio.

John Zwinck
  • 239,568
  • 38
  • 324
  • 436
  • +1 as I read this question I immediately thought *use an eventfd with semaphore semantics* – Sam Miller Jun 09 '13 at 15:00
  • 3
    On the other hand, condition variables can also be considered asynchronous, because the thread that sets the signal does not block, continues execution immediately, and can make no assumptions about when a worker thread will, in parallel, perform the work. Also, the *synchronous* aspect of condition variables could be transformed into an asynchronous system with Boost.Asio. I.e., when you drop a work item into a boost::asio::io_service, it is immediately handled by a worker thread (if free) in the same way as the work triggered by a signaled condition. This is the semantics I suggest here. – Dan Nissenbaum Jun 09 '13 at 16:04
  • eventfd seems analogous, in Linux, to a Windows OS-level Semaphore. I suspect that internally, boost::asio::io_service::run() (and its helpers) is implemented in terms of a blocking OS-primitive such as a semaphore on Windows, or a condition or eventfd on Linux. – Dan Nissenbaum Jun 09 '13 at 16:38