互斥锁和临界区有什么区别?


134

请从Linux,Windows角度进行解释?

我正在用C#编程,这两个术语会有所不同。请尽可能多地张贴带有示例等的内容。

谢谢

Answers:


232

对于Windows,关键部分的重量比互斥对象轻。

互斥可以在进程之间共享,但是总是导致对内核的系统调用,这会产生一些开销。

关键部分只能在一个进程中使用,但具有的优势是,它们仅在争用的情况下才切换到内核模式-非竞争性获取(通常是这种情况)非常快。在争用的情况下,它们进入内核以等待某些同步原语(例如事件或信号量)。

我编写了一个快速的示例应用程序,比较了两者之间的时间。在我的系统中,要进行1,000,000次无竞争的获取和释放,互斥体将占用一秒钟的时间。关键部分大约需要50毫秒才能完成1,000,000次采集。

这是测试代码,如果互斥是第一个或第二个,我会运行此代码并获得类似的结果,因此我们看不到任何其他效果。

HANDLE mutex = CreateMutex(NULL, FALSE, NULL);
CRITICAL_SECTION critSec;
InitializeCriticalSection(&critSec);

LARGE_INTEGER freq;
QueryPerformanceFrequency(&freq);
LARGE_INTEGER start, end;

// Force code into memory, so we don't see any effects of paging.
EnterCriticalSection(&critSec);
LeaveCriticalSection(&critSec);
QueryPerformanceCounter(&start);
for (int i = 0; i < 1000000; i++)
{
    EnterCriticalSection(&critSec);
    LeaveCriticalSection(&critSec);
}

QueryPerformanceCounter(&end);

int totalTimeCS = (int)((end.QuadPart - start.QuadPart) * 1000 / freq.QuadPart);

// Force code into memory, so we don't see any effects of paging.
WaitForSingleObject(mutex, INFINITE);
ReleaseMutex(mutex);

QueryPerformanceCounter(&start);
for (int i = 0; i < 1000000; i++)
{
    WaitForSingleObject(mutex, INFINITE);
    ReleaseMutex(mutex);
}

QueryPerformanceCounter(&end);

int totalTime = (int)((end.QuadPart - start.QuadPart) * 1000 / freq.QuadPart);

printf("Mutex: %d CritSec: %d\n", totalTime, totalTimeCS);

1
不知道这是否相关(因为我还没有编译并尝试您的代码),但是我发现使用INFINITE调用WaitForSingleObject会导致性能下降。传递给它一个超时值1然后在检查返回值时循环,这对我的某些代码的性能产生了巨大的影响。这主要是在等待外部进程句柄的情况下,但是...不是互斥体。YMMV。我很想知道互斥量在修改后如何执行。此测试产生的时间差似乎比预期的要大。
Troy Howard,2009年

5
@TroyHoward您基本上不是在那一点上旋转锁定吗?
dss539

这种区别的原因可能主要是历史原因。在无竞争的情况下(很少有原子指令,没有系统调用)实现与CriticalSection一样快的锁定,但跨进程(使用一块共享内存)工作并不困难。参见例如Linux futexes
regnarg

2
@TroyHoward尝试强制您的CPU始终以100%运行,并查看INFINITE是否工作得更好。在决定降低速度后,电源策略可能需要长达40毫秒的时间才能在我的计算机(Dell XPS-8700)上恢复为全速,如果您睡眠或仅等待一毫秒,则该策略可能无法执行。
史蒂文斯·米勒

我不确定我是否理解这里展示的内容。通常,进入关键部分需要获取某种信号量。您是说在后台,操作系统具有有效的方法来实现此关键部分的行为而无需互斥吗?
SN

89

从理论上讲,关键部分是一段代码,它不能一次由多个线程运行,因为该代码访问共享资源。

互斥是用来保护临界区的算法(和有时的数据结构的名称)。

信号量监视器是互斥锁的常见实现。

实际上,Windows中有许多互斥量实现。由于其实现的不同,它们的主要不同之处在于它们的锁定级别,范围,成本以及在不同争用级别下的性能。有关不同互斥体实现成本的图表,请参阅“ CLR由内而外-使用并发性实现可伸缩性 ”。

可用的同步原语。

lock(object)语句是使用Monitor- 实现的-请参见MSDN以供参考。

近年来,对无阻塞同步进行了大量研究。目标是以无锁或无等待的方式实现算法。在这种算法中,一个流程可以帮助其他流程完成其工作,以便该流程最终可以完成其工作。因此,即使试图执行某些工作的其他进程挂起,一个进程也可以完成其工作。使用Usinig锁,它们不会释放锁并阻止其他进程继续。


看到被接受的答案,我想也许是我记得关键部分的概念错了,直到我看到你写的《理论观点》。:)
Anirudh Ramanathan 2012年

2
实用的无锁编程就像香格里拉一样,只不过它存在。Keir Fraser的论文(PDF)对此进行了有趣的探讨(可追溯至2004年)。而且我们在2012年仍在为此奋斗。我们糟透了。
Tim Post

22

除了其他答案,以下详细信息特定于Windows上的关键部分:

  • 在没有竞争的情况下,获取关键部分就像InterlockedCompareExchange操作一样简单
  • 关键部分的结构为互斥量保留了空间。最初未分配
  • 如果关键部分的线程之间存在争用,则将分配并使用互斥量。关键部分的性能将降低到互斥锁的性能
  • 如果您预期竞争激烈,则可以分配关键部分以指定旋转计数。
  • 如果关键部分具有旋转计数,则尝试获取该关键部分的线程将在该多个处理器周期中进行旋转(繁忙等待)。这可以比睡眠状态带来更好的性能,因为执行上下文切换到另一个线程的周期数可能比拥有线程释放互斥锁所花费的周期数高得多。
  • 如果旋转计数过期,则将分配互斥量
  • 当拥有线程释放关键部分时,需要检查是否分配了互斥锁,如果已分配,则将设置互斥锁释放等待线程

在Linux中,我认为它们具有“自旋锁”,其作用与具有旋转计数的关键部分类似。


不幸的是,窗口关键部分涉及在内核模式下执行CAS操作,这比实际的互锁操作昂贵得多。同样,Windows关键部分可以具有关联的旋转计数。
09年

2
绝对不是这样。在用户模式下,可以使用cmpxchg完成CAS。
迈克尔”在2009年

我认为如果您调用InitializeCriticalSection,则默认旋转计数为零-如果要应用旋转计数,则必须调用InitializeCriticalSectionAndSpinCount。你有参考吗?
1800信息

18

关键部分和互斥对象不是特定于操作系统的,它们的多线程/多处理概念。

关键部分 是一段代码,只能在任何给定时间自行运行(例如,有5个线程同时运行,并且有一个名为“ critical_section_function”的函数可更新数组)……您不希望所有5个线程一次更新数组,因此,当程序运行critical_section_function()时,其他任何线程都不必运行critical_section_function。

Mutex * Mutex是一种实现关键部分代码的方法(将其像令牌一样考虑...线程必须拥有它才能运行critical_section_code)


2
同样,互斥锁可以在进程之间共享。
配置器

14

互斥锁是线程可以获取的对象,从而阻止其他线程获取它。这是建议性的,不是强制性的;线程可以使用互斥体表示的资源而无需获取它。

关键部分是操作系统保证不中断的代码长度。用伪代码,它将像:

StartCriticalSection();
    DoSomethingImportant();
    DoSomeOtherImportantThing();
EndCriticalSection();

1
我认为发布者正在谈论用户模式同步原语,例如win32 Critical section对象,它只是提供互斥。我不了解Linux,但是Windows内核具有一些关键区域,它们的行为就像您描述的一样-不可中断。
迈克尔

1
我不知道你为什么被拒绝。您已经正确描述了关键部分的概念,它与Windows内核对象CriticalSection不同,后者是一种互斥体。我认为OP正在询问后一个定义。
亚当·罗森菲尔德

至少我对语言不可知标记感到困惑。但是无论如何,这就是微软为它们的实现命名为与基类​​相同的结果。不良的编码习惯!
Mikko Rantanen

好吧,他要求尽可能多的细节,特别是说Windows和Linux听起来不错。+1 –也不理解-1:/
Jason Coco

14

在Linux中,“快速” Windows等于关键选择,这将是futex,它代表快速用户空间互斥体。futex和互斥锁之间的区别在于,对于futex,仅在需要仲裁时才涉及内核,因此您可以节省每次修改原子计数器时与内核交谈的开销。这..可以节省显著在某些应用时间谈判锁的数量。

也可以使用共享互斥量的方式在进程之间共享futex。

不幸的是,futex的实现可能非常棘手(PDF)。(2018年更新,它们并没有像2009年那样可怕)。

除此之外,这两个平台的功能几乎相同。您正在以一种(希望)不会引起饥饿的方式对共享结构进行原子,令牌驱动的更新。剩下的只是完成该任务的方法。


6

在Windows中,关键部分位于过程本地。互斥锁可以跨进程共享/访问。基本上,关键部分便宜得多。不能特别在Linux上发表评论,但是在某些系统上,它们只是同一件事的别名。


6

仅加2美分,关键部分定义为一种结构,对其进行的操作在用户模式上下文中执行。

ntdll!_RTL_CRITICAL_SECTION
   + 0x000 DebugInfo:Ptr32 _RTL_CRITICAL_SECTION_DEBUG
   + 0x004 LockCount:Int4B
   + 0x008递归计数:Int4B
   + 0x00c OwningThread:Ptr32无效
   + 0x010 LockSemaphore:Ptr32无效
   + 0x014 SpinCount:Uint4B

而互斥锁是在Windows对象目录中创建的内核对象(ExMutantObjectType)。互斥操作主要在内核模式下实现。例如,创建互斥锁时,您最终在内核中调用了nt!NtCreateMutant。


初始化并使用Mutex对象的程序崩溃时会发生什么?Mutex对象会自动释放吗?不,我会说。对?
安库尔

6
内核对象具有引用计数。关闭对象的句柄会减少引用计数,当引用计数达到0时,对象将被释放。当进程崩溃时,它的所有句柄都将自动关闭,因此只有该进程具有该句柄的互斥锁将被自动释放。
迈克尔

这就是关键部分对象受流程绑定的原因,而互斥锁可以在流程之间共享。
西西尔(Sisir)

2

迈克尔的好答案。我为C ++ 11中引入的互斥锁类添加了第三项测试。结果有些有趣,并且仍然支持他对单个进程的CRITICAL_SECTION对象的最初认可。

mutex m;
HANDLE mutex = CreateMutex(NULL, FALSE, NULL);
CRITICAL_SECTION critSec;
InitializeCriticalSection(&critSec);

LARGE_INTEGER freq;
QueryPerformanceFrequency(&freq);
LARGE_INTEGER start, end;

// Force code into memory, so we don't see any effects of paging.
EnterCriticalSection(&critSec);
LeaveCriticalSection(&critSec);
QueryPerformanceCounter(&start);
for (int i = 0; i < 1000000; i++)
{
    EnterCriticalSection(&critSec);
    LeaveCriticalSection(&critSec);
}

QueryPerformanceCounter(&end);

int totalTimeCS = (int)((end.QuadPart - start.QuadPart) * 1000 / freq.QuadPart);

// Force code into memory, so we don't see any effects of paging.
WaitForSingleObject(mutex, INFINITE);
ReleaseMutex(mutex);

QueryPerformanceCounter(&start);
for (int i = 0; i < 1000000; i++)
{
    WaitForSingleObject(mutex, INFINITE);
    ReleaseMutex(mutex);
}

QueryPerformanceCounter(&end);

int totalTime = (int)((end.QuadPart - start.QuadPart) * 1000 / freq.QuadPart);

// Force code into memory, so we don't see any effects of paging.
m.lock();
m.unlock();

QueryPerformanceCounter(&start);
for (int i = 0; i < 1000000; i++)
{
    m.lock();
    m.unlock();
}

QueryPerformanceCounter(&end);

int totalTimeM = (int)((end.QuadPart - start.QuadPart) * 1000 / freq.QuadPart);


printf("C++ Mutex: %d Mutex: %d CritSec: %d\n", totalTimeM, totalTime, totalTimeCS);

我的结果分别是217、473和19(请注意,我最近两个时间的比率与迈克尔的比率大致相当,但我的机器比迈克尔的年龄小至少四岁,因此您可以看到有证据表明2009年至2013年之间速度提高了,当XPS-8700推出时)。新的互斥锁类的速度是Windows互斥锁的两倍,但仍不到Windows CRITICAL_SECTION对象的十分之一。请注意,我仅测试了非递归互斥体。CRITICAL_SECTION对象是递归的(如果离开的次数相同,则一个线程可以重复输入它们)。


0

如果AC函数仅使用其实际参数,则称为可重入函数。

可重入函数可以同时被多个线程调用。

可重入函数示例:

int reentrant_function (int a, int b)
{
   int c;

   c = a + b;

   return c;
}

非重入函数示例:

int result;

void non_reentrant_function (int a, int b)
{
   int c;

   c = a + b;

   result = c;

}

C标准库strtok()不可重入,并且不能同时被2个或更多线程使用。

某些平台SDK带有strtok()的可重入版本,称为strtok_r();。

恩里科·米格洛尔(Enrico Migliore)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.