Mutex Objects
A mutex (short for mutual exclusion) is a synchronization primitive that can be used to protect shared data from being simultaneously accessed by multiple threads. A thread acquires the mutex before accessing the shared data and releases the mutex after it has finished accessing the data. If another thread tries to acquire the mutex while it is held by another thread, the second thread will block until the mutex is released.
Overview
Mutexes are fundamental to concurrent programming in Windows. They ensure that critical sections of code, which operate on shared resources, are executed by only one thread at a time, preventing race conditions and data corruption.
Key characteristics of mutexes:
- Ownership: A mutex is owned by the thread that successfully acquires it. Only the owning thread can release the mutex.
- State: A mutex can be in one of two states: signaled (available) or nonsignaled (owned).
- Blocking: Threads attempting to acquire a nonsignaled mutex will enter a waiting state until the mutex becomes signaled.
- Recursion (optional): Some mutex implementations can be recursive, allowing the owning thread to acquire the mutex multiple times without blocking itself.
Creating and Opening Mutexes
Mutexes can be created or opened using the following functions:
CreateMutex Function
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName);
This function creates a new mutex object or opens an existing one if the specified name already exists.
lpMutexAttributes: A pointer to aSECURITY_ATTRIBUTESstructure that specifies the security descriptor for the mutex. If NULL, the mutex gets a default security descriptor.bInitialOwner: If TRUE, the calling thread is granted initial ownership of the mutex. If FALSE, the mutex is created in the signaled state, and no thread owns it.lpName: The name of the mutex object. If NULL, the mutex has no name.
Return Value: If the function succeeds, the return value is a handle to the newly created or opened mutex object. If the function fails, the return value is NULL.
OpenMutex Function
HANDLE OpenMutex(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
This function opens an existing named mutex object.
dwDesiredAccess: The access to the mutex object. This parameter can beMUTEX_ALL_ACCESS.bInheritHandle: If TRUE, then the handle returned will be inherited by the child processes created by the current process.lpName: The name of the mutex object to be opened.
Return Value: If the function succeeds, the return value is a handle to the mutex object. If the function fails, the return value is NULL.
Using Mutexes
Once a mutex handle is obtained, threads can interact with it using several functions:
WaitForSingleObject
This is the primary function for acquiring a mutex. It waits until the specified object is in the signaled state or the time-out interval elapses.
DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
hHandle: A handle to the mutex object.dwMilliseconds: The time-out interval in milliseconds. Can beINFINITE.
Return Value: WAIT_OBJECT_0 if the function returns because the specified object is signaled; WAIT_TIMEOUT if the time-out interval elapses. Other return values indicate an error.
ReleaseMutex
This function releases ownership of a mutex object. The calling thread must own the mutex.
BOOL ReleaseMutex(HANDLE hMutex);
hMutex: A handle to the mutex object.
Return Value: TRUE if the function succeeds, FALSE otherwise.
Example Scenario
Consider a scenario where multiple threads need to update a shared counter. A mutex can protect this operation.
#include <windows.h>
#include <iostream>
HANDLE hMutex;
int sharedCounter = 0;
DWORD WINAPI UpdateCounterThread(LPVOID lpParam) {
WaitForSingleObject(hMutex, INFINITE); // Acquire the mutex
// Critical Section: Only one thread can execute this at a time
sharedCounter++;
std::cout << "Thread " << GetCurrentThreadId() << ": Counter = " << sharedCounter << std::endl;
// End of Critical Section
ReleaseMutex(hMutex); // Release the mutex
return 0;
}
int main() {
hMutex = CreateMutex(NULL, FALSE, NULL); // Create a mutex
if (hMutex == NULL) {
std::cerr << "CreateMutex failed: " << GetLastError() << std::endl;
return 1;
}
HANDLE hThread1 = CreateThread(NULL, 0, UpdateCounterThread, NULL, 0, NULL);
HANDLE hThread2 = CreateThread(NULL, 0, UpdateCounterThread, NULL, 0, NULL);
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
CloseHandle(hThread1);
CloseHandle(hThread2);
CloseHandle(hMutex); // Close the mutex handle
std::cout << "Final Counter Value: " << sharedCounter << std::endl;
return 0;
}
Considerations
- Deadlocks: Be careful about the order in which threads acquire multiple mutexes. If Thread A acquires Mutex1 and waits for Mutex2, while Thread B acquires Mutex2 and waits for Mutex1, a deadlock occurs.
- Performance: While mutexes are essential for synchronization, excessive locking and unlocking can impact performance.
- Alternatives: For simple resource protection within a single process, critical sections (
EnterCriticalSection,LeaveCriticalSection) are often more efficient than mutexes. Mutexes are more suitable for inter-process synchronization.
Note on Mutex vs. Critical Section
Mutexes are kernel objects and can be used for synchronization between processes. Critical sections are lighter-weight synchronization objects that can only be used for synchronization between threads within the same process. Mutexes involve context switching and have more overhead than critical sections.