【问题标题】:Ensure no threads are waiting on a Critical Section before destroying it在销毁临界区之前,确保没有线程在等待临界区
【发布时间】:2012-05-14 11:32:12
【问题描述】:

我在使用关键部分时遇到问题。我的应用程序有大量线程,比如 60 个,它们都需要访问全局资源。因此,我使用关键部分保护该资源。这在运行过程中非常有效,但是当我的应用程序关闭时,我会触发线程退出,然后销毁临界区。

如果其中一些线程在退出时在临界区等待,并因此被阻止退出自身,就会出现问题。

我已经为具有“已初始化”标志的 windows CriticalSection 调用编写了一个包装器,我在创建 crit 时将其设置为 true,并在我即将离开 crit 时设置为 false(这两种情况都是在暴击内设置)。在“进入暴击”包装函数尝试进入暴击之前检查此标志,如果标志为假则绕过请求。当任何线程成功进入暴击时,也会检查该标志,如果它为假,则立即离开暴击。

在删除暴击之前我要做的是将标志设置为 false,然后等待任何等待线程: 允许进入暴击;看到 Initialised 标志为假;然后离开暴击(这对每个线程来说应该是一个非常快速的操作)。

我通过检查 CRITICAL_SECTION 结构中的 LockCount 来检查等待访问 crit 的线程数,并等待它达到 0(在 XP 中,这是 LockCount - (RecursionCount-1);在 2003 及更高版本的服务器中,锁定计数为 @ 987654322@),然后再销毁临界区。

这个应该足够了,但是我发现当仍有一个线程(总是只有一个线程,永远不会更多)等待进入暴击时,LockCount 达到 0,这意味着如果我删除crit 时,另一个线程随后从等待 crit 中醒来,并导致崩溃,因为此时 CRITICAL_SECTION 对象已被销毁。

如果我保留我自己的等待访问线程的内部锁计数,我就有正确的计数;但这并不理想,因为我必须在暴击之外增加这个计数,这意味着该值不受保护,因此在任何时候都不能完全依赖。

有谁知道为什么 CRITICAL_SECTION 结构中的 LockCount 会减少 1?如果我使用自己的锁计数,则检查 CRITICAL_SECTION 的锁计数最后一个线程退出之后(在我销毁暴击之前),它仍然是 0...

或者,除了临界区之外,我有没有更好的方法来保护我的应用程序中的全局资源?

这是我的包装结构:

typedef struct MY_CRIT {
    BOOL Initialised;
    CRITICAL_SECTION Crit;
    int MyLockCount;
}

这是我的 Crit 初始化函数:

BOOL InitCrit( MY_CRIT *pCrit )
{
    if (pCrit)
    {
        InitializeCriticalSection( &pCrit->Crit );          
        pCrit->Initialised = TRUE;
        pCrit->MyLockCount = 0;
        return TRUE;
    }
    // else invalid pointer
    else    
        return FALSE;
}

这是我的输入暴击包装函数:

BOOL EnterCrit( MY_CRIT *pCrit )
{
    // if pointer valid, and the crit is initialised
    if (pCrit && pCrit->Initialised)
    {
        pCrit->MyLockCount++;
        EnterCriticalSection( &pCrit->Crit );
        pCrit->MyLockCount--;

        // if still initialised
        if (pCrit->Initialised)
        {
            return TRUE;
        }
        // else someone's trying to close this crit - jump out now!
        else
        {
            LeaveCriticalSection( &pCrit->Crit );
            return FALSE;
        }
    }
    else // crit pointer is null
        return FALSE;
}

这是我的 FreeCrit 包装函数:

void FreeCrit( MY_CRIT *pCrit )
{
    LONG    WaitingCount = 0;

    if (pCrit && (pCrit->Initialised))
    {
        // set Initialised to FALSE to stop any more threads trying to get in from now on:
        EnterCriticalSection( &pCrit->Crit );
        pCrit->Initialised = FALSE;
        LeaveCriticalSection( &pCrit->Crit );

        // loop until all waiting threads have gained access and finished:
        do {
            EnterCriticalSection( &pCrit->Crit );

            // check if any threads are still waiting to enter:
            // Windows XP and below:
            if (IsWindowsXPOrBelow())
            {
                if ((pCrit->Crit.LockCount > 0) && ((pCrit->Crit.RecursionCount - 1) >= 0))
                    WaitingCount = pCrit->Crit.LockCount - (pCrit->Crit.RecursionCount - 1);
                else
                    WaitingCount = 0;
            }
            // Windows 2003 Server and above:
            else
            {
                WaitingCount = ((-1) - (pCrit->Crit.LockCount)) >> 2;
            }

                        // hack: if our own lock count is higher, use that:
            WaitingCount = max( WaitingCount, pCrit->MyLockCount );

            // if some threads are still waiting, leave the crit and sleep a bit, to give them a chance to enter & exit:
            if (WaitingCount > 0)
            {
                LeaveCriticalSection( &pCrit->Crit );
                // don't hog the processor:
                Sleep( 1 );
            }
            // when no other threads are waiting to enter, we can safely delete the crit (and leave the loop):
            else
            {
                DeleteCriticalSection( &pCrit->Crit );
            }
        } while (WaitingCount > 0);
    }
}

【问题讨论】:

  • 我应该注意: LeaveCrit() 包装函数只是检查指针是否有效,然后离开暴击(没有必要检查Initialised 标志,因为我们无论如何都想离开暴击)。此外,在我的实际实现中,我在 10 秒后跳出的 CritFree 循环中有一个超时,以避免无限循环 - 崩溃总比永远卡住循环好。
  • CRITICAL_SECTION 的内部工作,正如您所提到的,特定于操作系统版本。因此,您不应依赖它,而应将等待线程的数量明确地维护为一个数字。可以使用相同的 CRITICAL_SECTION 保护对该号码的访问。
  • @Vlad:如果我必须等待进入暴击才能增加我正在等待进入它的事实,我该如何保护具有相同暴击的“等待线程”编号? ...我同意我不应该依赖内部运作;目前只是两害相权取其轻。
  • 在任何情况下,CRITICAL_SECTION 不得被锁定很长时间。关键部分的整个想法是标记算法的一个,真正关键部分,它需要对资源的独占访问。所以等待它应该不是问题。但是,如果这是一个问题,只需使用另一个 CRITICAL_SECTION 作为金额(并检查您的代码:它可能以错误的方式使用关键部分)。
  • 好的,如果你注释掉所有的“我触发线程退出,然后销毁临界区”会发生什么。东西,即。根本不做?

标签: c winapi critical-section


【解决方案1】:

您有责任确保在销毁 CS 之前不再使用它。假设当前没有其他线程试图进入,但它有可能很快就会尝试。现在你销毁CS,这个并发线程要做什么?它全速击中已删除的关键部分,导致内存访问冲突?

实际的解决方案取决于您当前的应用程序设计,但如果您正在销毁线程,那么您可能需要标记您的请求以停止这些线程,然后等待它的句柄等待它们的销毁。然后在您确定线程已完成时完成删除关键部分。

请注意,依赖 CS 成员值(例如 .LockCount)是不安全的,如果以正确的方式做事,您可能甚至不需要像 IsWindowsXPOrBelow 这样的东西。关键部分 API 建议您使用 CRITICAL_SECTION 结构作为“黑匣子”,让内部结构特定于实现。

【讨论】:

  • 感谢@Roman R。虽然我确实喜欢将临界区结构视为黑盒,但它缺少我们在代码中需要的功能,主要是用于确定暴击何时被初始化,并确保在删除之前没有使用它。我们经常在单独的代码中实现这些功能,我编写了这个包装器来包含这些功能。因此,等待线程退出将需要在我们使用 crits 的每一位代码中执行,这无处不在。我想要一个适用于所有人的单一解决方案 - 我猜这可能无法实现。
  • 这确实是错误的做法。正确的方法是 Roman R 建议的;通知您的工作线程停止,然后在销毁临界区对象之前等待它们停止。如果线程不在您的控制之下,那么您需要在更合乎逻辑的位置(例如 WinMain/DllMain)初始化/销毁关键部分。众所周知,锁定代码很难正确。最好交给专家。
  • 谢谢卢克。这完全是因为很难正确,我不得不添加这些包装器,因为我们的一些代码非常复杂且相互关联。但我完全知道你在说什么,我同意。认为重新组织线程创建/销毁方法可能是最好的方法。
  • 最好的线程创建/销毁方法,在很大程度上是在应用程序启动时创建它们,并且从不销毁它们。发信号通知工作线程停止,然后等待它们停止,这是一个混乱的练习,通常非常困难,如果可能最好避免(正如您所发现的那样)。遵循“您必须优雅地关闭线程并等待它们”的理念是 SO 和其他博客上成千上万的“如何停止我的线程”和“我的应用程序不会关闭”帖子的原因。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-10-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-01-13
  • 2012-10-28
相关资源
最近更新 更多