c++windowswinapi

Issues with Mutex Behavior in Multiple Instances


I encountered an issue while trying to run two instances of a program that communicate synchronously using a mutex. When I create the mutex using CreateMutexW with the bInitialOwner parameter set to true (which means the program initially owns the mutex), a problem arises. The second instance (the one that runs later) is unable to acquire the mutex after the first instance releases it; it keeps waiting for the mutex to be released. Meanwhile, the first instance is stuck in a loop where it continuously writes and reads data while still holding the mutex.

Importantly, this situation does not lead to a deadlock; rather, the second instance cannot acquire the mutex because the first instance keeps releasing and reacquiring it in its loop. If I set bInitialOwner to false, this problem does not occur.

I would like to understand what happens when a program attempts to wait on WaitForSingleObject(Mutex, INFINITE); while it is still the owner of the mutex.

Here’s the relevant part of my code:

#include <iostream>
#include <windows.h>

int main() {
    // Create file mapping
    HANDLE FileHandle = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, 255, L"MY");
    if (FileHandle == NULL) {
        std::cerr << "Unable to create file mapping: " << GetLastError() << std::endl;
        return 1;
    }

    // Create mutex
    HANDLE Mutex = CreateMutexW(nullptr, true, L"share");
    if (Mutex == NULL) {
        std::cerr << "Unable to create mutex: " << GetLastError() << std::endl;
        CloseHandle(FileHandle);
        return 1;
    }

    // Create semaphore
    HANDLE Semaphore = CreateSemaphoreW(nullptr, 0, 1, L"NUM");
    if (Semaphore == NULL) {
        std::cerr << "Unable to create semaphore: " << GetLastError() << std::endl;
        CloseHandle(Mutex);
        CloseHandle(FileHandle);
        return 1;
    }

    // Map view
    char *charw = (char *)MapViewOfFile(FileHandle, FILE_MAP_ALL_ACCESS, 0, 0, 255);
    if (charw == nullptr) {
        std::cerr << "Unable to map view: " << GetLastError() << std::endl;
        CloseHandle(Semaphore);
        CloseHandle(Mutex);
        CloseHandle(FileHandle);
        return 1;
    }

    // Check if the memory region is already open
    BOOL flag = (GetLastError() == ERROR_ALREADY_EXISTS);

    if (flag) {
        std::cout << "Opened memory region, waiting for messages\n";
        
        while (flag) {
            WaitForSingleObject(Mutex, INFINITE);
            std::cout << "Content in memory: " << charw << std::endl;
            ReleaseMutex(Mutex);

            WaitForSingleObject(Mutex, INFINITE);
            std::cout << TEXT("Input data into shared memory: "); 
            std::cin.getline(charw, 255);
            ReleaseMutex(Mutex);
        }
    } else {
        while (1) {
            WaitForSingleObject(Mutex, INFINITE); 
            std::cout << TEXT("Input data into shared memory: "); 
            std::cin.getline(charw, 255);
            ReleaseMutex(Mutex);

            WaitForSingleObject(Mutex, INFINITE);
            std::cout << "Content in memory: " << charw << std::endl;
            ReleaseMutex(Mutex);
        }
    }

    UnmapViewOfFile(charw);
    CloseHandle(Semaphore);
    CloseHandle(Mutex);
    CloseHandle(FileHandle);
    return 0;
}

Any insights or suggestions?


Solution

  • When you use bInitialOwner=true, the 1st process that actually creates the mutex will obtain immediate ownership of the mutex from CreateMutexW() itself, but you are never releasing that ownership until the creating process terminates. The 2nd process never gets the chance to obtain ownership of the mutex. The 1st process increases its ownership when using WaitForSingleObject() and decreases it using ReleaseMutex(), but the initial ownership is still being held.

    This is explained in Microsoft's documentation:

    Mutex Objects

    After a thread obtains ownership of a mutex, it can specify the same mutex in repeated calls to the wait-functions without blocking its execution. This prevents a thread from deadlocking itself while waiting for a mutex that it already owns. To release its ownership under such circumstances, the thread must call ReleaseMutex once for each time that the mutex satisfied the conditions of a wait function.

    ReleaseMutex function

    A thread obtains ownership of a mutex either by creating it with the bInitialOwner parameter set to TRUE or by specifying its handle in a call to one of the wait functions. When the thread no longer needs to own the mutex object, it calls the ReleaseMutex function so that another thread can acquire ownership.

    A thread can specify a mutex that it already owns in a call to one of the wait functions without blocking its execution. This prevents a thread from deadlocking itself while waiting for a mutex that it already owns. However, to release its ownership, the thread must call ReleaseMutex one time for each time that it obtained ownership (either through CreateMutex or a wait function).

    You are not calling ReleaseMutex() to release the initial ownership that was granted by CreateMutexW(). You are only releasing the ownership granted by WaitForSingleObject(). So, the initial owner is not fully releasing its ownership, that is why the 2nd process is unable to obtain any ownership at all.

    The simplest solution is to use bInitialOwner=false instead, so both processes are required to use WaitForSingleObject() only to obtain ownership. Microsoft's documentation even says so:

    CreateMutexW function

    Two or more processes can call CreateMutex to create the same named mutex. The first process actually creates the mutex, and subsequent processes with sufficient access rights simply open a handle to the existing mutex. This enables multiple processes to get handles of the same mutex, while relieving the user of the responsibility of ensuring that the creating process is started first. When using this technique, you should set the bInitialOwner flag to FALSE; otherwise, it can be difficult to be certain which process has initial ownership.

    If you really want to use bInitialOwner=true in this situation, you have to check the return value of GetLastError() immediately after CreateMutexW() returns, and skip the 1st call to WaitForSingleObject() if ownership has already been obtained, eg:

    #include <iostream>
    #include <windows.h>
    
    int main() {
        // Create file mapping
        HANDLE FileHandle = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, 255, L"MY");
        if (FileHandle == nullptr) {
            std::cerr << "Unable to create file mapping: " << GetLastError() << std::endl;
            return 1;
        }
    
        // Check if the memory region is already open
        BOOL flag = (GetLastError() == ERROR_ALREADY_EXISTS);
    
        // Map view
        char *charw = (char *)MapViewOfFile(FileHandle, FILE_MAP_ALL_ACCESS, 0, 0, 255);
        if (charw == nullptr) {
            std::cerr << "Unable to map view: " << GetLastError() << std::endl;
            return 1;
        }
    
        // Create mutex
        HANDLE Mutex = CreateMutexW(nullptr, true, L"share");
        if (Mutex == nullptr) {
            std::cerr << "Unable to create mutex: " << GetLastError() << std::endl;
            return 1;
        }
    
        // Check if the mutex is already owned
        bool mutexIsOwned = (GetLastError() != ERROR_ALREADY_EXISTS);
    
        char buffer[256];
    
        if (flag) {
            std::cout << "Opened memory region, waiting for messages\n";
            
            while (flag) {
                if (!mutexIsOwned) {
                    if (WaitForSingleObject(Mutex, INFINITE) != WAIT_OBJECT_0) {
                        std::cerr << "Unable to lock mutex: " << GetLastError() << std::endl;
                        return 1;
                    }
                }
                std::cout << "Content in memory: " << charw << std::endl;
                ReleaseMutex(Mutex);
    
                std::cout << "Input data into shared memory: "; 
                std::cin.getline(buffer, 255);
    
                if (WaitForSingleObject(Mutex, INFINITE) != WAIT_OBJECT_0)  {
                    std::cerr << "Unable to lock mutex: " << GetLastError() << std::endl;
                    return 1;
                }
    
                CopyMemory(charw, buffer, 255);
                ReleaseMutex(Mutex);
    
                mutexIsOwned = false;
            }
        } else {
            while (1) {
                std::cout << "Input data into shared memory: "; 
                std::cin.getline(buffer, 255);
    
                if (!mutexIsOwned) {
                    if (WaitForSingleObject(Mutex, INFINITE) != WAIT_OBJECT_0) {
                        std::cerr << "Unable to lock mutex: " << GetLastError() << std::endl;
                        return 1;
                    }
                }
     
                CopyMemory(charw, buffer, 255);
                ReleaseMutex(Mutex);
    
                if (WaitForSingleObject(Mutex, INFINITE) != WAIT_OBJECT_0) {
                    std::cerr << "Unable to lock mutex: " << GetLastError() << std::endl;
                    return 1;
                }
                std::cout << "Content in memory: " << charw << std::endl;
                ReleaseMutex(Mutex);
    
                mutexIsOwned = false;
            }
        }
    
        return 0;
    }