关键部分总是更快吗?

时间:2009-05-12 15:16:05

标签: c++ winapi synchronization critical-section

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

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

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

以下是CRITICAL_SECTION

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

7 个答案:

答案 0 :(得分:36)

当他们说某个关键部分是“快”时,他们的意思是“当它还没有被另一个线程锁定时获得一个便宜”。

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

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

我认为InterlockedIncrement API在用户模式下实现为“LOCK INC”操作码......换句话说,你可以获得一个无争议的关键部分而根本不进行任何环转换。

答案 1 :(得分:26)

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

回答问题的最佳方法是进行绩效衡量。 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进行同步和通信来分解“热”锁。
  6. 总而言之 - 具有锁争用的调优方案可能具有挑战性(但有趣!)工作。专注于测量应用程序性能并了解热路径的位置。 Windows Performance Tool kit中的xperf工具是您的朋友:)我们刚刚在Microsoft Windows SDK for Windows 7和.NET Framework 3.5 SP1(ISO is hereweb installer here)中发布了4.5版。您可以找到xperf工具here的论坛。 V4.5完全支持Win7,Vista,Windows Server 2008 - 所有版本。

答案 2 :(得分:4)

CriticalSections速度更快,但InterlockedIncrement / InterlockedDecrement更多。请参阅此实施用法示例LightweightLock full copy

答案 3 :(得分:3)

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

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

答案 4 :(得分:1)

这是一种看待它的方法:

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

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

因此,归结为加权平均值,其中权重取决于您的呼叫模式的具体情况。话虽这么说,如果你没有争论,那么CriticalSection就是大胜利。另一方面,如果你一直有很多争论,那么你将直接使用Mutex支付非常小的罚款。但在这种情况下,通过切换到Mutex获得的收益很小,所以你最好不要试图减少争用。

答案 5 :(得分:1)

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

答案 6 :(得分:0)

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

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

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