锁,互斥,信号量……有什么区别?


Answers:


532

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

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

旗语不相同,为互斥,但允许x个线程进入的,这可以被用于例如限制CPU的数量,IO或RAM在同一时间运行密集型任务。

有关互斥量和信号量之间差异的更多详细信息,请阅读此处

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


2
@mertinan我不能说我从未听说过,但这就是Wikipedia所说的“锁(数据库),(相对短暂的)锁在像索引一样的系统数据结构上”
Peter

2
监视器允许等待特定条件(例如,释放锁定时),即“监视器”。
Dzmitry Lazerka

25
信号量与互斥量不同。它们的用法非常不同,并且具有不同的属性(即关于所有权)。有关详细信息,请参见例如barrgroup.com/Embedded-Systems/How-To/RTOS-Mutex-Semaphore
nanoquack,2014年

3
如果您认为@nanoquack有误导性或不正确,请随时编辑我的答案。
彼得

3
为了更清楚地区分互斥量和信号量,在nanoquack的链接中,关键段落是“ 正确使用信号量是为了将信号从一个任务传递到另一任务。互斥量应按每个对象始终按此顺序进行并释放。使用它保护的共享资源的任务。相比之下,使用信号量的任务要么发出信号,要么等待,而不是同时发出信号。
ToolmakerSteve

117

关于这些词有很多误解。

这来自上一篇文章(https://stackoverflow.com/a/24582076/3163691),在这里非常适合:

1)关键部分 =用户对象,用于允许从一个进程中的许多其他线程中仅执行一个活动线程。其他未选择的线程(@获取该对象)进入睡眠状态

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

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

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

3)计数信号量(aka信号量) =内核对象,用于允许执行许多其他活动线程组。其他未选择的线程(@获取该对象)进入睡眠状态

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

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

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

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

繁忙等待=连续测试变量,直到出现某个值。

最后:

自旋锁(又名自旋锁) = 使用忙等待的。(通过xchg或类似的原子操作获得)。

[没有线程休眠,通常仅在内核级别使用。对于用户级代码无效)。

最后一点,我不确定,但是我可以打赌,上面的前三个同步对象(#1,#2和#3)将这个简单的野兽(#4)用作实现的一部分,这可是一笔大钱。

祝你有美好的一天!。

参考文献:

-Qing Li和Caroline Yao(CMP Books)的嵌入式系统实时概念。

-现代操作系统(第3版),作者:安德鲁·塔南鲍姆(Pearson Education International)。

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

另外,您可以看一下:https : //stackoverflow.com/a/24586803/3163691


1
实际上,关键部分不是内核对象,因此更轻量且无法跨进程同步。
弗拉迪斯拉夫斯·布拉科夫斯(Fladislavs Burakovs),2015年

2
// @弗拉迪斯拉夫斯·布拉科夫斯:你说得对!原谅我的编辑。为了一致性,我将对其进行修复。
幻想

如要在互斥量和信号量之间进行更清晰的区分,如nanoquack在其他地方所提到的,请参见barrgroup.com/Embedded-Systems/How-To/RTOS-Mutex-Semaphore-关键段落是“ 信号量的正确使用是为了从一项任务发出信号互斥体是指使用它保护的共享资源的每个任务始终按此顺序获取和释放互斥锁。相比之下,使用信号量的任务要么发出信号,要么等待,而不是同时发出信号。
ToolmakerSteve

再猜想其他锁定机制建立在[低效]自旋锁上:不太可能;AFAIK只需要一些原子操作以及睡眠队列。即使在内核中需要自旋锁地方,现代解决方案也可以将其影响最小化,如Wikipedia-Spinlock-Alternatives-..使用一种称为“自适应互斥锁”的混合方法。该想法是在尝试访问由锁锁定的资源时使用自旋锁。当前运行的线程,但睡眠如果线程当前没有运行(后者是总是在单处理器系统中的情况。)。
ToolmakerSteve

@ToolmakerSteve,我敢于为试图将线程ID“插入”“睡眠队列”中的“冲突”问题提供一个没有“自旋锁”的“解决方案”。无论如何,Wikipedia文本得出的结论是在实现中使用了自旋锁!
fante

27

使用(i)仅锁,(ii)仅信号灯,...,或(iii)两者的组合可以解决大多数问题!正如您可能已经发现的那样,它们非常相似:两者都防止争用情况,都具有acquire()/ release()操作,都导致零个或多个线程被阻塞/可疑...确实,关键的区别仅在于它们如何锁定和解锁

  • (或互斥)具有两个状态(0或1)。它可以被解锁锁定。它们通常用于确保一次只有一个线程进入关键部分。
  • 一个信号有很多州(0,1,2,...)。它可以被锁定(状态0)或解锁(状态1,2,3,...)。通常将一个或多个信号灯一起使用,以确保在某些资源的单位数量已经/尚未达到特定值时(通过递减至该值或递增至该值),仅一个线程正好进入关键部分)。

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

对于状态0的锁,如果以前调用的同一线程acquire()现在调用release,则释放成功。如果不同的线程尝试了此操作-取决于执行情况/库会发生什么情况(通常是尝试被忽略或抛出错误)。对于状态0的信号量,任何线程都可以调用release,它将成功(无论先前使用哪个线程将信号量置于状态0)。

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


引起很多困惑的是,实际上它们有很多差异此高级定义的。

要考虑的重要变化

  • 应该采取什么acquire()/ release()叫?- [不定大规模 ]
  • 您的锁/信号量是否使用“队列”或“集合”来记住等待的线程?
  • 可以将您的锁/信号灯与其他进程的线程共享吗?
  • 您的锁是“可重入”吗?-[通常是]。
  • 您的锁是“阻止/非阻止”吗?-[通常非阻塞用作阻塞锁(也称为自旋锁)导致繁忙的等待时间]。
  • 您如何确保操作是“原子的”?

这些取决于您的书/讲师/语言/图书馆/环境。
这是一个快速导览,指出某些语言如何回答这些细节。


C,C ++(pthreads

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

python(threading.py

  • threading.RLock)是大多相同的C / C ++ pthread_mutex_t秒。两者都是可重入的。这意味着它们只能由锁定它的同一线程来解锁。在这种情况下,sem_t信号量,threading.Semaphore信号量和theading.Lock不能重入 -因为任何线程都可以执行解锁/关闭信号量的情况。
  • 一个互斥体是相同的一个锁(该术语并不在python经常使用)。
  • 旗语threading.Semaphore)是大多相同sem_t。尽管使用sem_t了,但线程ID队列用于记住线程被锁定时尝试被阻塞的顺序。当线程解锁信号量时,队列中的第一个线程(如果有)被选择为新所有者。线程标识符从队列中移出,并且信号量再次被锁定。但是,使用时threading.Semaphore,将使用集合而不是队列,因此不会存储线程被阻塞的顺序- 可以选择集合中的任何线程作为下一个所有者。

Java(java.util.concurrent

  • java.util.concurrent.ReentrantLock)是大多相同的C / C ++ pthread_mutex_t的,和Python的threading.RLock,因为它也实现了一个可重入锁。在Java中,由于JVM充当中介,因此在进程之间共享锁比较困难。如果线程试图解锁它不拥有的锁,IllegalMonitorStateException则会抛出一个。
  • 互斥是相同的一个锁(该术语不经常使用在Java中)。
  • 旗语java.util.concurrent.Semaphore)是大多相同sem_tthreading.Semaphore。Java信号量的构造函数接受一个公平布尔值参数,该参数控制是使用集合(false)还是使用队列(true)来存储等待线程。

从理论上讲,信号灯经常被讨论,但是实际上,信号灯的使用并不多。信号量仅保存一个整数的状态,因此通常它相当不灵活,并且一次需要很多-导致难以理解代码。同样,有时不希望任何线程都可以释放信号量。而是使用更多的面向对象/更高级别的同步原语/抽象,例如“条件变量”和“监视器”。


22

看看John Kopplin的《多线程教程》

在“ 线程之间同步 ”部分中,他解释了事件,锁,互斥,信号量,等待计时器之间的差异。

一个互斥体只能由一个线程在同一时间内资,使线程来协调对共享资源的互斥访问

关键部分对象提供的同步与互斥对象提供的同步类似,不同之处在于关键部分对象只能由单个进程的线程使用

互斥锁关键节之间的另一个区别是,如果关键节对象当前由另一个线程 EnterCriticalSection()拥有,则无限期地等待所有权,而 WaitForSingleObject()与互斥锁一起使用的,则可以指定超时

旗语保持和一些最大值零之间的计数,从而限制了被同时访问共享资源的线程数。


15

我将尝试通过示例进行介绍:

锁:您将使用的一个示例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);
    }
}

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

我喜欢的代码示例是@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);
        }
    }
}

互斥Semaphore(1,1)量(Mutex)它是非常多的,并且经常在全球范围内使用(应用范围广,否则可以说lock是更合适的)。Mutex从全局可访问列表中删除节点时,将使用全局(最后一件事是您希望另一个线程在删除节点时执行某些操作)。当您获取Mutex是否有其他线程尝试获取Mutex相同的线程时,它将进入休眠状态,直到获取该线程的SAME线程Mutex释放它为止。

创建全局互斥锁的好例子是@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();
        }
    }
}

然后使用像:

using (new SingleGlobalInstance(1000)) //1000ms timeout on global lock
{
    //Only 1 of these runs at a time
    GlobalNodeList.Remove(node)
}

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


8

Wikipedia 专门介绍了信号量和互斥量之间区别

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

互斥锁具有所有者的概念,这是锁定互斥锁的过程。只有锁定互斥锁的进程才能对其进行解锁。相反,信号量没有所有者的概念。任何进程都可以解锁信号量。

与信号量不同,互斥锁提供优先级反转安全性。由于互斥锁知道其当前所有者,因此,只要优先级较高的任务开始等待互斥锁,便可以提升所有者的优先级。

互斥对象还提供了删除安全性,其中包含互斥对象的进程不会被意外删除。信号量不提供此功能。


5

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

同样,互斥锁是二进制的(它是锁定的还是未锁定的),而信号量则具有计数的概念,或者具有多个锁定和解锁请求的队列。

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


3
现在,这可能是在不同的操作系统,但Windows中的互斥锁可以被多个进程使用的不同至少.NET Mutex对象..
彼得·

2
“stackoverflow.com/questions/9389730/… ”同一进程或其他进程内的线程可以共享互斥体。因此,互斥锁一定不能特定于进程。
彼得

3

以Linux变体上的C编程为例。

锁:

•通常一个非常简单的构造二进制文件在操作中处于锁定或未锁定状态

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

•通常为自旋锁,线程在该自旋锁中连续检查锁的可用性。

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

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

文件锁:

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

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

•示例:flock,fcntl等。

互斥体:

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

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

•互斥体不是递归的(异常:PTHREAD_MUTEX_RECURSIVE)。

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

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

信号:

•这是内核维护的整数,其值不允许降至零以下。

•可用于同步进程。

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

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


0

Supporting ownershipmaximum number of processes share lock并且maximum number of allowed processes/threads in critical section是决定用的通用名称的名称/类型的并发对象的三个主要因素lock。由于这些因子的值是二进制的(具有两种状态),因此我们可以在3 * 8的类似真值的表中对其进行汇总。

  • X(支持所有权吗?):否(0)/是(1)
  • Y(共享过程数):> 1(∞)/ 1
  • Z(CA中的进程数/线程数):> 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表:)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.