互斥锁和临界区有啥区别?
Posted
技术标签:
【中文标题】互斥锁和临界区有啥区别?【英文标题】:What is the difference between mutex and critical section?互斥锁和临界区有什么区别? 【发布时间】:2010-10-22 11:19:21 【问题描述】:请从Linux、Windows的角度解释一下?
我正在用 C# 编程,这两个术语会有所不同吗?请尽可能多地发布,并附上示例等......
谢谢
【问题讨论】:
【参考方案1】:如果 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()
;
【讨论】:
在我未经训练的眼睛看来,这似乎无法回答有关互斥锁和临界区的问题。如果您对这与问题的关系添加一些解释,那么它将吸引更多的赞成票并保持开放。否则,它可能会因为不是此问题的答案而被删除。【参考方案2】:Linux 中与关键选择相同的“快速”Windows 将是 futex,它代表快速用户空间互斥锁。 futex 和 mutex 之间的区别在于,使用 futex,内核仅在需要仲裁时才涉及,因此您可以节省每次修改原子计数器时与内核对话的开销。这 .. 可以节省 大量 在某些应用程序中协商锁定的时间。
futex 也可以在进程之间共享,使用共享互斥锁的方式。
不幸的是,futex 可以是very tricky to implement (PDF)。 (2018 年更新,它们并不像 2009 年那样可怕)。
除此之外,它在两个平台上几乎相同。您正在以一种(希望)不会导致饥饿的方式对共享结构进行原子的、令牌驱动的更新。剩下的只是实现它的方法。
【讨论】:
【参考方案3】:互斥锁是一个线程可以获取的对象,防止其他线程获取它。这是建议性的,不是强制性的;线程可以使用互斥锁所代表的资源而不需要获取它。
临界区是操作系统保证不会被中断的一段代码。在伪代码中,它会是:
StartCriticalSection();
DoSomethingImportant();
DoSomeOtherImportantThing();
EndCriticalSection();
【讨论】:
我认为发帖者在谈论用户模式同步原语,例如 win32 关键部分对象,它只是提供互斥。我不了解 Linux,但 Windows 内核的关键区域的行为就像您描述的那样 - 不可中断。 我不知道你为什么被否决。您已经正确描述了关键部分的概念,它与称为CriticalSection 的Windows 内核对象不同,后者是一种互斥锁。我相信 OP 是在询问后一种定义。 至少我对与语言无关的标签感到困惑。但无论如何,这就是微软将它们的实现命名为与它们的基类相同的结果。糟糕的编码习惯! 嗯,他要求尽可能多的细节,并特别说 Windows 和 Linux 听起来概念很好。 +1 -- 也不理解 -1 :/【参考方案4】:迈克尔的回答很好。我为 C++11 中引入的 mutex 类添加了第三个测试。结果有些有趣,并且仍然支持他最初对单个进程的 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(请注意,我最后两次的时间比率与迈克尔的大致相当,但我的机器比他的机器至少年轻 4 岁,所以你可以看到速度增加的证据2009 年和 2013 年,当 XPS-8700 出现时)。新的互斥锁类的速度是 Windows 互斥锁的两倍,但仍不到 Windows CRITICAL_SECTION 对象速度的十分之一。请注意,我只测试了非递归互斥锁。 CRITICAL_SECTION 对象是递归的(一个线程可以重复进入它们,只要它离开的次数相同)。
【讨论】:
【参考方案5】:从理论上讲,critical section 是一段不能由多个线程同时运行的代码,因为该代码访问共享资源。
mutex 是一种算法(有时是数据结构的名称),用于保护临界区。
Semaphores 和 Monitors 是互斥体的常见实现。
在实践中,Windows 中提供了许多互斥锁实现。它们的主要区别在于它们的锁定级别、范围、成本以及在不同争用级别下的性能。请参阅CLR Inside Out - Using concurrency for scalability 了解不同互斥体实现的成本图表。
可用的同步原语。
Monitor Mutex Semaphore ReaderWriterLock ReaderWriterLockSlim Interlockedlock(object)
语句是使用 Monitor
实现的 - 请参阅 MSDN 以获取参考。
在过去几年中,对non-blocking synchronization 进行了大量研究。目标是以无锁或无等待的方式实现算法。在这样的算法中,一个进程帮助其他进程完成它们的工作,以便该进程最终可以完成它的工作。因此,即使尝试执行某些工作的其他进程挂起,进程也可以完成其工作。使用锁,他们不会释放他们的锁并阻止其他进程继续。
【讨论】:
看到接受的答案,我想也许我记错了关键部分的概念,直到我看到你写的 Theoretical Perspective。 :) 实用无锁编程就像香格里拉,只是它存在。 Keir Fraser 的paper (PDF) 相当有趣地探讨了这一点(可追溯到 2004 年)。 2012 年我们仍在为此苦苦挣扎。我们糟透了。【参考方案6】:为了增加我的 2 美分,关键部分被定义为一个结构,并且对它们的操作在用户模式上下文中执行。
ntdll!_RTL_CRITICAL_SECTION +0x000 调试信息:Ptr32 _RTL_CRITICAL_SECTION_DEBUG +0x004 LockCount : Int4B +0x008 递归计数:Int4B +0x00c OwningThread : Ptr32 无效 +0x010 LockSemaphore : Ptr32 无效 +0x014 旋转计数:Uint4B
而互斥体是在 Windows 对象目录中创建的内核对象 (ExMutantObjectType)。互斥操作主要在内核模式下实现。例如,在创建 Mutex 时,您最终会在内核中调用 nt!NtCreateMutant。
【讨论】:
当初始化和使用 Mutex 对象的程序崩溃时会发生什么? Mutex 对象会自动释放吗?不,我会说。对吗? 内核对象有一个引用计数。关闭对象的句柄会减少引用计数,当它达到 0 时,对象被释放。当一个进程崩溃时,它的所有句柄都会自动关闭,因此只有该进程有句柄的互斥锁将被自动释放。 这就是关键部分对象是进程绑定的原因,另一方面,互斥锁可以跨进程共享。【参考方案7】:对于 Windows,临界区比互斥体轻。
互斥锁可以在进程之间共享,但总是会导致对内核的系统调用有一些开销。
关键部分只能在一个进程中使用,但其优点是它们只在争用情况下切换到内核模式 - 非竞争获取,这应该是常见的情况,速度非常快。在争用的情况下,它们进入内核等待某个同步原语(如事件或信号量)。
我编写了一个快速示例应用程序,用于比较两者之间的时间。在我的系统上进行 1,000,000 次无竞争的获取和释放,互斥量需要一秒钟。 1,000,000 次获取的关键部分大约需要 50 毫秒。
这是测试代码,如果 mutex 是第一个或第二个,我运行它并得到类似的结果,所以我们没有看到任何其他效果。
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);
【讨论】:
不确定这是否相关(因为我还没有编译并尝试过您的代码),但我发现使用 INFINITE 调用 WaitForSingleObject 会导致性能不佳。将超时值 1 传递给它,然后在检查它的返回时循环,这对我的一些代码的性能产生了巨大的影响。这主要是在等待外部进程句柄的情况下,但是......不是互斥锁。 YMMV。我很想看看 mutex 在修改后的表现如何。此测试产生的时间差似乎比预期的要大。 @TroyHoward 你当时不是基本上只是自旋锁定吗? 这种区别的原因可能主要是历史原因。在无竞争的情况下(很少的原子指令,没有系统调用)实现与 CriticalSection 一样快的锁定并不难,但可以跨进程工作(使用一块共享内存)。参见例如Linux futexes. @TroyHoward 尝试强制您的 CPU 始终以 100% 运行,看看 INFINITE 是否工作得更好。在我的机器 (Dell XPS-8700) 决定减速后,电源策略可能需要长达 40 毫秒的时间才能爬回全速,如果您睡眠或仅等待一毫秒,它可能不会这样做。 我不确定我是否理解这里演示的内容。通常,进入临界区需要获取某种信号量。你是说在幕后,O/S 有一种有效的方式来实现这个关键部分的行为,而不需要互斥锁?【参考方案8】:除了其他答案之外,以下详细信息特定于 windows 上的关键部分:
在没有争用的情况下,获取临界区就像InterlockedCompareExchange
操作一样简单
临界区结构为互斥体留出了空间。它最初是未分配的
如果线程之间争用临界区,则将分配和使用互斥锁。临界区的性能会降低到互斥锁的性能
如果您预计会出现高争用,您可以分配指定旋转计数的临界区。
如果临界区与旋转计数发生争用,则尝试获取临界区的线程将旋转(忙-等待)那么多处理器周期。这可以带来比休眠更好的性能,因为执行上下文切换到另一个线程的周期数可能远高于拥有线程释放互斥锁所花费的周期数
如果自旋计数到期,将分配互斥锁
当所属线程释放临界区时,需要检查是否分配了互斥锁,如果是则设置互斥锁释放等待线程
在 linux 中,我认为它们有一个“自旋锁”,其作用类似于具有自旋计数的临界区。
【讨论】:
不幸的是,Window 临界区涉及在内核模式 中执行 CAS 操作,这比实际的联锁操作要昂贵得多。此外,Windows 关键部分可以具有与之关联的旋转计数。 这绝对不是真的。 CAS 可以在用户模式下使用 cmpxchg 完成。 如果您调用 InitializeCriticalSection,我认为默认的自旋计数为零 - 如果您想应用自旋计数,则必须调用 InitializeCriticalSectionAndSpinCount。你有这方面的参考吗?【参考方案9】:Critical Section 和 Mutex 不是操作系统特定的,它们是多线程/多处理的概念。
关键部分 是一段只能在任何给定时间由它自己运行的代码(例如,有 5 个线程同时运行,一个名为“critical_section_function”的函数会更新一个数组......你不希望所有 5 个线程都更新数组一次。所以当程序运行critical_section_function()时,没有其他线程必须运行它们的critical_section_function。
互斥体* 互斥锁是一种实现临界区代码的方式(把它想象成一个令牌......线程必须拥有它才能运行临界区代码)
【讨论】:
此外,互斥锁可以跨进程共享。【参考方案10】:在 Windows 中,关键部分是您的进程的本地部分。可以跨进程共享/访问互斥锁。基本上,关键部分要便宜得多。无法具体评论 Linux,但在某些系统上它们只是同一事物的别名。
【讨论】:
以上是关于互斥锁和临界区有啥区别?的主要内容,如果未能解决你的问题,请参考以下文章
临界区(critical section 每个线程中访问 临界资源 的那段代码)和互斥锁(mutex)的区别(进程间互斥量共享内存虚拟地址)