What is the proper way of doing event handling in C++?
Often, event queues are implemented as command design pattern:
In object-oriented programming, the command pattern is a design pattern in which an object is used to represent and encapsulate all the information needed to call a method at a later time. This information includes the method name, the object that owns the method and values for the method parameters.
In C++, the object that own the method and values for the method parameters is a nullary functor (i.e. a functor that takes no arguments). It can be created using boost::bind()
or C++11 lambdas and wrapped into boost::function
.
Here is a minimalist example how to implement an event queue between multiple producer and multiple consumer threads. Usage:
void consumer_thread_function(EventQueue::Ptr event_queue)try { for(;;) { EventQueue::Event event(event_queue->consume()); // get a new event event(); // and invoke it }}catch(EventQueue::Stopped&) {}void some_work(int n) { std::cout << "thread " << boost::this_thread::get_id() << " : " << n << '\n'; boost::this_thread::sleep(boost::get_system_time() + boost::posix_time::milliseconds(500));}int main(){ some_work(1); // create an event queue that can be shared between multiple produces and multiple consumers EventQueue::Ptr queue(new EventQueue); // create two worker thread and pass them a pointer to queue boost::thread worker_thread_1(consumer_thread_function, queue); boost::thread worker_thread_2(consumer_thread_function, queue); // tell the worker threads to do something queue->produce(boost::bind(some_work, 2)); queue->produce(boost::bind(some_work, 3)); queue->produce(boost::bind(some_work, 4)); // tell the queue to stop queue->stop(true); // wait till the workers thread stopped worker_thread_2.join(); worker_thread_1.join(); some_work(5);}
Outputs:
./testthread 0xa08030 : 1thread 0xa08d40 : 2thread 0xa08fc0 : 3thread 0xa08d40 : 4thread 0xa08030 : 5
Implementation:
#include <boost/function.hpp>#include <boost/thread/thread.hpp>#include <boost/thread/condition.hpp>#include <boost/thread/mutex.hpp>#include <boost/smart_ptr/intrusive_ptr.hpp>#include <boost/smart_ptr/detail/atomic_count.hpp>#include <iostream>class EventQueue{public: typedef boost::intrusive_ptr<EventQueue> Ptr; typedef boost::function<void()> Event; // nullary functor struct Stopped {}; EventQueue() : state_(STATE_READY) , ref_count_(0) {} void produce(Event event) { boost::mutex::scoped_lock lock(mtx_); assert(STATE_READY == state_); q_.push_back(event); cnd_.notify_one(); } Event consume() { boost::mutex::scoped_lock lock(mtx_); while(STATE_READY == state_ && q_.empty()) cnd_.wait(lock); if(!q_.empty()) { Event event(q_.front()); q_.pop_front(); return event; } // The queue has been stopped. Notify the waiting thread blocked in // EventQueue::stop(true) (if any) that the queue is empty now. cnd_.notify_all(); throw Stopped(); } void stop(bool wait_completion) { boost::mutex::scoped_lock lock(mtx_); state_ = STATE_STOPPED; cnd_.notify_all(); if(wait_completion) { // Wait till all events have been consumed. while(!q_.empty()) cnd_.wait(lock); } else { // Cancel all pending events. q_.clear(); } }private: // Disable construction on the stack. Because the event queue can be shared between multiple // producers and multiple consumers it must not be destroyed before the last reference to it // is released. This is best done through using a thread-safe smart pointer with shared // ownership semantics. Hence EventQueue must be allocated on the heap and held through // smart pointer EventQueue::Ptr. ~EventQueue() { this->stop(false); } friend void intrusive_ptr_add_ref(EventQueue* p) { ++p->ref_count_; } friend void intrusive_ptr_release(EventQueue* p) { if(!--p->ref_count_) delete p; } enum State { STATE_READY, STATE_STOPPED, }; typedef std::list<Event> Queue; boost::mutex mtx_; boost::condition_variable cnd_; Queue q_; State state_; boost::detail::atomic_count ref_count_;};
The C++ Standard doesn't address events at all. Usually, however, if you need events you are working within a framework that provides them (SDL, Windows, Qt, GNOME, etc.) and ways to wait for, dispatch and use them.
Aside from that, you may want to look at Boost.Signals2.
C++11 and Boost have condition variables. They are a means for a thread to unblock another one that is waiting for some event to occur. The link above brings you to the documentation for std::condition_variable
, and has a code sample that shows how to use it.
If you need to keep track of events (say, keystrokes) and need to process them in a FIFO (first-in first-out) manner, then you'll have to use or make some kind of multi-threaded event queuing system, as suggested in some of the other answers. Condition variables can be used as building blocks to write your own producer/consumer queue, if you choose not to use an existing implementation.