【问题标题】:Is Critical Section always faster?关键部分总是更快吗?
【发布时间】:2010-10-25 14:18:39
【问题描述】:

我在调试一个多线程的应用程序,发现CRITICAL_SECTION的内部结构。我发现 CRITICAL_SECTION 的数据成员 LockSemaphore 很有趣。

看起来LockSemaphore 是一个自动重置事件(不是顾名思义的信号量),当线程第一次等待被其他线程锁定的Critcal Section 时,操作系统会静默创建此事件。

现在,我想知道关键部分总是更快吗?事件是一个内核对象,每个关键部分对象都与事件对象相关联,那么与其他内核对象(如 Mutex)相比,Critical Section 如何更快?此外,内部事件对象实际上如何影响关键部分的性能?

这是CRITICAL_SECTION的结构:

struct RTL_CRITICAL_SECTION
{
    PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
    LONG LockCount;
    LONG RecursionCount;
    HANDLE OwningThread;
    HANDLE LockSemaphore;
    ULONG_PTR SpinCount;
};

【问题讨论】:

  • 还请记住,CriticalSection 实施细节可能因操作系统版本而异。查看这些细节可能很有启发性,但不要依赖它们,因为它们可能会发生变化。
  • 它肯定是 NT5 中的信号量

标签: c++ winapi synchronization critical-section


【解决方案1】:

当他们说临界区“快”时,他们的意思是“如果它还没有被另一个线程锁定,那么获取一个临界区很便宜”。

[请注意,如果它 已经被另一个线程锁定,那么它的速度有多快并不重要。]

之所以这么快,是因为在进入内核之前,它在其中一个LONG 字段(可能在LockCount 字段)上使用了等效的InterlockedIncrement,如果它成功了,那么它会考虑没有进入内核就获得了锁。

InterlockedIncrement API 我认为是在用户模式下作为“LOCK INC”操作码实现的……换句话说,您可以在完全不执行任何环转换到内核的情况下获得一个无争议的临界区。

【讨论】:

  • +1 很好的解释。如果你能通过阅读 LockCount 获得关键部分,你可以有效地测试自己。
  • @Arno:有TryEnterCriticalSection
【解决方案2】:

在性能工作中,很少有东西属于“始终”类别 :) 如果您自己使用其他原语实现类似于 OS 关键部分的东西,那么在大多数情况下可能会更慢。

回答您的问题的最佳方法是使用性能测量。操作系统对象的执行方式非常取决于场景。例如,如果争用率低,关键部分通常被认为是“快速”的。如果锁定时间小于自旋计数时间,它们也被认为是快速的。

要确定的最重要的事情是关键部分的争用是否是应用程序中的第一级限制因素。如果没有,那么只需正常使用关键部分并处理应用程序的主要瓶颈(或瓶颈)。

如果关键部分的性能很关键,那么您可以考虑以下方面。

  1. 仔细设置“热”临界区的自旋锁计数。如果性能是最重要的,那么这里的工作是值得的。请记住,虽然自旋锁确实避免了用户模式到内核的转换,但它以惊人的速度消耗 CPU 时间——在自旋时,没有其他任何东西可以使用该 CPU 时间。如果一个锁被持有足够长的时间,那么旋转线程将实际阻塞,释放那个 CPU 来做其他工作。
  2. 如果您有读写器模式,请考虑使用Slim Reader/Writer (SRW) locks。这里的缺点是它们仅适用于 Vista 和 Windows Server 2008 及更高版本的产品。
  3. 您可以将condition variables 与您的临界区一起使用,以最大限度地减少轮询和争用,仅在需要时唤醒线程。同样,这些在 Vista 和 Windows Server 2008 及更高版本的产品上受支持。
  4. 考虑使用Interlocked Singly Linked Lists (SLIST) - 这些是高效且“无锁”的。更好的是,它们在 XP 和 Windows Server 2003 及更高版本的产品上得到支持。
  5. 检查您的代码 - 您可以通过重构某些代码并使用互锁操作或 SLIST 进行同步和通信来打破“热”锁。

总而言之 - 具有锁争用的调优方案可能具有挑战性(但很有趣!)。专注于测量您的应用程序性能并了解您的热门路径在哪里。 Windows Performance Tool kit 中的 xperf 工具是您的朋友 :) 我们刚刚发布了适用于 Windows 7 和 .NET Framework 3.5 SP1 的 Microsoft Windows SDK 4.5 版(ISO is hereweb installer here)。您可以找到 xperf 工具的论坛here。 V4.5 完全支持 Win7、Vista、Windows Server 2008 - 所有版本。

【讨论】:

    【解决方案3】:

    CriticalSections 更快,但InterlockedIncrement/InterlockedDecrement 更多。请参阅此实现使用示例LightweightLock full copy

    【讨论】:

    • LightWeightLock 可能会慢一些,因为它是自旋锁。在优先级倒置的情况下也会死锁。
    • 我倾向于使用互锁函数(更具体地说是 InterlockedCompareExchange)来保护不调用其他函数的小代码块。在我的例子中,我注意到 Interlocked-functions 的速度是 CriticalSection 函数的两倍(这在我的例子中非常重要,因为它是一个经常执行的小块)。 Interlocked-functions 的缺点是你不能嵌套它们,而且你可能会不小心误用它们(例如,锁定一个线程,解锁另一个线程)。
    【解决方案4】:

    CriticalSection 将旋转一会儿(几毫秒)并继续检查锁是否空闲。在自旋计数“超时”后,它将回退到内核事件。因此,在锁的持有者迅速离开的情况下,您永远不必进行昂贵的内核代码转换。

    编辑:在我的代码中发现了一些 cmets:显然 MS 堆管理器使用 4000 的自旋计数(整数增量,而不是 ms)

    【讨论】:

    • 4秒是不是有点太多了?此外,当操作系统发现关键部分被锁定时,事件对象就会被创建。这有点开销,对吧?
    • 哇,4 秒在计算机方面是巨大的……你的意思是 4000 微秒(即 4 毫秒)吗?我认为在现代机器上上下文切换甚至不需要 4ms。你能提供 4sec 数字的引用吗?
    • 实际上,旋转计数值只是一个计数 - 它不是以时间单位表示的值。 4,000 值来自 InitializeCriticalSectionAndSpinCount() 的 MSDN 文档 请注意,这可能会因版本而异。文档说“大约 4,000 个”。
    • 一个循环中的 4000 个周期只是几微秒。
    【解决方案5】:

    这是一种看待它的方式:

    如果没有争用,那么与进入互斥锁的内核模式相比,自旋锁真的很快。

    当发生争用时,CriticalSection 比直接使用 Mutex 稍微贵一些(因为检测自旋锁状态需要额外的工作)。

    所以它归结为加权平均值,其中权重取决于您调用模式的具体情况。话虽如此,如果您几乎没有争论,那么关键部分就是大胜利。另一方面,如果您一直有很多争用,那么您将比直接使用 Mutex 付出非常小的代价。但在这种情况下,切换到 Mutex 所获得的收益很小,因此尝试减少争用可能会更好。

    【讨论】:

    • 将 SpinCount 设置为 0,即使在激烈争用的情况下,它至少不会比互斥锁慢。
    • 哈哈,和互斥锁一样!
    【解决方案6】:

    临界区比互斥锁快,因为临界区不是内核对象。这是当前进程的全局内存的一部分。互斥锁实际上驻留在内核中,互斥对象的创建需要内核切换,但在临界区的情况下不需要。即使临界区很快,当线程进入等待状态时,在使用临界区时也会有内核切换。这是因为线程调度发生在内核端。

    【讨论】:

      【解决方案7】:

      根据我的经验和实验,CRITICAL_SECTIONpthreads 实现相比非常慢。

      当将相同的代码与 pthread 实现进行比较时,当锁定/解锁的数量很大时,非常意味着切换线程要慢 10 倍

      因此我再也不会使用临界区了; pthreads 也可以在 MS Windows 上使用,性能噩梦终于结束了。

      【讨论】:

        猜你喜欢
        • 2016-06-28
        • 2017-09-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-01-15
        • 1970-01-01
        相关资源
        最近更新 更多