Windows Thread Synchronization 8

 

 

 

Condition Variables

 

Condition variables are synchronization primitives that enable threads to wait until a particular condition occurs. Condition variables are user-mode objects that cannot be shared across processes. Condition variables enable threads to atomically release a lock and enter the sleeping state. They can be used with critical sections or slim reader/writer (SRW) locks. Condition variables support operations that wake one or wake all waiting threads. After a thread is woken, it re-acquires the lock it released when the thread entered the sleeping state. For Windows Server 2003 and Windows XP/2000,  condition variables are not supported. The following are the condition variable functions.

 

Condition variable function

Description

InitializeConditionVariable()

Initializes a condition variable.

SleepConditionVariableCS()

Sleeps on the specified condition variable and releases the specified critical section as an atomic operation.

SleepConditionVariableSRW()

Sleeps on the specified condition variable and releases the specified SRW lock as an atomic operation.

WakeAllConditionVariable()

Wakes all threads waiting on the specified condition variable.

WakeConditionVariable()

Wakes a single thread waiting on the specified condition variable.

 

The following pseudo code demonstrates the typical usage pattern of condition variables.

 

 

CRITICAL_SECTION CritSection;

CONDITION_VARIABLE ConditionVar;

 

void PerformOperationOnSharedData()

{

   EnterCriticalSection(&CritSection);

 

   // Wait until the predicate is TRUE

   while(TestPredicate() == FALSE)

   {

      SleepConditionVariableCS(&ConditionVar, &CritSection, INFINITE);

   }

 

   // The data can be changed safely because we own the critical

   // section and the predicate is TRUE

   ChangeSharedData();

 

   LeaveCriticalSection(&CritSection);

 

   // If necessary, signal the condition variable by calling

   // WakeConditionVariable or WakeAllConditionVariable so other

   // threads can wake

}

 

For example, in an implementation of a reader/writer lock, the TestPredicate() function would verify that the current lock request is compatible with the existing owners. If it is, acquire the lock; otherwise, sleep. Condition variables are subject to spurious wakeups (those not associated with an explicit wake) and stolen wakeups (another thread manages to run before the woken thread). Therefore, you should recheck a predicate (typically in a while loop) after a sleep operation returns. You can wake other threads using WakeConditionVariable() or WakeAllConditionVariable() either inside or outside the lock associated with the condition variable. It is usually better to release the lock before waking other threads to reduce the number of context switches. It is often convenient to use more than one condition variable with the same lock. For example, an implementation of a reader/writer lock might use a single critical section but separate condition variables for readers and writers.

 

Slim Reader/Writer (SRW) Locks

 

Slim reader/writer (SRW) locks enable the threads of a single process to access shared resources; they are optimized for speed and occupy very little memory. Reader threads read data from a shared resource whereas writer threads write data to a shared resource. When multiple threads are reading and writing using a shared resource, exclusive locks such as a critical section or mutex can become a bottleneck if the reader threads run continuously but write operations are rare. SRW locks provide two modes in which threads can access a shared resource:

 

  1. Shared mode grants shared read-only access to multiple reader threads, which enables them to read data from the shared resource concurrently. If read operations exceed write operations, this concurrency increases performance and throughput compared to critical sections.
  2. Exclusive mode grants read/write access to one writer thread at a time. When the lock has been acquired in exclusive mode, no other thread can access the shared resource until the writer releases the lock.

 

A single SRW lock can be acquired in either mode; reader threads can acquire it in shared mode whereas writer threads can acquire it in exclusive mode. There is no guarantee about the order in which threads that request ownership will be granted ownership; SRW locks are neither fair nor FIFO (First In First Out). An SRW lock is the size of a pointer. The advantage is that it is fast to update the lock state. The disadvantage is that very little state information can be stored, so SRW locks cannot be acquired recursively. In addition, a thread that owns an SRW lock in shared mode cannot upgrade its ownership of the lock to exclusive mode. The following are the SRW lock functions.

 

SRW lock function

Description

AcquireSRWLockExclusive()

Acquires an SRW lock in exclusive mode.

AcquireSRWLockShared()

Acquires an SRW lock in shared mode.

InitializeSRWLock()

Initialize an SRW lock.

ReleaseSRWLockExclusive()

Releases an SRW lock that was opened in exclusive mode.

ReleaseSRWLockShared()

Releases an SRW lock that was opened in shared mode.

SleepConditionVariableSRW()

Sleeps on the specified condition variable and releases the specified lock as an atomic operation.

 

One-Time Initialization

 

Components are often designed to perform initialization tasks when they are first called, rather than when they are loaded. The one-time initialization functions ensure that this initialization occurs only once even when multiple threads may attempt the initialization. Many applications use the interlocked functions to ensure that only one thread performs the initialization. It is better to use the one-time initialization functions for the following reasons:

 

  1. They are optimized for speed.
  2. They create the appropriate barriers on processor architectures that require them.
  3. They support both locked and parallel initialization.
  4. They avoid internal locking so the code can operate asynchronously or synchronously.

 

The initialization process is managed through a one-time initialization structure. This structure contains data and state information.

 

One-Time Initialization: Synchronous Mode

 

The following steps describe one-time initialization in synchronous mode.

 

  1. Initially, the data stored with the initialization structure is NULL.
  2. When the first thread successfully calls the InitOnceBeginInitialize() function (without the INIT_ONCE_ASYNC flag), one-time initialization begins. Subsequent threads that attempt this initialization are blocked until this initialization completes or fails; if the first thread fails the next thread is allowed to attempt the initialization and so on. The calling thread should create a synchronization object and specify it in the lpContext parameter of the InitOnceComplete() function.

Alternatively, the first thread can call the InitOnceExecuteOnce() function to begin one-time initialization and execute the InitOnceCallback() callback function. The callback function should return a handle to the synchronization object in its lpContext parameter.

  1. If the initialization succeeds, the lpContext handle is stored in the initialization structure. Subsequent initialization attempts return this context data. If the initialization fails, the data is NULL.

 

One-Time Initialization: Asynchronous Mode

 

The following steps describe one-time initialization in asynchronous mode.

 

  1. Initially, the data stored with the initialization structure is NULL.
  2. When the first thread successfully calls the InitOnceBeginInitialize() function with the INIT_ONCE_ASYNC flag, one-time initialization begins. Concurrent attempts to initiate initialization do not change the state, but proceed as expected. Each thread should create a synchronization object and return it in the lpContext parameter of the InitOnceComplete() function. One thread will succeed in the completion attempt and the others must clean up their initialization.
  3. If initialization succeeds, the lpContext handle is stored in the initialization structure. Subsequent initialization attempts return this context data.

 

 

 

< Thread Synchronization 7 | Thread Synchronization Programming | Win32 Programming | Thread Synchronization 9 >