【问题标题】:EnterCriticalSection DeadlockEnterCriticalSection 死锁
【发布时间】:2011-02-18 13:51:39
【问题描述】:

多线程日志记录应用程序出现死锁情况。

小背景:

我的主应用程序有 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 中的 Print 调用次数增加一倍,我每次都可以轻松锁定......

另外,我更新的代码也反映在上面。我知道确保我的 Set & Reset 事件调用在临界区调用中。

【问题讨论】:

  • Offtopic:我想知道为什么代码没有突出显示?它在编辑视图中确实如此......无论如何,有时它确实如此。 =/
  • 我的亮点...也许可以尝试清除浏览器缓存并重新启动?
  • 你可以在调试器中运行它,对吧? “死锁”时线程在哪里?
  • 是的,我可以在调试器中运行它,也可以在我的主代码库中运行它,只要我没有遇到任何类型的负载;即在 for 循环内或连续调用 TDGB::Print 方法。请看我的两个测试函数(MyTest1 & MyTest2)。在调试器中,我到达 EnterCriticalSection 并挂起。

标签: c++ multithreading deadlock


【解决方案1】:

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

如果 Info 或其父窗口是在其他线程上创建的,则可能会出现以下情况:

控制台线程位于临界区中,处理消息。 主线程调用 Print() 并在等待控制台线程释放锁的关键部分上阻塞。 控制台线程在 Info (Caption) 上调用一个函数,这导致系统向窗口发送一条消息 (WM_SETTEXT)。 SendMessage 被阻塞,因为目标线程未处于消息警报状态(在调用 GetMessage/WaitMessage/MsgWaitForMultipleObjects 时未被阻塞)。

现在你遇到了死锁。

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

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

  • 永远不要在工作线程中进行任何 GUI 交互。仅使用 PostMessage 将非阻塞 UI 更新命令分派到 UI 线程,或者
  • 使用内核事件对象 + MSGWaitForMultipleObjects(在 GUI 线程上)以确保即使您阻塞了某个资源,您仍在调度消息。

【讨论】:

  • 你知道,这可能非常正确!我在多线程方面做了很多,我在其他地方没有问题;只有在这里我调用 Windows UI。顺便说一下,UI 是 Embarcadero VCL 组件。我怀疑核心线程已按照您的建议阻止了 SendMessage。去测试这个...
  • 是的,就是这样。我只是禁用 GUI 调用并转储我的环形缓冲区,或者如果我只是注释掉案例 WAIT_OBJECT_0: 中的所有 GUI 调用并在超时时转储缓冲区,现在一切正常。谢谢你挂在那里帮助突出这一点,克里斯!我对 Ben Voigt 的 cmets 很感兴趣,我想我会调查。我找到了一个提供参考实现的代码项目演示。临时我会看看 MSGWaitForMultipleObjects - 对我来说是一个新的 API 调用。
【解决方案2】:

如果不知道它在哪里死锁,很难弄清楚这段代码。两个cmets:

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

  • 您正在重置事件以响应 WAIT_TIMEOUT。这为在工作线程从 WaitForMultiple 返回但进入临界区之前的第二次 Print() 调用设置事件留下了一个小窗口。这将导致在实际有待处理数据时重置事件。

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

【讨论】:

  • 此信息帮助了我与核心问题无关。我发现根本原因在于我的环形缓冲区。环形缓冲区有一个错误并且没有返回,因此死锁并不是真正意义上的死锁。我的环形缓冲区根本没有返回。一旦我修复了那个错误,一切都开始工作了。我将此标记为正确答案,因为它指出了 WAIT_TIMEOUT 案例中的潜在问题。谢谢!
  • 只是给任何感兴趣的人的注释。我的环形缓冲区获取数据的速度如此之快,以至于缓冲区已经包裹并且我的 R/W 指针(读指针和写指针跟踪我在环中的位置)的边缘情况变得相等,并且在新写入时环形缓冲区代码当 R/W 指针相等时,看起来又像一个完整的缓冲区。同样,写入看起来像一个空缓冲区。因此,在这种边缘情况下,读满或写空导致缓冲区死锁。一旦纠正,我的“僵局”就消失了。
  • 啊,废话......经过进一步测试,我的僵局似乎没有消失。在超重负载下,我继续死锁,或者至少我的应用程序冻结了,但由于我修复了环形缓冲区问题,所以没有使用它。
【解决方案3】:

我强烈推荐无锁实现。

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

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

【讨论】:

  • 有趣!!!在我的辩护中,这个调试控制台通常不是活动的。它需要一个特殊的命令行开关来打开它,然后,只有某些事情会通过一个隐藏的窗口被记录下来,该窗口用于选择要记录的内容。它是一个日志系统,仅当我需要从临时代码打印、从隐藏的 UI 选择启用代码等时才调用它。但是对您在其他领域的想法非常感兴趣,我确实使用了关键部分。您可能有好的资源可以推荐阅读吗?
  • 在其他领域使用关键部分是可以的,如果你理解的话。但是调试代码中的关键部分(或任何类型的锁,包括埋在分配器或 I/O 层中的锁)是一场灾难(海森堡不确定性原理的软件版本)。至于要了解更多信息的资源,我建议您查看comp.programming.threads archives。在微软杀死 MS 公共新闻组之前,我从一些相同的专家那里学到了很多东西。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-12-27
  • 2014-06-08
  • 2013-11-30
  • 2013-12-25
  • 2015-01-29
  • 2017-03-02
相关资源
最近更新 更多