Leo
Leo

Reputation: 1223

What's the best way to synchronize this event implementation

Here is my attempt at implementing a C++ event.

class Event{
    typedef std::tr1::function<void( int& )> CallbackFunction;
    std::list< CallbackFunction > m_handlers;

    template<class M>
    void AddHandler(M& thisPtr, void typename (M::*callback)(int&)) 
    {           
        CallbackFunction bound = std::tr1::bind(callback, &thisPtr, _1);
        m_handlers.push_back(bound);
    }

    void operator()(int& eventArg) 
    { 
        iterate over list...
        (*iter)(eventArg);

    }}

The trouble here is thread-safety. If AddHandler and operator() are called at the same time things could break.

What is the best way to sync this? Using a mutex could kill performance. I wonder what happens behind the scenes of boost::signals or C# event in this case.

Upvotes: 1

Views: 329

Answers (3)

Tom
Tom

Reputation: 19302

A mutex is definitely what you're looking for. If each Event has its own mutex, I wouldn't worry much about performance; the reason is that unless you're adding a lot of handlers during the time you're handling events, it's unlikely the mutex will be in contention and slow you down.

However, if you have more than one thread calling the operator() method on the same object, this mutex could be a problem. But without it, how will you ensure your callbacks are invoked in a thread-safe way anyhow? (I notice you're passing in an integer reference and returning void, so I'm assuming these are not reentrant handlers.)

EDIT: very good question in your comment. To be honest, I never put much thought into whether or not mutexes had much overhead when used in a synchronous way. So I put together this little test.


#include <stdio.h>
#include <pthread.h>

#define USE_PTHREAD_MUTEX 1

int main(int argc, char * argv[]) {

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);

long useless_number = 0;
long counter;

  for(counter = 0; counter < 100000000; counter++) {
    #if USE_PTHREAD_MUTEX
    pthread_mutex_lock(&mutex);
    #endif
    useless_number += rand();

    #if USE_PTHREAD_MUTEX
    pthread_mutex_unlock(&mutex);
    #endif
  }

  printf("%ld\n", useless_number);

}

I ran this on my system and got the following runtimes.

With USE_PTHREAD_MUTEX 0, the average runtime is 1.2 seconds.

With USE_PTHREAD_MUTEX 1, the average runtime is 2.8 seconds.

Thus, to answer your question, there is definitely overhead. Your mileage may vary. Besides, if multiple threads are competing for access to a resource, more time will be spent blocking, necessarily. Also, in a purely synchronous context, it's likely that more time will be spent accessing the shared resource than waiting for a mutex to lock/unlock. That is to say, the overhead of the mutex logic itself will probably be insignificant compared to these things.

Upvotes: 1

CashCow
CashCow

Reputation: 31435

If list really is your class then due to the nature of it, you don't need to lock every time you access it. You will lock a mutex to post to the end of the list, and you will also lock when you think you might have reached the end.

You should keep a count of the number of handlers in the class and when you are about to start iterating, you can happily iterate without locking until you reach this number.

If handlers are ever going to be removed, then you have more of a thread-contention issue.

Upvotes: 1

Dave Branton
Dave Branton

Reputation: 494

First, before you dismiss any implementation possibility as being insufficiently 'fast', you need to determine what the performance requirements actually are. Are you going to be triggering these events thousands of times every second? And if you are, are you really going to need to be adding handlers to the handlers container the whole time too.

If the answer to both those questions is, for some reason, actually 'yes' then you may need to investigate lock-free containers. This will mean building your own container rather than being able to use an stl list. Lock-free containers will use atomic intrinsics (such as InterlockedCompareExchange in windows for instance) to determine atomically if the end of the list is NULL or otherwise. They would then use a similar intrinsic to actually append to the list. Additional complications will occur if multiple threads attempt to add handlers at the same time.

However in a world of multi-core machines and instruction re-ordering and so-on, these approaches can be fraught with danger. I personally use an event system not dissimilar to what you describe, I use it with Critical Sections (which are quite efficient in windows at least), and I do not experience performance issues. But on the other hand, nothing is sent through the event system any faster than about 20Hz or so.

As with any performance-related question the answer is always going to be based on the answer to another question; Exactly where do you need your performance?

Upvotes: 2

Related Questions