锁、互斥量和信号量有啥区别?

Posted

技术标签:

【中文标题】锁、互斥量和信号量有啥区别?【英文标题】:What is the difference between lock, mutex and semaphore?锁、互斥量和信号量有什么区别? 【发布时间】:2011-01-20 22:11:49 【问题描述】:

听说过这些跟并发编程有关的词,但是锁、互斥量和信号量有什么区别呢?

【问题讨论】:

ans;***.com/a/346678/1697099 我见过的最好的解释:crystal.uta.edu/~ylei/cse6324/data/semaphore.pdf Difference between binary semaphore and mutex的可能重复 【参考方案1】:

锁只允许一个线程进入被锁定的部分,并且锁不与任何其他进程共享。

互斥锁与锁相同,但它可以是系统范围的(由多个进程共享)。

semaphore 的作用与互斥锁相同,但允许 x 个线程进入,这可用于限制同时运行的 cpu、io 或 ram 密集型任务的数量。

有关互斥量和信号量之间差异的更详细的帖子,请阅读here。

您还拥有读/写锁,在任何给定时间允许无限数量的读取器或 1 个写入器。

【讨论】:

@mertinan 我不能说我听说过它,但这就是***所说的“闩锁(数据库),(相对短暂的)锁定系统数据结构,如索引" Monitor 允许等待某个条件(例如,当锁被释放时),“monitors”。 信号量与互斥量不同。它们的使用方式非常不同,并且具有不同的属性(即关于所有权)。有关详细信息,请参阅例如 barrgroup.com/Embedded-Systems/How-To/RTOS-Mutex-Semaphore @nanoquack 如果您认为我的答案具有误导性或不正确,请随时编辑。 为了更清楚地区分互斥量和信号量,在 nanoquack 的链接中,关键段落是“信号量的正确使用是为了从一个任务到另一个任务。互斥量意味着使用它所保护的共享资源的每个任务总是按此顺序获取和释放。相比之下,使用信号量的任务要么发出信号,要么等待,而不是两者兼而有之。"【参考方案2】:

关于这些词有很多误解。

这是来自之前的帖子 (https://***.com/a/24582076/3163691),非常适合这里:

1) 关键部分= 用户对象,用于仅允许执行一个活动线程来自许多其他在一个过程中。其他未选择的线程(@获取此对象)被置于睡眠

[没有进程间能力,非常原始的对象]。

2) Mutex Semaphore(又名 Mutex)= 内核对象,用于仅允许执行来自许多其他线程的一个活动线程在不同的进程中。其他未选择的线程(@获取此对象)被置于睡眠。该对象支持线程所有权、线程终止通知、递归(来自同一线程的多个“获取”调用)和“避免优先级反转”。

[进程间能力,使用非常安全,一种“高级”同步对象]。

3) 计数信号量(又名信号量)= 用于允许执行一组活动线程来自许多其他线程的内核对象。其他未选择的线程(@获取此对象)被置于睡眠

[进程间功能但使用起来不是很安全,因为它缺少以下“互斥”属性:线程终止通知、递归?、“避免优先级反转”?等]。

4) 现在,谈论“自旋锁”,首先是一些定义:

Critical Region= 由 2 个或更多进程共享的内存区域。

Lock= 一个变量,其值允许或拒绝进入“关键区域”。 (它可以实现为一个简单的“布尔标志”)。

忙于等待= 不断测试变量,直到出现某个值。

最后:

自旋锁(又名自旋锁)= ,它使用忙等待强>。 (的获取是通过xchg或类似的原子操作 em>)。

[无线程休眠,主要仅在内核级别使用。用户级代码效率低下]。

作为最后的评论,我不确定,但我可以向你打赌,上面的前 3 个同步对象(#1、#2 和 #3)利用这个简单的野兽(#4)作为他们的实施。

祝你有美好的一天!

参考资料:

-Real-Time Concepts for Embedded Systems by Qing Li 和 Caroline Yao(CMP 书籍)。

-Andrew Tanenbaum(培生国际教育)的现代操作系统(第 3 届)。

-Jeffrey Richter(Microsoft 编程系列)的 Microsoft Windows 编程应用程序(第 4 期)。

另外,你可以看看: https://***.com/a/24586803/3163691

【讨论】:

实际上临界区不是内核对象,因此更轻量级且无法跨进程同步。 @Vladislavs Burakovs:你是对的!原谅我的编辑。为了连贯性,我会修复它。 为了更清楚地区分互斥锁和信号量,正如 nanoquack 在其他地方提到的那样,请参阅barrgroup.com/Embedded-Systems/How-To/RTOS-Mutex-Semaphore - 关键段落是“信号量的正确使用是用于从一个任务到另一个任务的信号。互斥锁的使用和释放总是按这个顺序被每个使用它所保护的共享资源的任务所使用。相比之下,使用信号量的任务要么发出信号,要么等待——而不是两者兼而有之。" 重新推测其他锁机制建立在 [inefficient] spinlock:不太可能; AFAIK 只需要一些原子操作加上睡眠队列。即使在内核中需要自旋锁 的地方,现代解决方案也可以最大限度地减少其影响,如Wikipedia - Spinlock - Alternatives 中所述 - “.. 使用称为“自适应互斥锁”的混合方法。这个想法是使用自旋锁当试图访问被当前运行的线程锁定的资源,但如果线程当前没有运行则休眠。(在单处理器系统上总是这种情况。)" @ToolmakerSteve,我敢于为尝试将线程 ID“插入”到“睡眠队列”时的“冲突”问题提供没有“自旋锁”的“解决方案”。无论如何,***文本得出的结论是在实现中使用了自旋锁!!!。【参考方案3】:

大多数问题都可以通过 (i) 仅使用锁,(ii) 仅使用信号量,...,或 (iii) 两者结合来解决!您可能已经发现,它们非常相似:都阻止race conditions,都具有acquire()/release() 操作,都导致零个或多个线程被阻塞/怀疑...... 实际上,关键的区别仅在于它们如何锁定和解锁

(或mutex)有两种状态(0 或1)。它可以是解锁锁定。它们通常用于确保一次只有一个线程进入关键部分。 信号量有许多状态(0、1、2、...)。它可以是锁定(状态0)或解锁(状态1、2、3,...)。一个或多个信号量通常一起使用,以确保当某些资源的单元数已/未达到特定值时(通过向下计数到该值或向上计数到该值),只有一个线程准确地进入临界区)。

对于这两种锁/信号量,在原语处于状态 0 时尝试调用 acquire() 会导致调用线程被挂起。对于锁 - 尝试获取处于状态 1 的锁是成功的。对于信号量 - 尝试在状态 1, 2, 3, ... 中获取锁是成功的。

对于状态0的锁,如果之前调用acquire()的线程相同,现在调用释放,则释放成功。如果一个不同的线程尝试了这个——它取决于实现/库会发生什么(通常忽略尝试或抛出错误)。对于状态 0 的信号量,任何 线程都可以调用 release 并且它会成功(无论之前哪个线程使用获取将信号量置于状态 0)。

从前面的讨论中,我们可以看到锁有一个所有者的概念(唯一可以调用释放的线程是所有者),而信号量没有所有者(任何线程都可以在信号量上调用释放)。


造成很多混乱的是,实际上它们是这个高级定义的许多变体

需要考虑的重要变化

acquire()/release() 应该叫什么? -- [可变 massively] 您的锁/信号量是使用“队列”还是“集合”来记住等待的线程? 你的锁/信号量可以与其他进程的线程共享吗? 你的锁是“可重入的”吗? -- [通常是的]。 你的锁是“阻塞/非阻塞”吗? -- [通常非阻塞被用作阻塞锁(又名自旋锁)导致忙等待]。 如何确保操作是“原子的”?

这些取决于您的书籍/讲师/语言/图书馆/环境。 以下是一些语言如何回答这些细节的快速浏览。


C、C++ (pthreads)

mutex 是通过pthread_mutex_t 实现的。默认情况下,它们不能与任何其他进程共享 (PTHREAD_PROCESS_PRIVATE),但是互斥锁有一个名为 pshared 的属性。设置后,互斥锁在进程之间共享(PTHREAD_PROCESS_SHARED)。 与互斥锁是一回事。 信号量通过sem_t实现。与互斥锁类似,信号量可以在多个进程的线程之间共享,也可以对单个进程的线程保持私有。这取决于提供给sem_initpshared 参数。

蟒蛇(threading.py)

(threading.RLock) 与 C/C++ pthread_mutex_ts 基本相同。两者都是可重入。这意味着它们只能由锁定它的同一线程解锁。 sem_t 信号量、threading.Semaphore 信号量和theading.Lock 锁是不可重入的——因为这种情况任何线程都可以解锁锁/ 降低信号量。 mutex 与锁相同(该术语在 python 中不常用)。 信号量 (threading.Semaphore) 与sem_t 基本相同。尽管使用sem_t,线程ID 队列用于记住线程在锁定时尝试锁定它时被阻塞的顺序。当一个线程解锁一个信号量时,队列中的第一个线程(如果有的话)被选为新的所有者。线程标识符从队列中取出,信号量再次锁定。但是,对于threading.Semaphore,使用集合而不是队列,因此不会存储线程被阻塞的顺序——集合中的任何线程可以被选择为下一个所有者。

Java (java.util.concurrent)

lock (java.util.concurrent.ReentrantLock) 与 C/C++ pthread_mutex_t 和 Python 的 threading.RLock 基本相同,因为它也实现了可重入锁。由于 JVM 充当中介,Java 中的进程之间共享锁更加困难。如果线程尝试解锁不属于它的锁,则会抛出 IllegalMonitorStateExceptionmutex 与锁相同(该术语在 Java 中不常用)。 信号量 (java.util.concurrent.Semaphore) 与sem_tthreading.Semaphore 基本相同。 Java 信号量的构造函数接受一个 fairness 布尔参数,该参数控制是使用集合 (false) 还是队列 (true) 来存储等待线程。

理论上,信号量经常被讨论,但在实践中,信号量的使用并不多。一个信号量只保存 一个 整数的状态,所以通常它相当不灵活,并且一次需要很多——导致难以理解代码。此外,any 线程可以释放信号量这一事实有时是不受欢迎的。而是使用更多面向对象/更高级别的同步原语/抽象,例如“条件变量”和“监视器”。

【讨论】:

绝对是最彻底的答案。有例子会很有帮助。例如,信号量可以锁定客户主文件以读取共享文件,还是将所有人锁定在夜间更新?信号量可以锁定客户编号进行独占更新,还是锁定客户编号进行共享阅读?等等。或者应用程序应该创建自己的信号量文件而不使用系统信号量? “任何线程都可以释放信号量的事实有时是不受欢迎的” 不同线程会减少信号量的事实是信号量的定义特征。这就是信号量与互斥锁/锁的区别。 互斥锁和锁是一样的(这个词在Java中不常用)——这就是很多文章没有解释清楚的地方【参考方案4】:

看看 John Kopplin 的 Multithreading Tutorial。

线程间同步一节中,他解释了事件、锁、互斥量、信号量、等待定时器的区别

mutex 一次只能由一个线程拥有,使线程能够 协调对共享资源的互斥访问

关键部分对象提供类似的同步 由互斥对象提供,除了临界区对象可以是 仅由单个进程的线程使用

互斥体临界区之间的另一个区别是,如果 临界区对象当前由另一个线程拥有, EnterCriticalSection() 无限期地等待所有权,而 WaitForSingleObject(),与互斥锁一起使用,允许您 指定超时

信号量维持一个介于零和某个最大值之间的计数, 限制同时访问的线程数 共享资源。

【讨论】:

【参考方案5】:

我会尝试用例子来说明它:

锁定:您将使用lock 的一个示例是共享字典,其中添加了项目(必须具有唯一键)。 锁将确保一个线程不会进入检查字典中项目的代码机制,而另一个线程(即在关键部分中)已经通过了此检查并正在添加项目。如果另一个线程试图输入一个锁定的代码,它将等待(被阻塞)直到对象被释放。

private static readonly Object obj = new Object();

lock (obj) //after object is locked no thread can come in and insert item into dictionary on a different thread right before other thread passed the check...

    if (!sharedDict.ContainsKey(key))
    
        sharedDict.Add(item);
    

信号量: 假设您有一个连接池,那么单个线程可能会通过等待信号量获得连接来保留池中的一个元素。然后它使用连接,当工作完成时通过释放信号量来释放连接。

Code example that I love is one of bouncer given by @Patric - 就这样:

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace TheNightclub

    public class Program
    
        public static Semaphore Bouncer  get; set; 

        public static void Main(string[] args)
        
            // Create the semaphore with 3 slots, where 3 are available.
            Bouncer = new Semaphore(3, 3);

            // Open the nightclub.
            OpenNightclub();
        

        public static void OpenNightclub()
        
            for (int i = 1; i <= 50; i++)
            
                // Let each guest enter on an own thread.
                Thread thread = new Thread(new ParameterizedThreadStart(Guest));
                thread.Start(i);
            
        

        public static void Guest(object args)
        
            // Wait to enter the nightclub (a semaphore to be released).
            Console.WriteLine("Guest 0 is waiting to entering nightclub.", args);
            Bouncer.WaitOne();          

            // Do some dancing.
            Console.WriteLine("Guest 0 is doing some dancing.", args);
            Thread.Sleep(500);

            // Let one guest out (release one semaphore).
            Console.WriteLine("Guest 0 is leaving the nightclub.", args);
            Bouncer.Release(1);
        
    

Mutex 它几乎是Semaphore(1,1) 并且经常在全球范围内使用(应用程序范围内可以说lock 更合适)。从全局可访问列表中删除节点时,将使用全局Mutex(在删除节点时,您希望另一个线程做某事的最后一件事)。当您获取Mutex 时,如果不同的线程尝试获取相同的Mutex,它将进入休眠状态,直到获取Mutex 的同一线程释放它。

Good example on creating global mutex is by @deepee

class SingleGlobalInstance : IDisposable

    public bool hasHandle = false;
    Mutex mutex;

    private void InitMutex()
    
        string appGuid = ((GuidAttribute)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(GuidAttribute), false).GetValue(0)).Value.ToString();
        string mutexId = string.Format("Global\\0", appGuid);
        mutex = new Mutex(false, mutexId);

        var allowEveryoneRule = new MutexAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null), MutexRights.FullControl, AccessControlType.Allow);
        var securitySettings = new MutexSecurity();
        securitySettings.AddAccessRule(allowEveryoneRule);
        mutex.SetAccessControl(securitySettings);
    

    public SingleGlobalInstance(int timeOut)
    
        InitMutex();
        try
        
            if(timeOut < 0)
                hasHandle = mutex.WaitOne(Timeout.Infinite, false);
            else
                hasHandle = mutex.WaitOne(timeOut, false);

            if (hasHandle == false)
                throw new TimeoutException("Timeout waiting for exclusive access on SingleInstance");
        
        catch (AbandonedMutexException)
        
            hasHandle = true;
        
    


    public void Dispose()
    
        if (mutex != null)
        
            if (hasHandle)
                mutex.ReleaseMutex();
            mutex.Dispose();
        
    

然后使用like:

using (new SingleGlobalInstance(1000)) //1000ms timeout on global lock

    //Only 1 of these runs at a time
    GlobalNodeList.Remove(node)

希望这可以为您节省一些时间。

【讨论】:

【参考方案6】:

***在differences between Semaphores and Mutexes 上有一个很棒的部分:

互斥体本质上与二进制信号量相同,并且 有时使用相同的基本实现。之间的区别 他们是:

互斥体有一个所有者的概念,即进程 锁定了互斥锁。只有锁定互斥锁的进程才能 解锁它。相反,信号量没有所有者的概念。任何 进程可以解锁信号量。

与信号量不同,互斥锁提供 优先反转安全。由于互斥体知道它的当前所有者,它 可以提升所有者的优先级,只要 更高优先级的任务开始在互斥体上等待。

互斥锁还提供 删除安全,其中持有互斥锁的进程不能 不小心删除了。信号量不提供此功能。

【讨论】:

【参考方案7】:

我的理解是,互斥锁只能在单个进程中使用,但可以跨多个线程使用,而信号量可以跨多个进程使用,并且可以跨对应的线程集使用。

此外,互斥锁是二进制的(它要么被锁定,要么被解锁),而信号量具有计数的概念,或者一个包含多个锁定和解锁请求的队列。

有人可以验证我的解释吗?我是在 Linux 的上下文中说的,特别是 Red Hat Enterprise Linux (RHEL) 版本 6,它使用内核 2.6.32。

【讨论】:

现在这在不同的操作系统中可能会有所不同,但在 Windows 中,一个 Mutex 可以被多个进程使用,至少 .net Mutex 对象.. ***.com/questions/9389730/… "同一进程内或其他进程内的线程可以共享互斥锁。"所以没有一个互斥锁不能是特定于进程的。【参考方案8】:

在 Linux 变体上使用 C 编程作为示例的基本案例。

锁定:

• 通常是一个非常简单的构造二进制操作,无论是锁定还是解锁

• 没有线程所有权、优先级、排序等概念。

• 通常是自旋锁,其中线程不断检查锁的可用性。

• 通常依赖于原子操作,例如测试和设置、比较和交换、获取和添加等。

• 原子操作通常需要硬件支持。

文件锁定:

• 通常用于协调通过多个进程对文件的访问。

• 多个进程可以持有读锁,但是当任何单个进程持有写锁时,不允许其他进程获取读锁或写锁。

• 示例:flock、fcntl 等。

互斥:

• Mutex 函数调用通常在内核空间中工作并导致系统调用。

• 它使用所有权的概念。只有当前持有互斥锁的线程才能解锁它。

• 互斥锁不是递归的(例外:PTHREAD_MUTEX_RECURSIVE)。

• 通常与条件变量结合使用并作为参数传递给例如pthread_cond_signal、pthread_cond_wait 等

• 一些 UNIX 系统允许多个进程使用互斥锁,尽管这可能并非在所有系统上都强制执行。

信号量:

• 这是一个内核维护的整数,其值不允许低于零。

• 可用于同步进程。

• 信号量的值可以设置为大于 1 的值,在这种情况下,该值通常表示可用资源的数量。

• 值限制为 1 和 0 的信号量称为二进制信号量。

【讨论】:

【参考方案9】:

锁、互斥、信号量

这是一个普遍的愿景。细节取决于真实的语言实现

lock - 线程同步工具。当线程获得锁时,它成为能够执行代码块的单个线程。所有其他线程都被阻塞。只有拥有锁的线程才能解锁它

mutex - 互斥锁。它是一种锁。在某些语言上它是进程间机制,在某些语言上它是lock 的同义词。例如Java在synchronisedjava.util.concurrent.locks.Lock中使用lock

semaphore - 允许多个线程访问共享资源。你可以发现mutex也可以通过semaphore实现。它是一个独立的对象,管理对共享资源的访问。您会发现任何线程都可以signal 并解除阻塞。也用于发信号

[ios lock, mutex, semaphore]

【讨论】:

【参考方案10】:

Supporting ownershipmaximum number of processes share lockmaximum number of allowed processes/threads in critical section 是确定通用名称为lock 的并发对象的名称/类型的三个主要因素。由于这些因子的值是二元的(有两种状态),我们可以将它们总结在一个 3*8 的类真值表中。

X(支持所有权?):否(0)/是(1) Y(#sharing 进程):> 1 (∞) / 1 Z(CA 中的#processes/threads):> 1 (∞) / 1
  X   Y   Z          Name
 --- --- --- ------------------------
  0   ∞   ∞   Semaphore              
  0   ∞   1   Binary Semaphore       
  0   1   ∞   SemaphoreSlim          
  0   1   1   Binary SemaphoreSlim(?)
  1   ∞   ∞   Recursive-Mutex(?)     
  1   ∞   1   Mutex                  
  1   1   ∞   N/A(?)                 
  1   1   1   Lock/Monitor           

请随意编辑或扩展此表,我已将其发布为 ascii 表以供编辑:)

【讨论】:

以上是关于锁、互斥量和信号量有啥区别?的主要内容,如果未能解决你的问题,请参考以下文章

信号量与互斥锁区别

Java 中的互斥量和信号量是啥?主要区别是啥?

多线程的同步和互斥有啥区别

互斥量和二进制信号量之间的实际区别

使用互斥量和信号量的屏障实现

理解互斥量和信号量