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

时间:2009-04-29 00:23:33

标签: windows linux multithreading programming-languages

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

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

由于

10 个答案:

答案 0 :(得分:218)

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

答案 1 :(得分:83)

从理论角度来看,critical section是一段代码,由于代码访问共享资源,因此不能同时由多个线程运行。

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

SemaphoresMonitors是互斥锁的常见实现。

在实践中,Windows中有许多可用的互斥体实现。它们的主要不同之处在于它们的锁定水平,范围,成本以及在不同争用水平下的表现。有关不同互斥实现的成本图表,请参阅CLR Inside Out - Using concurrency for scalability

可用的同步原语。

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

在过去几年中,对non-blocking synchronization进行了大量研究。目标是以无锁或无等待的方式实现算法。在这样的算法中,过程有助于其他过程完成其工作,以便过程最终完成其工作。因此,即使当试图执行某些工作的其他进程挂起时,进程也可以完成其工作。 Usinig锁定,他们不会释放锁定并阻止其他进程继续。

答案 2 :(得分:20)

除了其他答案外,以下详细信息仅适用于Windows上的关键部分:

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

在linux中,我认为他们有一个“旋转锁定”,它与具有旋转计数的关键部分具有相似的目的。

答案 3 :(得分:18)

Critical Section和Mutex不是特定于操作系统的,它们是多线程/多处理的概念。

关键部分 是一段代码,只能在任何给定时间由它自己运行(例如,有5个线程同时运行,而一个名为“critical_section_function”的函数更新数组...你不希望所有5个线程都更新一次运行数组。所以当程序运行critical_section_function()时,其他任何线程都不能运行他们的critical_section_function。

<强>互斥* Mutex是一种实现关键部分代码的方式(想象它就像一个令牌......线程必须拥有它才能运行critical_section_code)

答案 4 :(得分:14)

Linux中“快速”Windows等级的关键选择将是futex,代表快速用户空间互斥。 futex和互斥锁之间的区别在于,使用futex时,只有在需要仲裁时才会涉及内核,因此每次修改原子计数器时都可以节省与内核通信的开销。那个..可以节省在一些应用程序中协商锁的重要的时间。

还可以使用您用来共享互斥锁的方式在进程之间共享futex。

不幸的是,futex可以是very tricky to implement(PDF)。 (2018年的更新,它们并不像2009年那样可怕)。

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

答案 5 :(得分:13)

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

关键部分是操作系统保证不会中断的一段代码。在伪代码中,它就像:

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

答案 6 :(得分:6)

在Windows中,关键部分是您的流程的本地部分。可以跨进程共享/访问互斥锁。基本上,关键部分要便宜得多。不能专门评论Linux,但在某些系统上,它们只是同一个东西的别名。

答案 7 :(得分:6)

只需添加我的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!NtCreateMutant。

答案 8 :(得分:1)

迈克尔的好回答。我为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对象是递归的(一个线程可以重复输入它们,只要它们保留相同的次数)。

答案 9 :(得分:-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();