Trending
Opinion: How will Project 2025 impact game developers?
The Heritage Foundation's manifesto for the possible next administration could do great harm to many, including large portions of the game development community.
In this next-gen oriented article, The Collective's Technical Director Philippe Paquet explains how mutex and critical sections can avoid the concurrent use of un-shareable resources, when multiple threads have shared access to the same resource such as a file or a block of memory.
1. Introduction
When multiple threads have shared access to the same resource such as a file or a block of memory, threads can interfere with one another. Mutex and critical sections are two mechanics used to avoid the concurrent use of un-shareable resources.
2. Mutex Objects
Mutex is an abbreviation for Mutual exclusion. Mutex objects are system objects that can only be owned by a single thread at any given time. Below is a Win32 example using mutex objects to control access to a global counter.
//
// Mutex.cpp
//
// Example using critical sections
//
//
#include <windows.h>
#include <stdio.h>
#include <process.h>
HANDLE g_hMutex;
bool g_bThreadOneFinished = false;
bool g_bThreadTwoFinished = false;
int g_iResult = 0;
void ThreadOne( void *)
{
for ( int i = 0; i < 10000; i++)
{
// Request ownership of mutex
WaitForSingleObject(g_hMutex, INFINITE);
// Access the shared resource
g_iResult += 1;
// Release the mutex
ReleaseMutex(g_hMutex);
}
// Finished
g_bThreadOneFinished = true;
_endthread();
}
void ThreadTwo( void *)
{
for ( int i = 0; i < 10000; i++)
{
// Request ownership of mutex
WaitForSingleObject(g_hMutex, INFINITE);
// Access the shared resource
g_iResult += 1;
// Release the mutex
ReleaseMutex(g_hMutex);
}
// Finished
g_bThreadTwoFinished = true;
_endthread();
}
int main()
{
// Create the mutex
g_hMutex = CreateMutex(NULL, FALSE, "MutexName");
// Start the threads
_beginthread(ThreadOne, 0, NULL);
_beginthread(ThreadTwo, 0, NULL);
// Wait for the threads to finish
while (( false == g_bThreadOneFinished)
|| ( false == g_bThreadTwoFinished))
{
Sleep(1);
}
// Free the mutex
CloseHandle(g_hMutex);
// Print the result
printf("Result: %i\n", g_iResult);
}
In our example main function, CreateMutex is called to create the mutex object. In the Win32 API, when created, a mutex object can be named and it is possible to specify if the calling thread immediately own the newly created mutex object.
In the ThreadOne and ThreadTwo functions, WaitForSingleObject is used to request and wait for ownership of the mutex object. You should note that while the WaitForSingleObject function lets a thread wait on a single mutex object, the Win32 API implements a WaitForMultipleObjects function that will let a thread wait on multiple mutex objects.
When the ThreadOne and ThreadTwo functions have accessed the shared resource, our g_iResult global counter, they release the mutex object with a call to ReleaseMutex. If calls to WaitForSingleObject and ReleaseMutex are nested, the thread must call the ReleaseMutex function once for each call to WaitForSingleObject.
As in our main function, when a mutex is no longer needed, the CloseHandle function is used to tell the system to free both the mutex object and the associated data.
The table below shows equivalence between the Win32 API and the Linux API. As you can see, the equivalent of a Win32 mutex object is a Linux semaphore.
Win32 | Linux |
CreateMutex | semgetsemctl |
CloseHandle | semctl |
WaitForSingleObject | semop |
ReleaseMutex | semop |
3. Critical Sections
A critical section is a code section that can only be accessed by a single thread at any given time. A synchronization mechanism, usually a semaphore, is used to protect the critical section. Below is a Win32 example using critical sections to control access to a global counter.
//
// CriticalSection.cpp
//
// Example using critical sections
//
//
#include <windows.h>
#include <stdio.h>
#include <process.h>
CRITICAL_SECTION g_criticalSection;
bool g_bThreadOneFinished = false;
bool g_bThreadTwoFinished = false;
int g_iResult = 0;
void ThreadOne( void *)
{
for ( int i = 0; i < 10000; i++)
{
// Request ownership of the critical section
EnterCriticalSection(&g_criticalSection);
// Access the shared resource
g_iResult += 1;
// Release ownership of the critical section
LeaveCriticalSection(&g_criticalSection);
}
// Finished
g_bThreadOneFinished = true;
_endthread();
}
void ThreadTwo( void *)
{
for ( int i = 0; i < 10000; i++)
{
// Request ownership of the critical section
EnterCriticalSection(&g_criticalSection);
// Access the shared resource
g_iResult += 1;
// Release ownership of the critical section
LeaveCriticalSection(&g_criticalSection);
}
// Finished
g_bThreadTwoFinished = true;
_endthread();
}
int main()
{
// Initialize the critical section
InitializeCriticalSection(&g_criticalSection);
// Start the threads
_beginthread(ThreadOne, 0, NULL);
_beginthread(ThreadTwo, 0, NULL);
// Wait for the threads to finish
while (( false == g_bThreadOneFinished)
|| ( false == g_bThreadTwoFinished))
{
Sleep(1);
}
// Release resources used by the critical section
DeleteCriticalSection(&g_criticalSection);
// Print the result
printf("Result: %i\n", g_iResult);
}
The main function of our example uses InitializeCriticalSection to setup the critical section. InitializeCriticalSection will not only initialize the data structure but will also create a system object (a semaphore) used to arbitrate the ownership of the critical section when contention arises. Unlike for mutex objects, the process is responsible for the memory used by the data structure. However, copying or moving the data structure will result in an undetermined behavior (a crash if you're lucky, data corruption if you're not).
In the ThreadOne and ThreadTwo functions, EnterCriticalSection is used to request and wait for ownership of the critical section while LeaveCriticalSection is used to release that ownership after incrementing our global counter.
When the two threads are finished, the main function releases the critical section and the associated system object by calling DeleteCriticalSection.
The table below shows equivalence between the Win32 API and the Linux API. As you can see, the equivalent of a Win32 critical section is a Linux mutex object.
Win32 | Linux |
InitializeCriticalSectionInitializeCriticalSectionAndSpinCount | pthread_mutex_init |
EnterCriticalSection | pthread_mutex_lock |
TryEnterCriticalSection | pthread_mutex_trylock |
LeaveCriticalSection | pthread_mutex_unlock |
DeleteCriticalSection | pthread_mutex_destroy |
4. Differences between Mutex and Critical Sections
As seen in the previous example, mutex objects and critical sections look very similar and behave almost identically. However, there are fundamental differences between them:
Critical sections don't work cross processes while named mutex objects do.
Unlike critical sections, it is possible to test mutex objects for abandonment. When a thread owning a critical section is terminated, the state of the critical section is undefined and an application can be deadlocked waiting for that critical section. When a thread owning a mutex object is terminated, the mutex object state changes to abandoned, allowing the application to care for the situation and avoid a deadlock.
Unlike critical sections, it is possible to specify a timeout value when waiting for a mutex object. A timeout value will allow the application to avoid a deadlock and care for that particular situation.
Mutex objects are very expensive to use. Every operation performed on a mutex object requires a user mode to kernel mode transition, as does waiting on the object. A user mode to kernel mode transition is a particularly slow operation requiring a minimum of 600 clock cycles.
5. When Should You Use a Mutex Object?
When synchronization across processes is required, you should use mutex objects. It is not possible to use critical sections across processes.
When stability is more important than speed, you should use mutex objects. The ability of mutex objects to be tested for abandonment and the possibility to specify a timeout value while waiting make mutex objects a far more robust synchronization solution than critical sections.
6. When Should You Use a Critical Section?
When speed is more important than stability, you should use critical sections. Mutex objects require a user mode to kernel mode transition. As that transition comes with a very high cost, critical sections comes into play. As long as there is no conflict, interlocked instructions are used to acquire and release the ownership of a critical section. Only when a conflict arises, a user mode to kernel mode transition is required to transfer the ownership of a critical section.
7. Debugging Mutex Object and Critical Sections.
As always, plan for debugging from the very beginning. Planning for debugging is important for any type of application but it is crucial in multi-threaded architectures.
The first thing you should be writing is debugging code. More precisely, thread safe versions of the following systems:
trace message system
log system
dump system
If those facilities are available in the API you are using - don't reinvent the wheel - use them.
Design your application to run both as a serial application and as a parallel application. By doing so, you will be able to debug your application as a serial application before having to debug it as a parallel application.
Always call GetLastError after calling CreateMutex. If you don't need to name a mutex object, don't name it. A mutex object can already exist in the system. If a mutex object already exists, CreateMutex will fail while returning a valid mutex object handle and you may, unknowingly, use a mutex object created by another application or by a conflicting instance of your application. Additionally, mutex objects share their name space with other system objects (events, semaphores, timers, jobs, and file-mapping object) increasing the risk of conflict. Following is an example of safe mutex object creation.
// Create the mutex
g_hMutex = CreateMutex(NULL, FALSE, "MutexName");
DWORD dwResult = GetLastError();
if (ERROR_ALREADY_EXISTS == dwResult)
{
// Finished process prematurely
ThreadSafeOutputDebugString("Mutex object already exist.");
exit(-1);
}
if (ERROR_INVALID_HANDLE == dwResult)
{
// Finished process prematurely
ThreadSafeOutputDebugString("Mutex name conflict?");
exit(-1);
}
if (NULL == g_hMutex)
{
// Finished process prematurely
ThreadSafeOutputDebugString("Mutex object creation failed.");
exit(-1);
}
Always test the result of WaitForSingleObject as a mutex object can be abandoned and the request for ownership can time out. Following is an example of safe mutex object acquisition.
// Request ownership of mutex
DWORD dwResult = WaitForSingleObject(g_hMutex, INFINITE);
if (WAIT_ABANDONED == dwResult)
{
// Finished thread prematurely
ThreadSafeOutputDebugString("Mutex was abandoned.");
_endthread();
}
if (WAIT_TIMEOUT == dwResult)
{
// Finished thread prematurely
ThreadSafeOutputDebugString("Mutex timed out");
_endthread();
}
_____________________________________________________
Read more about:
FeaturesYou May Also Like