informa
8 MIN READ
Features

Programming Multi-Threaded Architectures - Mutex Objects and Critical Sections

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

semget
semctl

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

InitializeCriticalSection
InitializeCriticalSectionAndSpinCount

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();
}

_____________________________________________________

Latest Jobs

Manticore Games

San Mateo, California
8.23.22
Senior Software Engineer - Mobile

Sony PlayStation

San Diego, California
6.23.22
Sr. Online Programmer

The Walt Disney Company

Glendale, California
8.1.22
Associate Marketing Manager - Walt Disney Games

Insomniac Games

Burbank, California
8.26.22
Accessibility Design Researcher
More Jobs   

CONNECT WITH US

Explore the
Subscribe to
Follow us

Game Developer Job Board

Game Developer Newsletter

@gamedevdotcom

Explore the

Game Developer Job Board

Browse open positions across the game industry or recruit new talent for your studio

Browse
Subscribe to

Game Developer Newsletter

Get daily Game Developer top stories every morning straight into your inbox

Subscribe
Follow us

@gamedevdotcom

Follow us @gamedevdotcom to stay up-to-date with the latest news & insider information about events & more