|
|||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionThis article discusses the basic synchronization concepts and practices that are supposed to be useful for beginners to do multithreaded programming. By saying beginner, I don't mean those that are beginners in learning C++ language, but the people that are somewhat new in multithreaded programming. The main concentration of this article is on synchronization techniques. Thus this article is like a tutorial on synchronization. The General ViewDuring their execution, threads, more or less, are interoperating with each other. This interoperation may have various forms and may be of various kinds. For example, a thread, after performing the task it is assigned to, informs another thread about it. Then the second thread whose job is a logical continuation of the first thread starts operating. All the forms of interoperations might be described by the term synchronization which can be supported in several ways. Most usable ones are those whose primary aim is to support synchronization itself. The following objects are intended to support the synchronization (this is not a complete list):
Each of these objects has a different special purpose and usage but the general purpose is to support synchronization. I will introduce them to you through this article later. There are other objects that can be used as synchronization mediums such as To use the Wait-functionsThe following function is the simplest wait-function amongst the other wait-functions. It has the following declaration format: DWORD WaitForSingleObject
(
HANDLE hHandle,
DWORD dwMilliseconds
);
The parameter For example, the following call checks whether a process [identified by DWORD dw = WaitForSingleObject(hProcess, 0);
switch (dw)
{
case WAIT_OBJECT_0:
// the process has exited
break;
case WAIT_TIMEOUT:
// the process is still executing
break;
case WAIT_FAILED:
// failure
break;
}
As you notice, we passed 0 to the function's Next wait-function is similar to the previous one except that it takes a list of descriptors and waits until either one of them or all of them become signaled: DWORD WaitForMultipleObjects
(
DWORD nCount,
CONST HANDLE *lpHandles,
BOOL fWaitAll,
DWORD dwMilliseconds
);
The parameter For example, the following code decides which process will exit first from the list of given HANDLE h[3];
h[0] = hThread1;
h[1] = hThread2;
h[2] = hThread3;
DWORD dw = WaitForMultipleObjects(3, h, FALSE, 5000);
switch (dw)
{
case WAIT_FAILED:
// failure
break;
case WAIT_TIMEOUT:
// no processes exited during 5000ms
break;
case WAIT_OBJECT_0 + 0:
// a process with h[0] descriptor has exited
break;
case WAIT_OBJECT_0 + 1:
// a process with h[1] descriptor has exited
break;
case WAIT_OBJECT_0 + 2:
// a process with h[2] descriptor has exited
break;
}
As we see, the function can return different values which show the reason the function returned. You already know the meaning of the first two values. Next values are returned by the following logic; A thread, if it calls a wait-function, enters the kernel mode from the user mode. This fact is both bad and good. It is bad because to enter the kernel mode, approximately 1000 processor cycles are required which may be too expensive in a concrete situation. The good point is that after entering the kernel mode, no processor usage is needed; the thread is asleep. Let's turn to MFC and see what it can do for us. There are two classes that encapsulate calls to wait-functions;
Each of these classes inherits a single class - EventsGenerally, events are used in cases when a thread [or threads] is supposed to start doing its job after a specified action has occurred. For example, a thread might wait until the necessary data is gathered and then start saving them in the hard drive. There are two kinds of events; manual-reset and auto-reset. By using an event we simply can notify another thread that a specified action has occurred. With a first kind of event, that is manual-reset, a thread can notify more than one thread about a specified action. But with a second kind of event, that is auto-reset, only one can be notified. In MFC, there isCEvent class that encapsulates the event object (in terms of Windows, it is represented by an HANDLE value). The constructor of CEvent allows us to create both manual-reset and auto-reset events. By default, the second kind of event is created. To notify the waiting threads, we should use CEvent::SetEvent method, this means that this kind of call will make the event enter the signaled state. If the event is manual-reset, then it will stay in signaled state until a corresponding CEvent::ResetEvent call is invoked which will make the event enter the nonsignaled state. This is the feature that allows a thread to notify more than one thread by a single SetEvent call. If the event is auto-reset, then only one thread from all waiting threads will be able to receive the notification. After it is received by a thread, the event will automatically enter the nonsignaled state. The following two examples will illustrate these thoughts. The first example:
// create an auto-reset event
CEvent g_eventStart;
UINT ThreadProc1(LPVOID pParam)
{
::WaitForSingleObject(g_eventStart, INFINITE);
...
return 0;
}
UINT ThreadProc2(LPVOID pParam)
{
::WaitForSingleObject(g_eventStart, INFINITE);
...
return 0;
}
In this code, a global // create a manual-reset event
CEvent g_eventStart(FALSE, TRUE);
UINT ThreadProc1(LPVOID pParam)
{
::WaitForSingleObject(g_eventStart, INFINITE);
...
return 0;
}
UINT ThreadProc2(LPVOID pParam)
{
::WaitForSingleObject(g_eventStart, INFINITE);
...
return 0;
}
This code differs from the previous one by only the Yet another method for working with events - Example - WorkerThreadsIn this example I will show how to create worker threads and how to destroy them properly. Here we define a controlling function which is used by all threads. Every time we click the view, one thread is created. All the created threads use the mentioned controlling function which will draw a moving ellipse in the view's client area. Here a manual-reset event is used which informs all the working threads about their death. Besides, we will see how to make the primary thread wait until all the worker threads leave the scene.
All the ellipses are traversing in the client area and are not leaving its boundaries
Critical SectionsUnlike other synchronization objects, critical sections are working in the user mode unless there is a need to enter the kernel mode. If a thread tries to run a code that is caught be a critical section, it first does a spin blocking and after a specified amount of time, it enters the kernel mode to wait for the critical section. Actually, a critical section consists of a spin counter and a semaphore; the former is for the user mode waiting, and the later is for the kernel mode waiting (sleeping). In Win32 API, there is a int g_nVariable = 0;
UINT Thread_First(LPVOID pParam)
{
if (g_nVariable < 100)
{
...
}
return 0;
}
UINT Thread_Second(LPVOID pParam)
{
g_nVariable += 50;
...
return 0;
}
This is not a safe code as no thread has a monopoly access to CCriticalSection g_cs;
int g_nVariable = 0;
UINT Thread_First(LPVOID pParam)
{
g_cs.Lock();
if (g_nVariable < 100)
{
...
}
g_cs.Unlock();
return 0;
}
UINT Thread_Second(LPVOID pParam)
{
g_cs.Lock();
g_nVariable += 20;
g_cs.Unlock();
...
return 0;
}
Here, two methods of If there are more than two shared resources to be protected, it would be a good practice to use a separate critical section per resource. Do not forget to match There is a practice to embed critical sections into C++ classes and thus make them thread-safe. This kind of trick might be needed when the objects of a specific class are supposed to be used by more than one thread simultaneously. The big picture looks like this: class CSomeClass
{
CCriticalSection m_cs;
int m_nData1;
int m_nData2;
public:
void SetData(int nData1, int nData2)
{
m_cs.Lock();
m_nData1 = Function(nData1);
m_nData2 = Function(nData2);
m_cs.Unlock();
}
int GetResult()
{
m_cs.Lock();
int nResult = Function(m_nData1, m_nData2);
m_cs.Unlock();
return nResult;
}
};
It's possible that at the same time two or more threads call MutexesMutexes, like critical sections, are designated to protect shared resources from simultaneous accesses. Mutexes are implemented inside the kernel and thus they enter the kernel mode to operate. A mutex can perform synchronization not only between different threads but also between different processes. Such a mutex should have a unique name to be recognized by another process (such mutexes are called named mutexes). MFC represents CSingleLock singleLock(&m_Mutex);
singleLock.Lock(); // try to capture the shared resource
if (singleLock.IsLocked()) // we did it
{
// use the shared resource ...
// After we done, let other threads use the resource
singleLock.Unlock();
}
Or the same by Win32 API functions: // try to capture the shared resource
::WaitForSingleObject(m_Mutex, INFINITE);
// use the shared resource ...
// After we done, let other threads use the resource
::ReleaseMutex(m_Mutex);
A mutex can also be used to limit the number of running instances by a single one. The following code might be placed at the beginning of HANDLE h = CreateMutex(NULL, FALSE, "MutexUniqueName");
if (GetLastError() == ERROR_ALREADY_EXISTS)
{
AfxMessageBox("An instance is already running.");
return(0);
}
To guarantee a globally unique name, use a GUID instead. SemaphoresIn order to limit the number of threads that use shared resources we should use semaphores. A semaphore is a kernel object. It stores a counter variable to keep track of the number of threads that are using the shared resource. For example, the following code creates a semaphore by the MFC CSemaphore g_Sem(5, 5);
As soon as a thread gets access to the shared resource, the counter variable of the semaphore is decremented by one. If it becomes equal to zero, then any further attempt to use the resource will be rejected until at least one thread that has captured the resource leaves it (in other words, releases the semaphore). We may turn to // Try to use the shared resource
::WaitForSingleObject(g_Sem, INFINITE);
// Now the user's counter of the semaphore has decremented by one
//... Use the shared resource ...
// After we done, let other threads use the resource
::ReleaseSemaphore(g_Sem, 1, NULL);
// Now the user's counter of the semaphore has incremented by one
Communication between Secondary Threads and the Primary ThreadIf a primary thread wants to inform a secondary thread about some action, it is convenient to use an event object. But doing vice-versa will be inefficient and not convenient for users since stopping the primary thread to wait for an event may (and mostly does) slow down the application. In this case it would be correct to use user-defined messages to interact with the primary thread. Such a message should be addressed to a specific window which means that the descriptor of such a window should be visible to callers (secondary threads). To create a user-defined message, we firstly should define an identifier for that message (more correctly - define the message itself). Supposedly, such an identifier should be visible to both the primary thread and secondary threads: #define WM_MYMSG WM_USER + 1
#define WM_MYMSG WM_APP + 1
Next, a handler method should be declared for the message inside the window class declaration to which (window) the message is going to be addressed: afx_msg LRESULT OnMyMessage(WPARAM , LPARAM );
Of course, there should be some definition of the method: LRESULT CMyWnd::OnMyMessage(WPARAM wParam, LPARAM lParam)
{
// A notification got
// Do something ...
return 0;
}
And finally, to assign the handler to the message identifier, BEGIN_MESSAGE_MAP(CMyWnd, CWnd)
...
ON_MESSAGE(WM_MYMSG, OnMyMessage)
END_MESSAGE_MAP()
Now a secondary thread having a window handle [that lives in the primary thread], can notify it by the user-defined message as follows: UINT ThreadProc(LPVOID pParam)
{
HWND hWnd = (HWND) pParam;
...
// notify the primary thread's window
::PostMessage(hWnd, WM_MYMSG, 0, 0);
return 0;
}
HistoryThis text was first written more than three years ago. At that time I was a two-year old programmer. My intention was to write a book about MFC. Funny? But I was too young to write a book, and thus my chapters have stayed in my computer only. Now I've rewritten a text from there and submitted it to you. And of course, any note you think is worth suggesting about this essay would be appreciated very much. | ||||||||||||||||||||||||||||||