23

我正在调试一个多线程应用程序,发现CRITICAL_SECTION. 我发现LockSemaphoreCRITICAL_SECTION 的数据成员很有趣。

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

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

这是 的结构CRITICAL_SECTION

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

7 回答 7

38

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

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

它快速的原因是,在进入内核之前,它InterlockedIncrement在其中一个LONG字段(可能在LockCount字段上)上使用等效项,如果它成功,那么它认为在没有进入内核的情况下获得了锁。

我认为该InterlockedIncrementAPI 在用户模式下作为“LOCK INC”操作码实现......换句话说,您可以获得一个无争议的关键部分,而无需对内核进行任何环转换。

于 2009-05-12T16:12:04.983 回答
28

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

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

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

如果临界区性能很关键,那么您可以考虑以下事项。

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

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

于 2009-05-12T15:53:37.947 回答
4

CriticalSections 更快,但InterlockedIncrement / InterlockedDecrement更多。请参阅此实现使用示例LightweightLock 完整副本

于 2009-05-12T16:38:56.967 回答
3

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

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

于 2009-05-12T15:28:52.493 回答
1

这是一种看待它的方法:

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

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

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

于 2009-05-13T22:18:33.877 回答
1

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

于 2015-01-06T09:20:48.630 回答
0

根据我的经验和实验,与实施CRITICAL_SECTION相比,速度非常慢pthreads

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

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

于 2021-02-23T10:17:20.753 回答