Mutex does not work as I expected

2019-06-14 14:36发布

问题:

My Environment: C++ Builder XE4.

I am using Mutex. In the following code, I expect that while Timer1 would acquire mutex, Timer2 process would be skipped. However, Timer2 process was not skipped at all.

What is the problem in the code?

Unit1.cpp

//---------------------------------------------------------------------------

#include <vcl.h>
#pragma hdrstop

#include "Unit1.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
    : TForm(Owner)
{
}
//---------------------------------------------------------------------------

String MutexName = L"Project1";
HANDLE HWNDMutex;

void __fastcall TForm1::FormShow(TObject *Sender)
{
    HWNDMutex = CreateMutex(NULL, false, MutexName.c_str());
    if (HWNDMutex == NULL) {
        String msg = L"failed to create mutex";
        OutputDebugString(msg.c_str());
    }

    Timer1->Enabled = false;
    Timer1->Interval = 1000; // msec
    Timer1->Enabled = true;

    Timer2->Enabled = false;
    Timer2->Interval =  200; // msec
    Timer2->Enabled = true;
}

__fastcall TForm1::~TForm1()
{
    CloseHandle(HWNDMutex);
}

void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
    if (WaitForSingleObject(HWNDMutex, INFINITE) == WAIT_TIMEOUT) {
        return;
    }

    if (CHK_update->Checked) {
        String msg = L"Timer1 " + Now().FormatString(L"yyyy/mm/dd hh:nn:ss.zzz");
        Memo1->Lines->Add(msg);
    }

    for(int loop=0; loop<10; loop++) {
        Application->ProcessMessages();
        Sleep(90); // msec
    }

    ReleaseMutex(HWNDMutex);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Timer2Timer(TObject *Sender)
{
    if (WaitForSingleObject(HWNDMutex, INFINITE) == WAIT_TIMEOUT) {
        return;
    }

    if (CHK_update->Checked) {
        String msg = L">>>Timer2 " + Now().FormatString(L"yyyy/mm/dd hh:nn:ss.zzz");
        Memo1->Lines->Add(msg);
    }

    ReleaseMutex(HWNDMutex);
}
//---------------------------------------------------------------------------

Result

Timer1 2017/11/08 15:20:39.781
>>>Timer2 2017/11/08 15:20:39.786
>>>Timer2 2017/11/08 15:20:40.058
>>>Timer2 2017/11/08 15:20:40.241
>>>Timer2 2017/11/08 15:20:40.423
>>>Timer2 2017/11/08 15:20:40.603
Timer1 2017/11/08 15:20:40.796
>>>Timer2 2017/11/08 15:20:40.799
>>>Timer2 2017/11/08 15:20:41.071
>>>Timer2 2017/11/08 15:20:41.254
>>>Timer2 2017/11/08 15:20:41.436
>>>Timer2 2017/11/08 15:20:41.619
Timer1 2017/11/08 15:20:41.810
>>>Timer2 2017/11/08 15:20:41.811
>>>Timer2 2017/11/08 15:20:42.083
>>>Timer2 2017/11/08 15:20:42.265
>>>Timer2 2017/11/08 15:20:42.448
>>>Timer2 2017/11/08 15:20:42.633

I tried using TMutex with acquire() and release(), but it did not work either.

回答1:

A mutex has a thread affinity and thus is re-entrant:

A mutex object is a synchronization object whose state is set to signaled when it is not owned by any thread, and nonsignaled when it is owned. Only one thread at a time can own a mutex object, whose name comes from the fact that it is useful in coordinating mutually exclusive access to a shared resource. For example, to prevent two threads from writing to shared memory at the same time, each thread waits for ownership of a mutex object before executing the code that accesses the memory. After writing to the shared memory, the thread releases the mutex object.

...

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.

TTimer is a message-based timer. You have two timers running in the same thread. Which means their OnTimer events are serialized by default in relation to each other. Only one event can be running at a time (unless you do something stupid like call Application->ProcessMessages(), which is a re-entrant nightmare).

Timer2 will trigger first (4-5 times, actually), acquiring and releasing the mutex lock each time, before Timer1 triggers. Then Timer1 triggers, acquires the lock, runs a loop to pump the main UI message queue, thus allowing Timer2 to trigger again (multiple times) while Timer1Timer() is still running. Timer2 will re-acquire and release the same lock that the UI thread already has, so WaitForSingleObject() exits with WAIT_OBJECT_0 immediately. Then the loop ends and Timer1 releases the lock.

Your mutex is useless in this code. A mutex is meant for inter-thread synchronization, but you have no worker threads in this code! You have a single thread synchronizing against itself, which is redundant, and exactly the kind of deadlock-causing situation that many synchronization objects avoid by supporting re-entry.

A critical section also has a thread affinity and is re-entrant, so that is not going to help you, either:

A critical section object provides synchronization similar to that provided by a mutex object, except that a critical section can be used only by the threads of a single process.

...

When a thread owns a critical section, it can make additional calls to EnterCriticalSection or TryEnterCriticalSection without blocking its execution. This prevents a thread from deadlocking itself while waiting for a critical section that it already owns. To release its ownership, the thread must call LeaveCriticalSection one time for each time that it entered the critical section. There is no guarantee about the order in which waiting threads will acquire ownership of the critical section.

However, a semaphore would work for what you are attempting, as it does not have a thread affinity:

A semaphore object is a synchronization object that maintains a count between zero and a specified maximum value. The count is decremented each time a thread completes a wait for the semaphore object and incremented each time a thread releases the semaphore. When the count reaches zero, no more threads can successfully wait for the semaphore object state to become signaled. The state of a semaphore is set to signaled when its count is greater than zero, and nonsignaled when its count is zero.

The semaphore object is useful in controlling a shared resource that can support a limited number of users. It acts as a gate that limits the number of threads sharing the resource to a specified maximum number. For example, an application might place a limit on the number of windows that it creates. It uses a semaphore with a maximum count equal to the window limit, decrementing the count whenever a window is created and incrementing it whenever a window is closed. The application specifies the semaphore object in call to one of the wait functions before each window is created. When the count is zero—indicating that the window limit has been reached—the wait function blocks execution of the window-creation code.

...

A thread that owns a mutex object can wait repeatedly for the same mutex object to become signaled without its execution becoming blocked. A thread that waits repeatedly for the same semaphore object, however, decrements the semaphore's count each time a wait operation is completed; the thread is blocked when the count gets to zero. Similarly, only the thread that owns a mutex can successfully call the ReleaseMutex function, though any thread can use ReleaseSemaphore to increase the count of a semaphore object.

If you switch to a semaphore, your code as shown would deadlock itself as soon as Application->ProcessMessages() is called and the semaphore counter drops to 0, because of your use of INFINITE timeouts. So use smaller timeouts to prevent that.

Try this:

//---------------------------------------------------------------------------

#include <vcl.h>
#pragma hdrstop

#include "Unit1.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
    : TForm(Owner)
{
}
//---------------------------------------------------------------------------

HANDLE hSemaphore;

void __fastcall TForm1::FormShow(TObject *Sender)
{
    hSemaphore = CreateSemaphore(NULL, 1, 1, NULL);
    if (hSemaphore == NULL) {
        OutputDebugString(L"failed to create semaphore");
    }

    Timer1->Enabled = false;
    Timer1->Interval = 1000; // msec
    Timer1->Enabled = true;

    Timer2->Enabled = false;
    Timer2->Interval =  200; // msec
    Timer2->Enabled = true;
}

__fastcall TForm1::~TForm1()
{
    if (hSemaphore)
        CloseHandle(hSemaphore);
}

void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
    if (WaitForSingleObject(hSemaphore, 0) != WAIT_OBJECT_0) {
        return;
    }

    if (CHK_update->Checked) {
        String msg = L"Timer1 " + Now().FormatString(L"yyyy/mm/dd hh:nn:ss.zzz");
        Memo1->Lines->Add(msg);
    }

    for(int loop=0; loop<10; loop++) {
        Application->ProcessMessages();
        Sleep(90); // msec
    }

    ReleaseSemaphore(hSemaphore, 1, NULL);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Timer2Timer(TObject *Sender)
{
    if (WaitForSingleObject(hSemaphore, 0) != WAIT_OBJECT_0) {
        return;
    }

    if (CHK_update->Checked) {
        String msg = L">>>Timer2 " + Now().FormatString(L"yyyy/mm/dd hh:nn:ss.zzz");
        Memo1->Lines->Add(msg);
    }

    ReleaseSemaphore(hSemaphore, 1, NULL);
}
//---------------------------------------------------------------------------

On a side note: beware of giving a kernel-based synchronization object a name. That allows other processes to access it and mess around with its state behind your back. Don't name objects that you don't intend to share across process boundaries! Mutexes and semaphores are namable objects.