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

2024-10-22 08:29:00
admin
原创
96
摘要:问题描述:请从 Linux、Windows 的角度解释一下?我使用 C# 编程,这两个术语会有什么区别吗?请尽可能多地发布内容,并附上示例等……谢谢解决方案 1:对于 Windows 来说,临界区比互斥锁更轻量。互斥锁可以在进程之间共享,但总是会导致对内核的系统调用,这会产生一些开销。临界区只能在一个进程中使...

问题描述:

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

我使用 C# 编程,这两个术语会有什么区别吗?请尽可能多地发布内容,并附上示例等……

谢谢


解决方案 1:

对于 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
", totalTime, totalTimeCS);

解决方案 2:

从理论角度来看,关键部分是一段不能由多个线程同时运行的代码,因为该代码访问共享资源。

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

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

实际上,Windows 中有许多可用的互斥锁实现。它们的主要区别在于其实现的锁定级别、范围、成本以及在不同争用级别下的性能。请参阅CLR Inside Out - 使用并发性实现可扩展性,了解不同互斥锁实现的成本图表。

可用的同步原语。

  • 监视器

  • 互斥锁

  • 信号

  • 读写锁

  • 读写锁Slim

  • 互锁

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

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

解决方案 3:

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

  • 在没有争用的情况下,获取关键部分就像InterlockedCompareExchange操作一样简单

  • 临界区结构为互斥锁保留空间。它最初未分配

  • 如果线程之间争用某个关键部分,则将分配并使用互斥锁。关键部分的性能将降低到互斥锁的性能

  • 如果您预计竞争激烈,则可以分配指定旋转次数的临界区。

  • 如果对具有旋转计数的临界区存在争用,则尝试获取临界区的线程将旋转(忙等待)那么多处理器周期。这可以带来比睡眠更好的性能,因为执行上下文切换到另一个线程的周期数可能比拥有线程释放互斥锁所需的周期数高得多

  • 如果旋转计数到期,则将分配互斥锁

  • 当拥有线程释放临界区时,需要检查互斥锁是否已分配,如果已分配,则将设置互斥锁以释放等待线程

在 Linux 中,我认为它们有一个“旋转锁”,其用途与具有旋转计数的临界区类似。

解决方案 4:

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

临界区
是一段在任意给定时间内只能自行运行的代码(例如,有 5 个线程同时运行,并且有一个名为“critical_section_function”的函数用于更新数组……您不希望所有 5 个线程同时更新数组。因此,当程序运行 critical_section_function() 时,其他线程都不必运行其 critical_section_function。

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

解决方案 5:

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

临界区是操作系统保证不被中断的一段代码。用伪代码来表示,它应该是这样的:

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

解决方案 6:

Windows 中与 Linux 中关键选择相当的“快速”功能是futex,它代表快速用户空间互斥锁。futex 和互斥锁之间的区别在于,使用 futex 时,内核仅在需要仲裁时才参与,因此您无需在每次修改原子计数器时与内核通信。这……可以节省某些应用程序中协商锁的大量时间。

futex 还可以在进程之间共享,使用与共享互斥锁相同的方式。

不幸的是,futexes 的实现非常棘手(PDF)。(2018 年更新,它们并不像 2009 年那么可怕)。

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

解决方案 7:

我只是想补充一点,关键部分被定义为一种结构,对它们的操作是在用户模式上下文中执行的。

ntdll!_RTL_CRITICAL_SECTION
   +0x000 调试信息:Ptr32 _RTL_CRITICAL_SECTION_DEBUG
   +0x004 锁定计数: Int4B
   +0x008 递归计数 : Int4B
   +0x00c 所属线程 : Ptr32 Void
   +0x010 LockSemaphore : Ptr32 无效
   +0x014 旋转次数: Uint4B

而互斥体是内核对象(ExMutantObjectType),在 Windows 对象目录中创建。互斥体操作大多在内核模式下实现。例如,创建互斥体时,最终会在内核中调用 nt!NtCreateMutant。

解决方案 8:

在 Windows 中,临界区是进程的本地区域。互斥锁可以在进程间共享/访问。基本上,临界区的成本要低得多。无法具体评论 Linux,但在某些系统上,它们只是同一事物的别名。

解决方案 9:

Michael 的回答很棒。我为 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
", totalTimeM, totalTime, totalTimeCS);

我的结果是 217、473 和 19(请注意,我最后两个的时间比率与 Michael 的大致相当,但我的机器至少比他的年轻四年,因此您可以看到 2009 年至 2013 年 XPS-8700 问世期间速度提高的证据)。新的互斥锁类的速度是 Windows 互斥锁的两倍,但仍不到 Windows CRITICAL_SECTION 对象速度的十分之一。请注意,我只测试了非递归互斥锁。CRITICAL_SECTION 对象是递归的(一个线程可以反复进入它们,前提是它离开的次数相同)。

解决方案 10:

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

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

临界区可能比普通内核互斥锁实现得更有效。第一个答案中给出的示例有点误导,因为它没有描述同步原语的设计目的:同步来自多个线程的对某事物的访问。该示例仅测量了临界区/互斥锁从未被其他线程拥有的简单情况。虽然如果两个线程在短时间内互锁访问数据,临界区可能会更有效,但如果我们让许多线程访问同一块数据,临界区可能会效率降低。每个线程都会自旋锁定,直到放弃并等待信号量,这是临界区实现的一部分。在测量执行时间时也应考虑这种情况。

解决方案 11:

如果 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()不是可重入的,不能同时被两个或更多线程使用。

某些平台 SDK 附带了可重入版本,strtok()称为strtok_r()

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用