As I’ve been programming OpenP2P, I’ve iterated over a few styles of programming – a process which consumed a lot of time, but taught me a lot.
Originally, I built OpenP2P on top of boost::asio to make use of its asynchronous features, which I had supposed would make my programming life easier, as well providing greater efficiency. The style was effectively based upon the Actor model, as deployed by languages such as Erlang, for which I would have a thread managing the state for each object – although in this case the ‘thread’ was an abstraction and the system as a whole was based on a small number of threads on which methods would be cooperatively queued and executed. In other words, it was an event-based system.
The problems became obvious as the number of boost::shared_ptr’s required increased dramatically, as I tried to manage all the state of the abstract threads I had created, and I felt that I needed garbage collection to be able to safely free all the data. RAII on the stack, a very useful technique in C++, quickly became useless as state must now all exist on the heap.
Then I discovered this.
The paper argues that historically threading packages have provided poor performance, but this isn’t inherent to the threading model and is only an artifact of these implementations. It then goes on to display that more modern threading implementations can achieve excellent performance. Such conclusions are not isolated, as demonstrated by this presentation which makes a similar argument, focusing particularly on Java.IO vs Java.NIO.
The point is that threading packages have many advantages, making it easier to understand the code and to manage state on the stack (utilising RAII), and therefore that it isn’t wise to switch systems to the event-based architecture in pursuit of a temporary performance increase. In fact, Linux’s NPTL shows that that time is over.
So, after comparing the two approaches, I decided to change OpenP2P to a synchronous thread-based architecture, where operations such as socket.receive() would no longer take a callback, which would be called possibly at some time in the future from some thread, but would simply take the necessary parameters and block, returning to the current thread when it completes, returning a value indicating error (or throwing an exception). If you want to do other things simultaneously -> spawn a new thread. The only thing I ended up adding was a facility for easily canceling any operations, for example when a signal is raised from another thread or when a timer completes.
An event-based architecture is obviously useful for some purposes, such as a GUI, although I can see potential advantages in an approach of using a separate thread for each event, to prevent the GUI from blocking if one of the event handling routines decides to do something like open a file, which should generally be fast, but in this case happens to be on an external hard drive that takes 10 seconds or so to wake up. However, creating threads clearly has to be efficient (or otherwise we might have to keep threads around for new events), and this could potentially generate a large number, as well as creating all sorts of synchronisation concerns since events are likely to raise other events. Naturally, it all comes down to whether you can rely on your event handlers to cooperate.