关键区段总是更快吗?

23

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

它看起来像是一个自动重置事件(尽管名称中暗示了它是一个信号量),当第一次有一个线程等待被其他线程锁定的 Critical Section 时,操作系统会静默地创建这个事件。

现在,我想知道临界区是否总是更快?事件是核对象,每个临界区对象都关联着事件对象,那么相比于互斥体等其他内核对象,CRITICAL_SECTION 如何更快?此外,内部事件对象如何实际影响临界区性能?

下面是 CRITICAL_SECTION 结构:

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

2
同时请记住,CriticalSection 的实现细节可能会因操作系统版本的不同而有所变化。查看这些细节可能是有益的,但不要依赖它们,因为它们可能会改变。 - Adrian McCarthy
这确实是NT5中的信号量。 - paulm
7个回答

39

当他们说一个临界区是"快速的"时,他们的意思是"当它没有被其他线程锁定时,获取它是很便宜的。"

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

之所以它很快,是因为在进入内核之前,它使用等效的InterlockedIncrement来操作其中一个LONG字段(可能是LockCount字段),如果成功了,则认为锁已被获取,而无需进入内核。

我认为InterlockedIncrement API是在用户模式下实现的,作为一个"LOCK INC"指令...换句话说,你可以在根本不需要进行任何内核环的情况下,获得一个未争用的临界区。


1
+1 很好的解释。通过读取 LockCount,您可以有效地测试自己是否可以获取关键部分。 - Arno
1
@Arno:对于这个问题,有TryEnterCriticalSection可以解决。 - rwong

28
在性能工作中,很少有事情属于“总是”的范畴 :)如果您使用其他基元自己实现类似于OS关键部分的内容,则在大多数情况下,这将更慢。
最好的方法是通过性能测量来回答您的问题。操作系统对象的表现非常依赖于具体场景。例如,如果争用较低,则通常认为关键部分是“快速”的。如果锁定时间小于旋转计数时间,则也被认为是快速的。
最重要的是确定关键部分上的争用是否是应用程序中的第一限制因素。如果不是,则只需正常使用关键部分并解决应用程序的主要瓶颈(或瓶颈)即可。
如果关键部分的性能至关重要,则可以考虑以下内容。
  1. 为您的“热”关键部分仔细设置自旋锁计数。如果性能至关重要,则这里的工作是值得的。请记住,虽然自旋锁确实避免了用户模式到内核的转换,但它以疯狂的速度消耗CPU时间 - 在自旋期间,没有其他东西可以使用该CPU时间。如果锁定时间足够长,则旋转线程将实际阻塞,释放该CPU以执行其他工作。
  2. 如果您有读者/写者模式,请考虑使用Slim Reader/Writer (SRW) locks。这里的缺点是它们仅适用于Vista和Windows Server 2008及更高版本的产品。
  3. 您可以尝试将条件变量与关键部分一起使用,以最小化轮询和争用,仅在需要时唤醒线程。同样,这些在Vista和Windows Server 2008及更高版本的产品上受支持。
  4. 考虑使用Interlocked Singly Linked Lists(SLIST)- 这些是高效且“无锁”的。更好的是,它们支持XP和Windows Server 2003及更高版本的产品。
  5. 检查您的代码 - 您可以通过重构一些代码并使用交错操作或SLIST进行同步和通信来分解“热”锁。
总之,调整存在锁竞争的场景可能是具有挑战性(但有趣!)的工作。重点是测量应用程序的性能,并了解热点路径所在。Windows Performance Tool kit中的xperf工具在这里是您的好朋友 :)我们刚刚发布了Microsoft Windows SDK for Windows 7和.NET Framework 3.5 SP1的4.5版本(ISO在此处网络安装程序在此处)。您可以在此处找到有关xperf工具的论坛。 V4.5完全支持Win7,Vista,Windows Server 2008-所有版本。

4

1
LightWeightLock 可能会比较慢,因为它是自旋锁。在优先级反转的情况下,它也可能会死锁。 - Raymond Chen
我倾向于使用Interlocked函数(更具体地说是InterlockedCompareExchange)来保护不调用其他函数的小代码块。在我的情况下,我注意到Interlocked函数的速度是CriticalSection函数的两倍(这对我非常重要,因为这是一个经常执行的小块)。 Interlocked函数的缺点是您无法嵌套它们,并且您可能会意外地误用它们(例如,在一个线程中锁定,在另一个线程中解锁)。 - Patrick

3
临界区将会自旋一小段时间(几毫秒),并且持续检查锁是否被释放。当自旋计数“超时”后,它将退回到内核事件。因此,在锁的持有者快速退出的情况下,您永远不必进行昂贵的内核代码转换。
编辑:去我的代码中找到了一些注释:显然 MS 堆管理器使用的自旋计数为 4000(整数增量,而非毫秒)。

1
4秒不是有点太长了吗?此外,事件对象会在操作系统发现临界区被锁定时立即创建。这有点浪费资源,对吧? - aJ.
哇,计算机术语中的4秒是非常长的时间... 你可能是指4000微秒(即4毫秒)吗?我认为在现代计算机上,上下文切换甚至不需要4毫秒。你能提供4秒数字的引用吗? - rmeador
4
实际上,自旋计数值只是一个计数器 - 它并不是以时间单位表示的值。4000这个值来自于InitializeCriticalSectionAndSpinCount()的MSDN文档。请注意,这个值可能会在不同版本中有所改变。文档中说“大约为4,000”。 - Foredecker
一个循环中的4000个周期只需要几微秒。 - Lothar

1

关键区域比互斥锁更快,因为关键区域不是内核对象。它是当前进程的全局内存的一部分。互斥锁实际上驻留在内核中,创建互斥锁对象需要内核切换,但在关键区域的情况下不需要。尽管关键区域很快,但当线程处于等待状态时,使用关键区域仍会进行内核切换。这是因为线程调度发生在内核端。


1

以下是一种看待它的方式:

如果没有争用,那么自旋锁相对于使用 Mutex 进入内核模式来说速度非常快。

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

因此,这归结为加权平均值,其中权重取决于您的调用模式的具体情况。话虽如此,如果您很少有争用,那么 CriticalSection 就是一个巨大的胜利。另一方面,如果您一直有很多争用,那么与直接使用 Mutex 相比,您将支付一个非常小的惩罚。但在这种情况下,通过切换到 Mutex 获得的收益很小,因此您可能最好尝试减少争用。


将SpinCount设置为0,即使在高度争用的情况下,它也至少不会比互斥锁慢。 - Lothar
哈哈,这和互斥锁是一样的! - Brian Cannard

0

根据我的经验和实验,CRITICAL_SECTION 在与 pthreads 的实现相比非常慢。

极慢意味着在锁定/解锁数量很大时,线程切换速度比 pthread 实现慢10倍左右,在与相同代码的 pthread 实现进行比较。

因此,我不再使用 Critical Section;在 MS Windows 上也可以使用 pthreads,最终性能噩梦结束了。


“the pthreads implementation”的名称是什么,即您所讨论的函数的确切名称是什么? - ChrisW
我指的是已经移植到MS Windows的标准UN*X pthreads库。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接