EnterCriticalSection死锁

时间:2011-02-18 13:51:39

标签: c++ multithreading deadlock

使用多线程日志记录应用程序看似死锁的情况。

小背景:

我的主应用程序有4-6个线程在运行。主要线程负责监视我正在做的各种事情的健康状况,更新GUI等...然后我有一个传输线程和一个接收线程。发送和接收线程与物理硬件通信。我有时需要调试发送和接收线程正在看到的数据;即,由于数据的时间关键性质而打印到控制台而不会中断它们。顺便说一句,数据是在USB总线上。

由于应用程序的线程性质,我想创建一个调试控制台,我可以从其他线程发送消息。调试消耗作为低优先级线程运行并实现环形缓冲区,这样当您打印到调试控制台时,消息将快速存储到环形缓冲区并设置和事件。调试控制台的线程来自进入的绑定消息中的WaitingOnSingleObject事件。当检测到事件时,控制台线程使用该消息更新GUI显示。简单呃?打印调用和控制台线程使用关键部分来控制访问。

注意:如果我发现我正在丢弃消息,我可以调整环形缓冲区大小(至少是这个想法)。

在测试应用程序中,如果我通过鼠标点击缓慢调用其Print方法,则控制台可以正常工作。我有一个按钮,我可以按下它来发送消息到控制台,它的工作原理。但是,如果我放置任何类型的加载(许多调用Print方法),一切都死锁。当我跟踪死锁时,我的IDE的调试器跟踪到EnterCriticalSection并坐在那里。

注意:如果我删除Lock / UnLock调用并只使用Enter / LeaveCriticalSection(请参阅代码),我有时会工作,但仍然发现自己处于死锁状态。为了排除堆栈推/弹的死锁,我现在直接调用Enter / LeaveCriticalSection,但这并没有解决我的问题....这里发生了什么?

这是一个Print语句,它允许我将一个简单的int传递给显示控制台。

void TGDB::Print(int I)
{
    //Lock();
    EnterCriticalSection(&CS);

    if( !SuppressOutput )
    {
        //swprintf( MsgRec->Msg, L"%d", I);
        sprintf( MsgRec->Msg, "%d", I);
        MBuffer->PutMsg(MsgRec, 1);
    }

    SetEvent( m_hEvent );
    LeaveCriticalSection(&CS);
    //UnLock();
}

// My Lock/UnLock methods
void TGDB::Lock(void)
{
    EnterCriticalSection(&CS);
}

bool TGDB::TryLock(void)
{
    return( TryEnterCriticalSection(&CS) );
}

void TGDB::UnLock(void)
{
        LeaveCriticalSection(&CS);
}

// This is how I implemented Console's thread routines

DWORD WINAPI TGDB::ConsoleThread(PVOID pA)
{
DWORD rVal;

         TGDB *g = (TGDB *)pA;
        return( g->ProcessMessages() );
}

DWORD TGDB::ProcessMessages()
{
DWORD rVal;
bool brVal;
int MsgCnt;

    do
    {
        rVal = WaitForMultipleObjects(1, &m_hEvent, true, iWaitTime);

        switch(rVal)
        {
            case WAIT_OBJECT_0:

                EnterCriticalSection(&CS);
                //Lock();

                if( KeepRunning )
                {
                    Info->Caption = "Rx";
                    Info->Refresh();
                    MsgCnt = MBuffer->GetMsgCount();

                    for(int i=0; i<MsgCnt; i++)
                    {
                        MBuffer->GetMsg( MsgRec, 1);
                        Log->Lines->Add(MsgRec->Msg);
                    }
                }

                brVal = KeepRunning;
                ResetEvent( m_hEvent );
                LeaveCriticalSection(&CS);
                //UnLock();

            break;

            case WAIT_TIMEOUT:
                EnterCriticalSection(&CS);
                //Lock();
                Info->Caption = "Idle";
                Info->Refresh();
                brVal = KeepRunning;
                ResetEvent( m_hEvent );
                LeaveCriticalSection(&CS);
                //UnLock();
            break;

            case WAIT_FAILED:
                EnterCriticalSection(&CS);
                //Lock();
                brVal = false;
                Info->Caption = "ERROR";
                Info->Refresh();
                aLine.sprintf("Console error: [%d]", GetLastError() );
                Log->Lines->Add(aLine);
                aLine = "";
                LeaveCriticalSection(&CS);
                //UnLock();
            break;
        }

    }while( brVal );

    return( rVal );
}

MyTest1和MyTest2只是我响应按下按钮而调用的两个测试功能。无论我点击按钮有多快,MyTest1都不会出现问题。 MyTest2几乎每次都会死锁。

// No Dead Lock
void TTest::MyTest1()
{
    if(gdb)
    {
        // else where: gdb = new TGDB;
        gdb->Print(++I);
    }
}


// Causes a Dead Lock
void TTest::MyTest2()
{
    if(gdb)
    {
        // else where: gdb = new TGDB;
        gdb->Print(++I);
        gdb->Print(++I);
        gdb->Print(++I);
        gdb->Print(++I);
        gdb->Print(++I);
        gdb->Print(++I);
        gdb->Print(++I);
        gdb->Print(++I);
    }
}

更新: 在我的环缓冲区实现中发现了一个错误。在负载很重的情况下,当缓冲区被包装时,我没有正确检测到满缓冲区,因此缓冲区没有返回。我很确定这个问题现在已经解决了。一旦我修复了环形缓冲区问题,性能就会好得多。但是,如果我减少iWaitTime,我的死锁(或冻结问题)将返回。

所以经过进一步测试后负载更重,看来我的死锁没有消失。在超重负载下我继续死锁或至少我的应用程序冻结但没有在它附近使用,因为我修复了环缓冲区问题。如果我将MyTest2中的打印电话数量加倍,我每次都可以轻松锁定....

此外,我的更新代码如上所示。我知道确保我的Set&amp;重置事件调用是在关键部分调用内。

3 个答案:

答案 0 :(得分:4)

关闭这些选项后,我会询问有关此“信息”对象的问题。它是一个窗口,哪个窗口是它的父级,它是在哪个线程上创建的?

如果在另一个线程上创建了Info或其父窗口,则可能发生以下情况:

控制台线程位于关键部分内,处理消息。 主线程调用Print()并在关键部分上阻塞,等待控制台线程释放锁定。 Console线程调用Info(Caption)上的函数,这导致系统向窗口发送消息(WM_SETTEXT)。 SendMessage阻塞,因为目标线程不处于消息可警告状态(在调用GetMessage / WaitMessage / MsgWaitForMultipleObjects时未阻止)。

现在你遇到了僵局。

这种#$(%^只要你将阻塞例程与任何与windows交互的东西混合就会发生。在GUI线程上使用的唯一合适的阻塞函数是MSGWaitForMultipleObjects,否则SendMessage调用托管在线程上的窗口很容易死锁

避免这种情况涉及两种可能的方法:

  • 从不在工作线程中进行任何GUI交互。仅使用PostMessage将非阻塞UI更新命令分派给UI线程,或者
  • 使用内核事件对象+ MSGWaitForMultipleObjects(在GUI线程上)确保即使您在资源上阻塞,您仍然在发送消息。

答案 1 :(得分:2)

在不知道死锁的地方,这段代码很难弄明白。两条评论:

  • 鉴于这是c ++,您应该使用Auto对象来执行锁定和解锁。以防万一因为Log抛出异常而变得非灾难性。

  • 您正在重置事件以响应WAIT_TIMEOUT。这为第二次Print()调用留下了一个小机会,当工作线程从WaitForMultiple返回时,但在它进入临界区之前设置事件。当实际数据未决时,这将导致事件被重置。

但你需要调试它并揭示“死锁”的位置。如果一个线程卡在EnterCriticalSection上,那么我们可以找出原因。如果两个线程都不是,那么不完整的打印只是事件迷失的结果。

答案 2 :(得分:2)

我强烈建议使用无锁实现。

这不仅可以避免潜在的死锁,而且调试工具也是您绝对不想锁定的地方。格式化调试消息对多线程应用程序的时间安排的影响已经够糟了......让锁同步并行代码只是因为你检测它会使调试徒劳无功。

我建议使用基于SList的设计(Win32 API提供SList实现,但您可以使用InterlockedCompareExchange和InterlockedExchange轻松构建线程安全模板)。每个线程都有一个缓冲池。每个缓冲区将跟踪它来自的线程,在处理缓冲区之后,日志管理器会将缓冲区发布回源线程的SList以供重用。希望编写消息的线程将缓冲区发布到记录器线程。这也可以防止任何线程饿死缓冲区的其他线程。将缓冲区放入队列时唤醒记录器线程的事件完成了设计。