Concurrency and Race Conditions
Race condition happens in many cases not just for threads and processes. A race condition occurs when two threads access a shared variable at the same time (concurrent). The first thread reads or accesses the variable, and at the 'same' time the second thread reads the same value from the variable. Then the first thread and second thread perform their operations such as write, on the same value, and they race to see which thread can write the value last to the shared variable. The value of the thread that writes its value last is preserved, because the thread is writing over the value that the previous thread wrote and of course the first thread which wrote first will have incorrect result though the process was completed successfully. Each thread is allocated a predefined period of time to execute on a processor using such as round robin, interleaving and other methods. When the time slice that is allocated for the thread expires, the thread's context is saved until its next turn on the processor (context switching), and the processor begins the execution of the next thread.
The reason for this is that the operating system decides which thread gets executed first. The order and timing in which the threads start are not all that important. So, a thread that is given 'least priority' for example, by the operating system gets executed last. The most common symptom of a race condition is unpredictable values of variables that are shared between multiple threads. This results from the unpredictability of the order in which the threads execute. Sometime one thread wins, and sometime the other thread wins. At other times, execution works correctly. Also, if each thread is executed separately, the variable value behaves correctly. The race condition is a well known problem that is very difficult to debug. The following Figure tries to demonstrate the race condition, showing some possible interleaving of threads will results in an undesired final computation values.
Can you imagine the result if there are more than two threads which are racing each other to access the shared resources such as in multithreading?
Race Condition Program Example
The following code example tries to demonstrate the race condition (without any synchronization mechanism).
Create a new empty Win32 console application project. Give a suitable project name and change the project location if needed.
Then, add the source file and give it a suitable name.
Next, add the following source code.
#include <windows.h>
#include <stdio.h>
const DWORD numThreads = 4;
DWORD WINAPI helloFunc(LPVOID arg)
{
// The call to the wprintf() will affect the thread time execution
wprintf(LRetard program, I'm thread %u\n, GetCurrentThreadId());
// This is a dummy sleep to simulate tasks to be completed.
// The value also will affect the thread time execution
// You may want to test different Sleep() values...
Sleep(1000);
return 0;
}
int wmain()
{
HANDLE hThread[numThreads];
DWORD dwThreadID, dwEvent, i;
for(int i=0;i<numThreads;i++)
{
hThread[i] = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)helloFunc,(LPVOID)dwThreadID,0,&dwThreadID);
if(hThread[i] != NULL)
wprintf(LCreateThread() is OK, thread ID is %u\n, dwThreadID);
else
wprintf(LCreateThread() failed, error %u\n,GetLastError());
}
// Waits until one or all of the specified objects are
// in the signaled state or the time-out interval elapses.
// 3rd param - TRUE, the function returns when the state of all objects in the lpHandles array is signaled.
// If FALSE, the function returns when the state of any one of
// the objects is set to signaled. In the latter case, the return value
// indicates the object whose state caused the function to return.
// 4th param - If INFINITE, the function will
// return only when the specified objects are signaled.
dwEvent = WaitForMultipleObjects(numThreads,hThread,FALSE,INFINITE);
wprintf(L\n);
switch (dwEvent)
{
// hThread[0] was signaled
case WAIT_OBJECT_0 + 0:
// TODO: Perform tasks required by this event
wprintf(LFirst event was signaled...\n);
break;
// hThread[1] was signaled
case WAIT_OBJECT_0 + 1:
// TODO: Perform tasks required by this event
wprintf(LSecond event was signaled...\n);
break;
// hThread[2] was signaled
case WAIT_OBJECT_0 + 2:
// TODO: Perform tasks required by this event
wprintf(LThird event was signaled...\n);
break;
// hThread[3] was signaled
case WAIT_OBJECT_0 + 3:
// TODO: Perform tasks required by this event
wprintf(LFourth event was signaled...\n);
break;
case WAIT_TIMEOUT:
wprintf(LWait timed out...\n);
break;
// Return value is invalid.
default:
wprintf(LWait error %d\n, GetLastError());
ExitProcess(0);
}
wprintf(L\n);
for(i = 0;i<4;i++)
{
if(CloseHandle(hThread[i]) != 0)
wprintf(LClosing the hThread[%d] handle is OK...\n, i);
else
wprintf(LFailed to close the hThread[%d] handle, error %u...\n, GetLastError());
}
return 0;
}
Build and run the project. The following are the sample outputs when the program was run many times.
The following are sample outputs when we change the Sleep(1000); to the smaller value, Sleep(500);
By editing the following part, the output should be clearer.
…
…
for(int i=0;i<numThreads;i++)
{
hThread[i] = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)helloFunc,(LPVOID)dwThreadID,0,&dwThreadID);
if(hThread[i] != NULL)
wprintf(LCreateThread() is OK, #%d thread ID is %u\n, i, dwThreadID);
else
wprintf(LCreateThread() failed, error %u\n, GetLastError());
}
…
…
The following screenshot is a sample output.
Next, add/edit some part of the code as shown below, demonstrating the threads is doing some tasks on the global variable x.
#include <windows.h>
#include <stdio.h>
// Global variable
// May use the 'volatile' keyword instead of 'const' to avoid
// the compiler optimization especially for the Release version
const DWORD numThreads = 4;
DWORD x = 0;
DWORD WINAPI helloFunc(DWORD arg)
{
// The call to the wprintf() will affect the thread time execution
wprintf(LThread %u, arg = %u\n, GetCurrentThreadId(), arg);
// Try updating the global variable, x
x = x + arg;
wprintf(Lx = %u\n, x);
// This is a dummy sleep to simulate tasks to be completed.
// The value also will affect the thread time execution
// You may want to test different Sleep() values...
Sleep(1000);
return 0;
}
int wmain()
{
HANDLE hThread[numThreads];
DWORD dwThreadID, dwEvent, i;
for(int i=0;i<numThreads;i++)
{
hThread[i] = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)helloFunc,(LPVOID)i,0,&dwThreadID);
if(hThread[i] != NULL)
wprintf(LCreateThread() is OK, #%d thread ID is %u\n, i, dwThreadID);
else
wprintf(LCreateThread() failed, error %u\n,GetLastError());
}
// Waits until one or all of the specified objects are
// in the signaled state or the time-out interval elapses.
// 3rd param - TRUE, the function returns when the state of all objects in the lpHandles array is signaled.
// If FALSE, the function returns when the state of any one of
// the objects is set to signaled. In the latter case, the return value
// indicates the object whose state caused the function to return.
// 4th param - If INFINITE, the function will
// return only when the specified objects are signaled.
dwEvent = WaitForMultipleObjects(numThreads,hThread,FALSE,INFINITE);
wprintf(L\n);
switch (dwEvent)
{
// hThread[0] was signaled
case WAIT_OBJECT_0 + 0:
// TODO: Perform tasks required by this event
wprintf(LFirst event was signaled...\n);
break;
// hThread[1] was signaled
case WAIT_OBJECT_0 + 1:
// TODO: Perform tasks required by this event
wprintf(LSecond event was signaled...\n);
break;
// hThread[2] was signaled
case WAIT_OBJECT_0 + 2:
// TODO: Perform tasks required by this event
wprintf(LThird event was signaled...\n);
break;
// hThread[3] was signaled
case WAIT_OBJECT_0 + 3:
// TODO: Perform tasks required by this event
wprintf(LFourth event was signaled...\n);
break;
case WAIT_TIMEOUT:
wprintf(LWait timed out...\n);
break;
// Return value is invalid.
default:
wprintf(LWait error %d\n, GetLastError());
ExitProcess(0);
}
wprintf(L\n);
for(i = 0;i<4;i++)
{
if(CloseHandle(hThread[i]) != 0)
wprintf(LClosing the hThread[%d] handle is OK...\n, i);
else
wprintf(LFailed to close the hThread[%d] handle, error %u...\n, GetLastError());
}
return 0;
}
Rebuild and re-run the program several times. The following screenshots show the sample outputs.
Can you see the 'funny' outputs mainly the values of the global variable x? The outputs are not consistent, non-deterministic and time sensitive. Sometimes the outputs are in the sequence as expected; however, many times the outputs are not as expected. Keep in mind that the program (thread execution) runs without 'error'.