Chapter: Multicore Application Programming For Windows, Linux, and Oracle Solaris - Windows Threading

Study Material, Lecturing Notes, Assignment, Reference, Wiki description explanation, brief detail

Slim Reader/Writer Locks

Windows Vista introduced support for the slim reader/writer lock, which is useful in situa-tions where data needs to be either read or updated.

Slim Reader/Writer Locks

 

Windows Vista introduced support for the slim reader/writer lock, which is useful in situa-tions where data needs to be either read or updated. The lock allows multiple threads to have read access or a single thread to have write access to the data. The lock is initialized using a call to InitializeSRWLock(). Since the locks are essentially user variables and use no kernel resource, there is no equivalent function to delete a lock.

 

The call to acquire a lock as a reader is AcquireSRWLockShared(), and the call for a reader to release it is ReleaseSRWLockShared(). Multiple readers can share access to the lock; however, a writer must obtain exclusive access. The call to acquire the lock as a writer is AcquireSRWLockExclusive(), and the call for a writer to release the lock is

ReleaseSRWLockExclusive().

 

Listing 6.20 shows an example of using a slim reader/writer lock. The reader/writer lock is useful in this situation because both the update and the read require access to two elements. If there were not a lock around the update and read of the array, it would be likely that an update would cause a read to return inconsistent data from the array.

 

Listing 6.20   Creating and Using a Slim Reader/Writer Lock

 

#include <process.h> #include <windows.h>

 

int array[100][100];

 

SRWLOCK lock;

 

unsigned int __stdcall        update( void *param )

 

{

 

for (int y=0; y<100; y++) for (int x=0; x<100; x++)

{

 

AcquireSRWLockExclusive( &lock );

 

array[x][y]++;

 

array[y][x]--;

 

ReleaseSRWLockExclusive( &lock );

 

}

 

return 0;

 

}

 

unsigned int __stdcall        read( void * param )

 

{

 

int value=0;

 

for (int y=0; y<100; y++) for (int x=0; x<100; x++)

{

 

AcquireSRWLockShared( &lock );

 

value = array[x][y] + array[y][x];

 

ReleaseSRWLockShared( &lock );

 

}

 

printf( "Value = %i\n", value ); return value;

 

}

 

int _tmain( int argc, _TCHAR* argv[] )

 

{

 

HANDLE h1, h2;

 

InitializeSRWLock( &lock );

 

h1 = (HANDLE)_beginthreadex( 0, 0, &update, (void*)0, 0, 0); h2 = (HANDLE)_beginthreadex( 0, 0, &read, (void*)0, 0, 0); WaitForSingleObject( h1, INFINITE );

 

WaitForSingleObject( h2, INFINITE );

 

CloseHandle( h1 );

 

CloseHandle( h2 ); getchar();

return 0;

 

}

Windows 7 introduced two further function calls into the slim reader/writer API. In the Windows Vista API, function calls to acquire the reader/writer lock will return only once the lock has been acquired. In some situations, it is better to perform some other operation in the event that the lock is not available. The two functions that provide this are TryAcquireSRWLockExclusive() and TryAcquireSRWLockShared(). These two functions return immediately. If the lock is available, the function call will have acquired the lock for the thread, and the return value will be nonzero; if the lock is unavailable, the return value will be zero.

 

Semaphores

 

Semaphores are a way of keeping track of a count of numbers, as well as a way of com-municating resource availability between threads. At the simplest level, they can be used as an alternative implementation of a mutex, while a more complex use would be to communicate readiness between multiple threads.

 

A semaphore can be created through a call to CreateSemaphore(), which takes four parameters. The first parameter is the security attributes, or it is null if the default is to be used. The second parameter is the initial value for the semaphore. The third parameter is the maximum value for the semaphore. The final parameter is an optional name for the semaphore. The name can be used when other threads or processes want to attach to the same semaphore. If a semaphore of the given name exists and it was created with the SEMAPHORE_ALL_ACCESS access right, then the function will return a handle to the existing semaphore.

 

The second way of creating a semaphore is through the CreateSemaphoreEx() call. This takes the same first four parameters but adds two more. The fifth parameter is a set of flags, but it must be passed the value zero. The sixth parameter is for access rights.

 

Passing SEMAPHORE_ALL_ACCESS as this parameter will create a semaphore that can be shared between processes. The name of the shared semaphore also needs to be placed in the global namespace. Creating shared semaphores will be covered later in this chapter.

 

The final way of getting a handle to a semaphore is to call OpenSemaphore(), pass-ing in three parameters. The first parameter gives the desired access rights. The second parameter is a boolean that indicates whether the handle to the semaphore should be inherited by child processes. This is one of the options available through the security attribute used by SemaphoreCreate() and SemaphoreCreateEx(). The third parame-ter is the name of the semaphore. This function call will not create the semaphore if it does not already exist.

 

Semaphores are kernel objects, so the create function will return a handle to the new semaphore. When the application has finished with the semaphore, it should release it with a call to CloseHandle. Once there are no outstanding handles to the semaphore, the kernel object is disposed of.

 

A semaphore can be decremented through a call to one of the wait functions. The one that is most likely to be useful is WaitForSingleObject(), which takes the handle of the semaphore and a timeout. The function will either return having decremented the semaphore or return when the timeout expires.

A semaphore can be incremented through a call to ReleaseSemaphore(). This call takes three parameters: the handle of the semaphore, the amount to increment the sema-phore by, and an optional pointer to a LONG variable where the previous value will be written. Attempts to increment the semaphore beyond the maximum value that it can hold are ignored. It is important to notice that a semaphore has no concept of owner-ship, so it cannot tell whether a thread attempts to increment the semaphore by a greater amount than it was decremented.

 

Listing 6.21 shows an example of a semaphore being used as a replacement for a mutex. The semaphore is created to hold a maximum value of 1 and an initial value of 1. Two threads are created, and both threads execute the same code, which increments the variable value by 200. The end result of this is that the variable value should contain 400 when the application terminates.

 

Listing 6.21   Using a Semaphore as a Mutex

#include <windows.h> #include <process.h> #include <stdio.h>

 

HANDLE semaphore; int value;

 

void addToValue( int increment )

 

{

 

WaitForSingleObject( semaphore, INFINITE );

 

value+=increment;

 

ReleaseSemaphore( semaphore, 1, 0 );

 

}

 

unsigned int __stdcall test( void * )

 

{

 

for ( int counter=0; counter<100; counter++ )

 

{

 

addToValue( 2 );

 

}

 

return 0;

 

}

 

int _tmain( int argc, _TCHAR* argv[])

 

{

 

HANDLE h1, h2; value = 0;

semaphore = CreateSemaphore( 0, 1, 1, 0 );

 

h1 = (HANDLE)_beginthreadex( 0, 0, &test, (void*)0, 0, 0); h2 = (HANDLE)_beginthreadex( 0, 0, &test, (void*)0, 0, 0); WaitForSingleObject( h1, INFINITE );

WaitForSingleObject( h2, INFINITE );

 

CloseHandle( h1 );

 

CloseHandle( h2 );

 

CloseHandle( semaphore );

 

printf( "Value = %i\n", value );

 

getchar(); return 0;

}

 

Condition Variables

 

Condition variables were introduced in Vista. They work with either a critical section or a slim reader/writer lock to allow threads to sleep until a condition becomes true. They are user constructs, so they cannot be shared across processes. The call InitializeConditionVariable() initializes the condition variable for use.

 

A thread uses a condition variable either by acquiring a slim reader/writer lock and then calling SleepConditionVariableSRW() or by entering a critical section and call-ing SleepConditionVariableCS(). When the threads are woken from the sleep call, they will again have acquired either the critical section lock or the reader/writer lock (depending on how the condition variable is being used). The first thing that the thread needs to do is test to determine whether the conditions it is waiting on are true, since it is possible for the thread to be woken when the conditions are not met. If the conditions have not been met, the thread should return to sleeping on the condition variable.

 

There are two calls to wake threads sleeping on a condition variable. WakeConditionVariable() wakes one of the threads waiting on a condition variable. WakeAllConditionVariable() wakes all the threads sleeping on a condition variable.

 

Listing 6.22 shows an example of using a condition variable to mediate a producer-consumer pairing of threads. The producer thread would add items onto a queue. To do this, the thread first needs to enter the critical section where it is safe to manipulate the queue data structure. Once the item has been added to the queue, it is safe to exit the critical section. The number of items originally in the queue is returned by the addItemToQueue() function. If there were no items in the queue, then it is possibl that other threads are waiting on the condition variable and need to be woken up by the producer thread.

 

Listing 6.22   Producer-Consumer Example Using a Condition Variable

#include <windows.h>

 

CONDITION_VARIABLE CV;

 

CRITICAL_SECTION CS;

 

void addItem( int value )

{

LONG oldQueueLength;

 

EnterCriticalSection( &CS );

 

oldQueueLength = queueLength;

 

addItemToQueue( value );

 

LeaveCriticalSection( &CS );

 

if ( oldQueueLength==0 )  // If the queue was empty

 

{

 

WakeConditionVariable( &CV ); // Wake one sleeping thread

 

}

 

}

 

int removeItem()

 

{

 

int item;

 

EnterCriticalSection( &CS );

 

while ( QueueLength==0 )  // If the queue is empty

 

{

 

SleepConditionVariableCS( &CV, &CS, INFINITE );      // Sleep

 

}

 

item = removeItemFromQueue();

 

LeaveCriticalSection( &CS ); return item;

}

 

void _tmain()

 

{

 

InitializeCriticalSection( &CS );

 

InitializeConditionVariable( &CV );

 

 

DeleteCriticalSection( &CS );

 

}

 

The consumer thread enters the critical section to remove an item from the queue. If there are no items on the queue, it sleeps on the condition variable. When it is woken, either it is a spurious wake-up or there is an item in the queue. If the wake-up was spu-rious, the thread will return to sleep. Otherwise, it will remove an item from the queue, exit the critical section, and return the item from the queue to the calling function.


Study Material, Lecturing Notes, Assignment, Reference, Wiki description explanation, brief detail


Copyright © 2018-2020 BrainKart.com; All Rights Reserved. Developed by Therithal info, Chennai.