Cross-Platform GUI Programming with wxWidgets
|
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:
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:
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:
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:
|
|