Win32 Multithreading and Synchronization

Win32 Multithreading and Synchronization


Table of Contents:

Introduction :

This tutorial will explain how to create and manage multiple threads in Windows. This is an advanced tutorial; the reader should be familiar with Win32 programming.

The need for responsive user-centric programs is a good reason to create programs that use multiple threads. By creating multiple threads, a program can seem to do many things at once (on a computer with more than one CPU, the program actually will do many things at once). For example, a word processor could have one thread that handles the painting of the processor's window while another thread saves the document being typed every few minutes while yet another thread actively monitors the document for spelling errors all without forcing the user to wait while each task is completed.

Background:

On a single CPU Windows machine, all threads are kept track of and given a slice of processor time (usually a few milliseconds). Windows will cycle through the list it keeps, pausing and resuming each thread. Once a thread has used up its time on the processor, Windows will pause the thread, record its CPU registers (along with some other data), and, after restoring all its data, activate the next thread. Each thread has two priorities: base and dynamic. The base priority can be changed, however, it cannot be changed to have a priority higher than that of the parent thread. Dynamic priority is the same as the base priority, however, it can be raised or lowered by Windows. Windows chooses which thread to run next based on the thread's dynamic priority. Naturally, threads with a higher dynamic priority run first.

Multithreading:

Initially, every program gets one thread, known as the primary thread, which is created by the Windows Object Manager. The primary thread can be used to create child threads. To create another thread call CreateThread. CreateThread takes the following parameters:

HANDLE CreateThread(

   LPSECURITY_ATTRIBUTES lpThreadAttributes, // access privileges

   DWORD dwStackSize,                        // initial stack size

   LPTHREAD_START_ROUTINE lpStartAddress,    // thread function

   LPVOID lpParameter,                       // thread argument

   DWORD dwCreationFlags,                    // creation option

   LPDWORD lpThreadId                        // thread identifier

);

      

If the first parameter is passed as NULL, the thread will get the default security descriptor. The second parameter allows you to set the initial stack size, you can pass 0 to give the thread the default stack size. The third parameter is a pointer to the function the thread should start executing. The fourth parameter holds any arguments you want to pass to the function. You can pass either a 0 or CREATE_SUSPENDED to the fifth parameter. If it is 0, the thread starts running as soon as it is created. If the value is CREATE_SUSPENDED, the thread will be created suspended and will not start running until you call ResumeThread. The last parameter is a pointer to a DWORD which will hold the unique ID of the thread, after it is created.

If a thread was created with the CREATE_SUSPENDED flag, you will want to call ResumeThread to start using it. ResumeThread takes the following parameter:

DWORD ResumeThread(HANDLE hThread);


hThread is the handle received by CreateThread. To suspend the thread again, call SuspendThread:

DWORD SuspendThread(HANDLE hThread);

Once again, hThread is the handle received by CreateThread.

While a thread is suspended, you may want to raise its base priority and then wake it again. A thread with a higher priority will get more processor time. To change a thread's base priority, call SetThreadPriority:

BOOL SetThreadPriority(

HANDLE hThread, // handle to the thread

int nPriority // thread priority level

);

nPriority can be one of seven values:

THREAD_PRIORITY_LOWEST Two levels below process
THREAD_PRIORITY_BELOW_NORMAL One level below process
THREAD_PRIORITY_NORMAL Normal priority
THREAD_PRIORITY_ABOVE_NORMAL One level above process
THREAD_PRIORITY_HIGHEST Two levels above process
THREAD_PRIORITY_TIME_CRITICAL Priority of 15
THREAD_PRIORITY_IDLE Priority of 1

 

To retrieve a thread's base priority level, call GetThreadPriority:

int GetThreadPriority(HANDLE hThread);

After you are done using a thread, you may want to terminate it. To do this, call ExitThread. ExitThread followed by a CloseHandle is the graceful way to shut down a thread. To immediately stop a thread, call TerminateThread. The specs of each function are:

VOID ExitThread( DWORD dwExitCode);

 

BOOL CloseHandle(HANDLE hThread);

 

BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode);


Windows automatically calls ExitThread when a thread ends its function. dwExitCode is the exit code you want to pass. The exit code can later be retrieved using GetExitCodeThread:

BOOL GetExitCodeThread(

HANDLE hThread,

LPDWORD lpExitCode // pointer to a DWORD to hold the exit code

);

The following is an example of how to use threads.

#define WIN32_LEAN_AND_MEAN

#include <windows.h>

#include <iostream>

DWORD ThreadID;

HANDLE TName;

void ExampleFunction()

{

for(int x=0; x<10; x++)

{

std:cout<<"Doing important stuff!\n";

}

}

void main()

{

TName= CreateThread(NULL, 0,

(LPTHREAD_START_ROUTINE)ExampleFunction,

NULL, CREATE_SUSPENDED, &ThreadID);

if (TName == NULL)

{

std::cout<<"Could not create thread!\n";

return false;

}

if(!SetThreadPriority(TName, THREAD_PRIORITY_BELOW_NORMAL))

{

std::cout<<"SetThreadPriority failed!\n";

return false;

}

if ((ResumeThread(TName)) == -1)

{

std::cout<<"ResumeThread failed!\n";

return false;

}

WaitForSingleObject(TName, INFINITE); // discussed later

CloseHandle(TName);

}

Synchronization:

Picture this: a word processor program creates two threads, one to read a file and another to write to a file. Everything is fine at first, the first thread waits for the second to finish writing to the file before reading it. The two threads work happily, everything is fine as long as the writing thread always writes first. But one dark day, the reading thread reads the file before the writing thread writes to the file and the program fails. This is known as a race condition because both threads race to finish their file operation. The program will always fail if the reading thread wins the race. A race condition is the result of bad synchronization. Another problem is the dreaded deadlock. In a deadlock, both threads will wait for each other to finish but neither thread will finish until the other finishes first, causing them to become locked. The solution to this problem is to use one, or more, of the synchronization objects provided by Windows.

To gain ownership of any of the synchronization objects, a thread can use WaitForSingleObject, WaitForSingleObjectEx, WaitForMultipleObjects, or WaitForMultipleObjectsEx.

WaitForSingleObject allows a thread to suspend itself while it waits for ownership of one of the synchronization objects.

DWORD WaitForSingleObject(

HANDLE hHandle, // handle to object

DWORD dwMilliseconds // time-out interval

);

The second parameter is the amount of time, in milliseconds, the thread is willing to wait for the object before it returns. If the second parameter is set to INFINITE, the function will not time-out. If the second parameter is set to 0, the function will try to gain ownership of the object and return immediately, even if it didn't gain ownership.

WaitForSingleObjectEx is the same as WaitForSingleObject except, it adds one more option: alert if I/O operation completes.

DWORD WaitForSingleObjectEx(

HANDLE hHandle, // handle to object

DWORD dwMilliseconds, // time-out interval

BOOL bAlertable // alertable option

);

If the last parameter is set to true, the function will return when an asynchronous I/O operation completes.

WaitForMultipleObjects allows a thread to wait for multiple synchronization objects at once. It can be set to return when any or all of the objects become available.

DWORD WaitForMultipleObjects(

DWORD nCount, // number of handles in array

CONST HANDLE *lpHandles, // object-handle array

BOOL bWaitAll, // wait option

DWORD dwMilliseconds // time-out interval

);

If the third parameter is set to TRUE, the function will wait for all of the objects to become available. If the third parameter is set to FALSE, the function will wait for any of the objects to become available, the return value of the function will be the index to the handles array to let you know which object that has been obtained.

WaitForMultipleObjectsEx is the same as WaitForSingleObjectEx, except it allows you to wait for multiple objects.

DWORD WaitForMultipleObjectsEx(

DWORD nCount, // number of handles in array

CONST HANDLE *lpHandles, // object-handle array

BOOL bWaitAll, // wait option

DWORD dwMilliseconds, // time-out interval

BOOL bAlertable // alertable option

);

Mutexes:

Like all other synchronization objects, a mutex is created by the Windows Object Manager. Mutex is short for mutual exclusion, meaning that only one thread can own it at a time. Think of it as a sort of ticket, any thread holding the ticket gets to access whatever is being protected by the mutex. While the thread is doing its job, all the other threads wait. Once the thread is done, it gives the ticket away and the next thread can do what it needs to do while the other threads wait.

To create a mutex call CreateMutex:

HANDLE CreateMutex(

LPSECURITY_ATTRIBUTES lpMutexAttributes,

BOOL bInitialOwner, // initial owner

LPCTSTR lpName // object's name

);

The first parameter can be passed as NULL to obtain the default access rights. If the second parameter is set to TRUE, the creator of the mutex will have ownership first. The third parameter is optional, it can be used to easily identify the mutex.

To release the mutex after doing whatever needs to be done, call ReleaseMutex.

BOOL ReleaseMutex(

HANDLE hMutex // handle to mutex

);

Only the thread that owns the mutex can release it. To destroy the mutex, call CloseHandle with the handle to the mutex.

Semaphores:

A semaphore is useful when you want to only allow a limited amount of threads to access a protected resource at a time. With a mutex, only one thread can own it at any given moment. With a semaphore, multiple threads can own it at a time. Consequently, any thread can also destroy the semaphore. To create a semaphore call CreateSemaphore.

HANDLE CreateSemaphore(

LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,

LONG lInitialCount, // initial count

LONG lMaximumCount, // maximum count

LPCTSTR lpName // object's name

);

The first parameter is handled the exact same as the first parameter of CreateMutex. The second parameter sets the initial count of the semaphore, usually it's set to the same value as that of the maximum count. The initial count cannot be less than zero nor greater than the maximum count. The third parameter sets the maximum count which limits the number of threads that can own the semaphore. The last parameter is handled the same way as that of CreateMutex. After a thread gains possession of the semaphore the initial count is decremented one. If the initial count reaches 0, no more threads can gain possession of the semaphore. The initial count is incremented after a thread releases the semaphore.

To release a semaphore, call ReleaseSemaphore.

BOOL ReleaseSemaphore(

HANDLE hSemaphore, // handle to semaphore

LONG lReleaseCount, // count increment amount

LPLONG lpPreviousCount // previous count

);

The second parameter sets by how much the count should be incremented, usually this is 1. The third parameter holds a pointer to a variable that will be filled with the previous count, after ReleaseSemaphore completes.

Critical Sections:

A critical section is very similar to a mutex. A critical section can only be owned by one thread at a time, however, a critical section cannot be shared between processes, a mutex can. Because of this, a critical section works more quickly. To create a critical section, call InitializeCriticalSection; to own a critical section, call EnterCriticalSection; to give up ownership of a critical section, call LeaveCriticalSection; to destroy a critical section, call DeleteCriticalSection.

VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

 

VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);


VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);


VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

 

All of the functions require a pointer to the critical section object. You declare one like this:

CRITICAL_SECTION cs;


Any thread that is of the process that created the critical section can destroy the critical section.

Events:

An event object is useful when you want to alert threads of an action occurring, a button being pushed for example. An event object is sent to all waiting threads. Call CreateEvent to create an event; call SetEvent to signal an event; call ResetEvent to turn the signal off. When an event is signaled, all threads that wait for it will receive it; when the event is not signaled, the threads will wait. You can also call PulseEvent to quickly signal and unsignal an event. This is useful in releasing the waiting threads.

CreateEvent takes the following parameters:

HANDLE CreateEvent(

LPSECURITY_ATTRIBUTES lpEventAttributes,

BOOL bManualReset, // reset type

BOOL bInitialState, // initial state

LPCTSTR lpName // object name

);


If the second parameter is set to TRUE, the event must be reset manually. If the second parameter is set to FALSE, the event will return to its unsignaled state immediately after a call to SetEvent. If the third parameter is set to TRUE, the event will be created and set to the signaled state. The last parameter is an optional name for the object.

SetEvent, ResetEvent, and PulseEvent are all called in the same way:

BOOL SetEvent(HANDLE hEvent);

 

BOOL ResetEvent(HANDLE hEvent);

 

BOOL PulseEvent(HANDLE hEvent);

 

hEvent being the handle returned from CreateEvent.

The combination of a mutex and an event is a good way to avoid deadlocks. The following is an example of using the mutex and event synchronization objects.

#define WIN32_LEAN_AND_MEAN

#include <windows.h>

#include <iostream>

HANDLE hMutex, hWriteDone, hReadDone;

int num, state;

void Writer()
{
for(int x=10; x>=0; x--)
{

while (true)
{

if (WaitForSingleObject(hMutex, INFINITE) == WAIT_FAILED)
{

std::cout<<"In writing loop, no mutex!\n";

ExitThread(0);
}

if (state == 0)
{

ReleaseMutex(hMutex);

WaitForSingleObject(hReadDone, INFINITE);

continue;
}
break;
}
std::cout<<"Write done\n";

num= x;

state= 0;

ReleaseMutex(hMutex);

PulseEvent(hWriteDone);
}
}

void Reader()
{
while(true)
{

if (WaitForSingleObject(hMutex, INFINITE) == WAIT_FAILED)
{
std::cout<<"In reader, no mutex!\n";

ExitThread(0);

}
if (state == 1)
{
ReleaseMutex(hMutex);

WaitForSingleObject(hWriteDone, INFINITE);

continue;
}

if (num == 0)
{

std::cout<<"End of data\n";

ReleaseMutex(hMutex);

ExitThread(0);
}
else {

std::cout<<"Read done\n";

state=1;

ReleaseMutex(hMutex);

PulseEvent(hReadDone);

}
}
}

void main()
{

HANDLE TName[2];
DWORD ThreadID;

state= 1;

hMutex= CreateMutex(NULL, FALSE, NULL);
hWriteDone= CreateEvent(NULL, TRUE, FALSE, NULL);
hReadDone= CreateEvent(NULL, TRUE, FALSE, NULL);

TName[0]= CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)Writer,
NULL, 0, &ThreadID);

TName[1]= CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)Reader,
NULL, 0, &ThreadID);

WaitForMultipleObjects(2, TName, TRUE, INFINITE);
CloseHandle(TName);
}

References: