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

请从 Linux,Windows 的角度解释一下?

我是用 C # 编程的,这两个术语会有区别吗。请张贴尽可能多的,与例子等... 。

谢谢

109456 次浏览

在 Windows 中,关键部分是进程的本地部分。互斥锁可以跨进程共享/访问。基本上,临界部分要便宜得多。不能具体评论 Linux,但在某些系统上,它们只是同一事物的别名。

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

临界段是由操作系统保证不会中断的代码长度。在伪代码中,就像这样:

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

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

关键部分 是一段代码,必须在任何给定的时间只由它自己运行(例如,有5个线程同时运行,一个名为“关键 _ section _ function”的函数更新一个数组... 你不希望所有5个线程同时更新数组。因此,当程序运行足够重要的 _ section _ function ()时,其他线程都不必运行它们的足够重要的 _ section _ function ()。

互斥对象 * Mutex 是实现关键节代码的一种方法(把它想象成一个令牌... ... 线程必须拥有它才能运行关键节代码)

对于 Windows,关键部分比互斥锁更轻量级。

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

关键部分只能在一个进程中使用,但是它们的优点是只有在出现争用的情况下才切换到内核模式——无争用获取(这应该是常见的情况)的速度快得令人难以置信。在争用的情况下,它们进入内核等待某个同步原语(如事件或信号量)。

我编写了一个快速示例应用程序,比较他们两人之间的时间。在我的系统上,对于1,000,000个未竞争的获取和释放,一个互斥锁占用一秒钟。1,000,000次获得一个临界段需要约50毫秒。

这是测试代码,我运行了这个,如果互斥对象是第一个或第二个,得到了类似的结果,所以我们没有看到任何其他效果。

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);

除了其他答案,以下细节是特定于窗口的关键部分:

  • 在没有争用的情况下,获取关键部分就像 InterlockedCompareExchange操作一样简单
  • 临界节结构为互斥锁保留了空间。它最初是未分配的
  • 如果某个关键部分的线程之间存在争用,则将分配和使用互斥锁。临界区的性能将降低到互斥对象的性能
  • 如果您预期高竞争,您可以分配指定旋转计数的关键部分。
  • 如果在一个关键部分上存在带有自旋计数的争用,那么尝试获取该关键部分的线程将在那么多处理器周期内自旋(繁忙等待)。这可以带来比睡眠更好的性能,因为执行上下文切换到另一个线程的周期数可能远远高于所属线程释放互斥锁所需的周期数
  • 如果自旋计数过期,则会分配互斥对象
  • 当拥有线程释放关键部分时,需要检查是否分配了互斥锁,如果分配了,那么它将设置互斥锁以释放等待的线程

在 linux 中,我认为他们有一个“自旋锁”,用于类似的临界部分与自旋计数。

从理论上讲,临界区是一段代码,不能由多个线程同时运行,因为这段代码可以访问共享资源。

互斥锁是一种用于保护关键部分的算法(有时是数据结构的名称)。

信号量 监视器是互斥对象的常用实现。

实际上,在 Windows 中有许多互斥锁实现可用。它们主要由于其锁定级别、范围、成本以及在不同争用级别下的性能的实现而不同。请参阅 CLR Inside Out- 为可伸缩性使用并发性为不同互斥对象实现的成本绘制图表

可用的同步原语。

lock(object)语句是使用 Monitor实现的——请参阅 MSDN

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

仅仅为了增加我的2分钱,关键部分被定义为一个结构,对它们的操作是在用户模式上下文中执行的。

ntdll!_RTL_CRITICAL_SECTION
+0x000 DebugInfo        : Ptr32 _RTL_CRITICAL_SECTION_DEBUG
+0x004 LockCount        : Int4B
+0x008 RecursionCount   : Int4B
+0x00c OwningThread     : Ptr32 Void
+0x010 LockSemaphore    : Ptr32 Void
+0x014 SpinCount        : Uint4B

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

在 Linux 中,与关键选择相当的“快速”Windows 应该是 未来,它代表快速用户空间互斥锁。Fuex 和互斥对象之间的区别在于,对于 fuex,只有在需要仲裁时才涉及内核,因此每次修改原子计数器时都可以节省与内核通信的开销。那个。.在某些应用程序中,可以节省 意义重大协商锁的时间。

也可以使用共享互斥对象的方法在进程之间共享一个互斥对象。

不幸的是,未来曲线可以是 很难实施(PDF)。(2018年更新,它们不再像2009年那样可怕)。

除此之外,它在两个平台上的表现基本相同。您正在以一种(希望)不会导致饥饿的方式对共享结构进行原子的、令牌驱动的更新。剩下的只是实现这一目标的方法。

迈克尔的回答很棒。我已经为 C + + 11中引入的互斥类添加了第三个测试。这个结果有些有趣,并且仍然支持他最初对单个进程的 CRITICS _ 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(注意,我最后两次测试的时间比例与迈克尔的大致相当,但我的机器至少比他的年轻4年,所以你可以看到 XPS-8700在2009年和2013年之间速度提高的证据)。新的互斥对象类的速度是 Windows 互斥对象的两倍,但仍然不到 Windows CRITICS _ SECTION 对象的十分之一。注意,我只测试了非递归互斥锁。CRITICS _ SECTION 对象是递归的(一个线程可以重复输入它们,前提是它离开的次数相同)。

如果一个 C 函数只使用它的实际参数,那么它就被称为可重入函数。

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

可重入函数示例:

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();