Synchronization Objects

team bbl


In almost any use of threads, data is shared between different threads. When two threads attempt to access the same data, whether it is an object or a resource, then the access has to be synchronized to avoid data being accessed or modified by more than one thread at the same time. There are almost always so-called invariants in a programassumptions about related elements of data, such as having a correct first element pointer in a list and having each element point to the next element and a NULL pointer in the last element. During insertion of a new element, there is a moment when this invariant is broken. If this list is used from two threads, then you must guard against this moment so that no other client of the list is using it in this intermediate state.

It is the programmer's responsibility to make sure that shared data is not just grabbed by any thread but rather is accessed in a controlled manner. This section describes the classes you can use to achieve this protection.

wxMutex

The name comes from mutual exclusion and is the easiest synchronization element to use. It makes sure that only one thread is accessing a particular piece of data. To gain access to the data, the application calls wxMutex::Lock, which blocks (halts execution) until the resource is free. wxMutex::Unlock frees the resource again. Although you can use wxMutex's Lock and Unlock functions directly, by using the wxMutexLocker class, you can be sure that the mutex is always released correctly when the instance is destroyed, even if an exception occurred in your code.

In the following example, we assume that the MyApp class contains an m_mutex member of type wxMutex.

 void MyApp::DoSomething() {     wxMutexLocker lock(m_mutex);     if (lock.IsOk())     {         ... do something      }     else     {         ... we have not been able to         ... acquire the mutex, fatal error     } } 

There are three important rules for using mutexes:

  1. A thread cannot generally lock a mutex that is already locked (no mutex recursion). Although there are systems that allow this, it is not portable across all operating systems.

  2. A thread cannot unlock a mutex that a different thread has locked. If you need such a construct, you must use semaphores (discussed later).

  3. If you are in a thread that is able to do other work if it cannot lock the mutex, you should call wxMutex::TRyLock. It returns immediately and indicates whether it was able to lock the mutex (wxMUTEX_NO_ERROR) or not (wxMUTEX_DEAD_LOCK or wxMUTEX_BUSY). This is especially important for the main (GUI) thread, which should never be blocked because your application would become unresponsive to user input.

Deadlocks

A deadlock occurs if two threads are waiting for resources that the other thread has already acquired. So supposing that thread A has already acquired mutex 1 and thread B has already acquired mutex 2, if thread B is now waiting for mutex 1 and thread A is waiting for mutex 2, the wait would go on forever. Some systems will be able to indicate this by returning the special error code wxMUTEX_DEAD_LOCK from Lock, Unlock, or tryLock. On other systems, the application will just hang until the user kills it.

There are two common solutions to this problem:

  • Fixed order. A consistent hierarchy is imposed for acquiring multiple locks on objects. In the previous example, every thread must always acquire mutex 1 first and then mutex 2 so that deadlock cannot occur.

  • Try lock. Acquire the first lock and then call tryLock on any subsequent mutex. If this fails, release all locks and start again. This is a more costly approach, but you might use it when a fixed order solution is not flexible enough and would result in complicated code.

wxCriticalSection

A critical section is used for guarding a certain section of code rather than data, but in practice, it is very similar to a mutex. It is only different on those platforms where a mutex is visible outside an application and can be shared between processes, whereas a critical section is only visible within the application. This makes a critical section slightly more efficientat least on the platforms that have a native implementation. Because of this origin, the terminology is also slightly differenta mutex may be locked (or acquired) and unlocked (or released), whereas a critical section is entered and left by the program.

You should try to use the wxCriticalSectionLocker class whenever possible instead of directly using wxCriticalSection for the same reasons that wxMutexLocker is preferable to wxMutex.

wxCondition

A condition variable is used for waiting on some change of state on shared data. For example, you could have a condition indicating that a queue has data available. The shared data itselfthe queue in this exampleis usually protected by a mutex.

You could try to solve the entire problem with a loop that locks a mutex, tests the amount of available data, and releases the lock again if there is no data. However, this is very inefficient because the loop is running all the time, just waiting for the right moment to grab the mutex. Such situations are more efficiently solved using conditions because the thread can block until another thread indicates a change of state.

Multiple threads may be waiting on the same condition, in which case you have two choices to wake up one or more of the threads. You can call Signal to wake one waiting thread, or you can call Broadcast to wake up all of the threads waiting on the same condition. If several predicates are signaled through the same wxCondition, then Broadcast must be used; otherwise a thread might be awakened and be unable to run because it waits for the "wrong" predicate to become true.

wxCondition Example

Let's suppose we have two threads:

  • A producing thread, which puts ten elements onto the queue and then signals "queue full" and waits for "queue empty" before it starts filling the queue again.

  • A consuming thread, which has to wait for "queue full" before removing items.

We need one mutex, m_mutex, guarding the queue and two condition variables, m_isFull and m_isEmpty. These are constructed passing the m_mutex variable as parameter. It is important that you always explicitly test the predicate because a condition might have been signaled before you were waiting on it.

In pseudo-code, this is the EnTRy code for the producing thread:

 while ( notDone ) {    wxMutexLocker lock(m_mutex) ;    while( m_queue.GetCount() > 0 )    {       m_isEmpty.Wait() ;    }    for ( int i = 0 ; i < 10 ; ++i )    {       m_queue.Append( wxString::Format(wxT("Element %d"),i) ) ;    }    m_isFull.Signal(); } 

Here's the code for the consuming thread:

 while ( notDone ) {    wxMutexLocker lock(m_mutex) ;    while( m_queue.GetCount() == 0 )    {       m_isFull.Wait() ;    }    for ( int i = queue.GetCount() ; i > 0 ; i )    {       m_queue.RemoveAt( i ) ;    }    m_isEmpty.Signal(); } 

The Wait method unlocks the mutex and then waits for the condition to be signaled. When it returns, it has locked the mutex again, leading to a clean synchronization.

It is important to test the predicate not only before entering but also when Wait returns because something else might have been going on between the signaling and the awakening of our thread so that the predicate is not true anymore; there are even systems that produce spurious wakeups.

Note that a call to Signal might happen before the other thread calls Wait and, just as with pthread conditions, the signal is lost. So if you want to be sure that you don't miss a signal, you must keep the mutex associated with the condition initially locked and lock it again before calling Signal. This means that this call is going to block until Wait is called by another thread.

The following example shows how a main thread can launch a worker thread, which starts running and then waits until the main thread signals it to continue:

 class MySignallingThread : public wxThread { public:     MySignallingThread(wxMutex *mutex, wxCondition *condition)     {         m_mutex = mutex;         m_condition = condition;         Create();     }     virtual ExitCode Entry()     {         ... do our job ...         // tell the other(s) thread(s) that we're about to         // terminate: we must lock the mutex first or we might         // signal the condition before the waiting threads start         // waiting on it!         wxMutexLocker lock(m_mutex);         m_condition.Broadcast(); // same as Signal() here                                   // one waiter only         return 0;     } private:     wxCondition *m_condition;     wxMutex *m_mutex; }; void TestThread() {     wxMutex mutex;     wxCondition condition(mutex);     // the mutex should be initially locked     mutex.Lock();     // create and run the thread but notice that it won't be able to     // exit (and signal its exit) before we unlock the mutex below     MySignallingThread *thread =         new MySignallingThread(&mutex, &condition);     thread->Run();     // wait for the thread termination: Wait() atomically unlocks     // the mutex which allows the thread to continue and starts     // waiting     condition.Wait();     // now we can exit } 

Of course, here it would be much better to simply use a joinable thread and call wxTHRead::Wait on it, but this example does illustrate the importance of properly locking the mutex when using wxCondition.

wxSemaphore

Semaphores are a kind of universal combination of mutex and counter. The most important difference from a mutex is that they can be signaled from any thread, not just the owning one, so you can think of a semaphore as a counter without an owner.

A thread calling Wait on a semaphore has to wait for the counter to become positive, and then the thread decrements the counter and returns. A thread calling Post on a semaphore increments the counter and returns.

There is one additional feature of a semaphore in wxWidgetsyou can indicate a maximum value at construction time, 0 being the default (representing "unlimited"). If you have given a non-zero maximum value, and a thread calls Post at the wrong moment, leading to a counter value higher the maximum, you will get a wxSEMA_OVERFLOW error. To illustrate this concept, let's look at the "universal mutex" described earlier:

  • A mutex that can be locked and unlocked from different threads would be implemented using a semaphore with an initial count of 1. The mutex.Lock would be implemented using semaphore.Wait and mutex.Unlock using semaphore.Post.

  • The first thread calling Lock (that is, Wait) finds a positive value in the semaphore, decrements it, and continues immediately.

  • The next thread calling Lock sees a zero value and has to wait until someone (not necessarily the first thread) calls Unlock (that is, Post).

    team bbl



    Cross-Platform GUI Programming with wxWidgets
    Cross-Platform GUI Programming with wxWidgets
    ISBN: 0131473816
    EAN: 2147483647
    Year: 2005
    Pages: 262

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net