繁体   English   中英

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

[英]What is the difference between mutex and critical section?

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

我在 C# 编程,这两个术语会有所不同吗? 请尽可能多地张贴示例等......

谢谢

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

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

互斥锁是一种用于保护临界区的算法(有时是数据结构的名称)。

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

实际上,Windows 中有许多互斥体实现可用。 它们的主要区别在于它们的锁定级别、范围、成本以及在不同争用级别下的性能。 有关不同互斥体实现的成本图表,请参阅CLR Inside Out - Using concurrency for scaling。

可用的同步原语。

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

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

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

  • 在没有争用的情况下,获取临界区就像InterlockedCompareExchange操作一样简单
  • 临界区结构为互斥锁留有空间。 它最初是未分配的
  • 如果临界区的线程之间存在争用,则将分配和使用互斥锁。 临界区的性能将下降到互斥锁的性能
  • 如果您预计会出现高争用,您可以分配指定自旋计数的临界区。
  • 如果在具有自旋计数的临界区上存在争用,则尝试获取临界区的线程将自旋(忙等待)多个处理器周期。 这可以导致比休眠更好的性能,因为执行上下文切换到另一个线程的周期数可能远高于拥有线程释放互斥锁所花费的周期数
  • 如果自旋计数到期,则将分配互斥锁
  • 当所属线程释放临界区时,需要检查互斥锁是否被分配,如果是则设置互斥锁释放一个等待线程

在 linux 中,我认为它们有一个“自旋锁”,其作用与具有自旋计数的临界区相似。

临界区和互斥体不是操作系统特定的,它们的多线程/多处理概念。

临界区是一段只能在任何给定时间由它自己运行的代码(例如,有 5 个线程同时运行,一个名为“critical_section_function”的函数更新数组......你不想要所有 5 个线程立即更新数组。因此,当程序运行critical_section_function() 时,其他线程都不必运行它们的critical_section_function。

mutex* Mutex 是一种实现临界区代码的方式(把它想象成一个令牌……线程必须拥有它才能运行critical_section_code)

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

临界区是由操作系统保证不被中断的一段代码。 在伪代码中,它会是这样的:

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

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

futex 也可以在进程之间共享,使用共享互斥锁的方法。

不幸的是,futex 实现起来可能非常棘手(PDF)。 (2018 年更新,它们几乎不像 2009 年那么可怕)。

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

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

补充一下我的 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)。 互斥操作主要在内核模式下实现。 例如,在创建互斥锁时,您最终会在内核中调用 nt!NtCreateMutant。

迈克尔的精彩回答。 我为 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 对象是递归的(一个线程可以重复进入它们,前提是它离开的次数相同)。

我发现声明关键部分保护代码部分不被多个线程输入的解释非常具有误导性。 保护代码没有意义,因为代码是只读的,不能被多个线程修改。 人们通常想要的是保护数据不被多个线程修改,导致不连贯 state。通常一个互斥锁(或临界区,实现相同的目的)应该与一些数据相关联。 访问此数据的每个代码部分都应获取互斥锁/关键部分,并在完成访问数据后释放。 这可能比仅仅锁定线程进入 function 更细粒度。此外,根据我的经验,通过某些同步锁定功能更容易出错,尤其是死锁。 可以在此处找到涵盖该主题的一篇好文章: https://www.bogotobogo.com/cplusplus/multithreaded4_cplusplus11B.php

因此,总而言之(递归)互斥锁和临界区基本上实现了相同的目的,不是保护代码,而是保护数据。

临界区可以比普通的 kernel 互斥体更有效地实现。 第一个答案中给出的示例有点误导,因为它没有描述同步原语的设计目的:同步访问某事。 来自多个线程。 当临界区/互斥锁永远不会被另一个线程拥有时,这个例子只是衡量了这种微不足道的情况。 例如,如果两个线程在短的、互锁的周期内访问数据,那么临界区可能会更有效率,但如果我们有很多线程访问同一块数据,它们的效率可能会降低。 每个线程都会自旋锁直到放弃并等待信号量,临界区的一部分执行。 在测量执行时间时也应考虑这种情况。

如果 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()

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM