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:
hThread is the handle received by CreateThread. To suspend the thread again, call SuspendThread:
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:
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:
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:
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:
HANDLE hThread,
LPDWORD lpExitCode // pointer to a DWORD to hold the exit code
);
The following is an example of how to use threads.
#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.
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.
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 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 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:
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.
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.
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.
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.
All of the functions require a pointer to the critical section object. You declare one like this:
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:
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:
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.
#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:
- Microsoft Developer Network
- Windows 98 Developer's Handbook by Ben Ezzell and Jim Blaney, Sybex Publishing