首页 文章

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

提问于
浏览
317

我听说这些词与并发编程有关,但它们之间的区别是什么?

8 回答

  • 18

    大多数问题都可以使用(i)锁定,(ii)只是信号量,......,或(iii)两者的组合来解决!正如您可能已经发现的那样,它们非常相似:两者都阻止race conditions,两者都有 acquire() / release() 操作,都会导致零个或多个线程被阻塞/怀疑......真的,关键的区别仅在于 how they lock and unlock .

    • lock (或 mutex )有两种状态(0或1) . 它可以是解锁的或锁定的 . 它们通常用于确保一次只有一个线程进入临界区 .

    • semaphore 有许多状态(0,1,2,...) . 它可以被锁定(状态0)或解锁(状态1,2,3,......) . 一个或多个信号量通常一起使用,以确保当某个资源的单元数已经/未达到特定值时(通过倒计数到该值或计算到该值),只有一个线程准确地进入临界区域) .

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

    对于处于状态0的锁,如果具有相同的线程以前称为 acquire() ,现在调用release,然后发布成功 . 如果一个不同的线程尝试了这个 - 它是由实现/库决定发生的事情(通常忽略尝试或抛出错误) . 对于状态0中的信号量,任何线程都可以调用release并且它将成功(无论先前使用哪个线程获取将信号量置于状态0) .

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


    引起很多困惑的是,在实践中它们是这个高级定义的 many variations .

    Important variations to consider

    • 应该调用 acquire() / release() 怎么办? - [变化massively]

    • 你的锁/信号量是否使用"queue"或"set"来记住等待的线程?

    • 您的锁/信号量可以与其他进程的线程共享吗?

    • 你的锁"reentrant"? - [通常是] .

    • 你的锁"blocking/non-blocking"? - [通常非阻塞用作阻塞锁(又称自旋锁)导致忙碌等待] .

    • 如何确保操作"atomic"?

    这些取决于您的书/讲师/语言/图书馆/环境 .
    这是一个快速浏览,注意一些语言如何回答这些细节 .


    C,C(pthreads)

    • mutex 通过 pthread_mutex_t 实现 . 默认情况下,它们不能与任何其他进程共享( PTHREAD_PROCESS_PRIVATE ),但是互斥锁具有名为pshared的属性 . 设置后,进程之间共享互斥锁( PTHREAD_PROCESS_SHARED ) .

    • lock 与互斥锁相同 .

    • semaphore 通过 sem_t 实现 . 与互斥锁类似,信号量可以在许多进程的threasds之间共享,也可以保持对一个进程的线程的私有 . 这取决于提供给 sem_init 的pshared参数 .

    python(threading.py)

    • A lockthreading.RLock )与C / C pthread_mutex_t s大致相同 . 两者都是可重入的 . 这意味着它们只能通过锁定它的同一个线程解锁 . 在这种情况下, sem_t 信号量, threading.Semaphore 信号量和 theading.Lock 锁不是可重入的 - 因为任何线程都可以执行解锁锁定/关闭信号量的情况 .

    • mutex 与锁相同(该术语在python中不经常使用) .

    • A semaphorethreading.Semaphore )与 sem_t 大致相同 . 虽然使用 sem_t ,但是线程ID队列用于记住在锁定线程时尝试锁定线程时线程被阻塞的顺序 . 当线程解锁信号量时,队列中的第一个线程(如果有的话)被选为新的所有者 . 线程标识符从队列中取出,信号量再次被锁定 . 但是,使用 threading.Semaphore ,将使用集合而不是队列,因此不会存储线程被阻塞的顺序 - 集合中的任何线程都可以选择为下一个所有者 .

    Java(java.util.concurrent)

    • lockjava.util.concurrent.ReentrantLock )与C / C pthread_mutex_t 's, and Python' s threading.RLock 大致相同,因为它还实现了可重入锁定 . 由于JVM充当中介,因此在Java中共享锁之间的锁是更难的 . 如果线程试图解锁它不拥有的锁,则抛出 IllegalMonitorStateException .

    • mutex 与锁相同(该术语在Java中不经常使用) .

    • semaphorejava.util.concurrent.Semaphore )与 sem_tthreading.Semaphore 大致相同 . Java信号量的构造函数接受一个公平布尔参数,该参数控制是使用set(false)还是使用队列(true)来存储等待的线程 .


    从理论上讲,信号量经常被讨论,但在实践中,信号量并没有被如此多地使用 . 信号量只保持一个整数的状态,因此通常它是相当不灵活的,并且需要立即执行许多 - 导致难以理解代码 . 此外,任何线程都可以释放信号量的事实有时是不受欢迎的 . 使用更多面向对象/更高级别的同步原语/抽象,例如"condition variables"和"monitors" .

  • 3

    我将尝试用例子来介绍它:

    Lock: 您将使用 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);
        }
    }
    

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

    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 的SAME线程释放它 .

    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();
            }
        }
    }
    

    然后使用像:

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

    希望这能为您节省一些时间 .

  • 396

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

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

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

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

  • 85

    关于这些词有很多误解 .

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

    1) Critical Section =用于允许从许多其他人 within one process 执行 one active thread 的用户对象 . 其他未选择的线程将放入 sleep .

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

    2) Mutex Semaphore (aka Mutex) =用于允许从许多其他人执行 one active thread 的内核对象, among different processes . 其他未选择的线程将被放入 sleep . 此对象支持线程所有权,线程终止通知,递归(来自同一线程的多个'acquire'调用)和'priority inversion avoidance' .

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

    3) Counting Semaphore (aka Semaphore) =用于允许从许多其他人执行 a group of active threads 的内核对象 . 其他未选择的线程将放入 sleep .

    [进程间功能然而不是非常安全,因为它缺少以下'互斥'属性:线程终止通知,递归?,'优先级反转避免'等等] .

    4) And now, talking about 'spinlocks', first some definitions:

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

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

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

    最后:

    Spin-lock (aka Spinlock) = lock ,它使用 busy waiting . ( lock 的获取由 xchg 或类似的 atomic operations 完成) .

    [无线程休眠,主要仅在内核级别使用 . 对用户级代码的影响很大] .

    作为最后的评论,我不确定但是我可以打赌你们上面的前3个同步对象(#1,#2和#3)使用这个简单的野兽(#4)作为其实现的一部分 .

    祝你有美好的一天! .

    References:

    • 清李与Caroline Yao(CMP Books)的嵌入式系统的实时概念 .

    • Andrew Tanenbaum(Pearson Education International)的现代操作系统(第3版) .

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

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

  • 12

    看看约翰·科普林的Multithreading Tutorial .

    Synchronization Between Threads 部分,他解释了事件,锁,互斥,信号量,等待计时器之间的差异

    一个互斥锁一次只能由一个线程拥有,使线程能够协调对共享资源的互斥访问 . 关键部分对象提供类似于互斥对象提供的同步,除了关键部分对象只能由线程使用单个进程的另一个区别在于互斥锁和临界区之间的另一个区别是,如果临界区对象当前由另一个线程拥有,则EnterCriticalSection()将无限期地等待所有权,而与互斥锁一起使用的WaitForSingleObject()允许您指定超时信号量维持一个介于零和某个最大值之间的计数,从而限制同时访问共享资源的线程数 .

  • 14

    维基百科在differences between Semaphores and Mutexes上有一个很棒的部分:

    互斥量与二进制信号量基本相同,有时使用相同的基本实现 . 它们之间的区别在于:互斥锁具有所有者的概念,即锁定互斥锁的过程 . 只有锁定互斥锁的进程才能解锁 . 相比之下,信号量没有所有者的概念 . 任何进程都可以解锁信号量 . 与信号量不同,互斥体提供优先级反转安全性 . 由于互斥锁知道其当前所有者,因此只要优先级较高的任务开始等待互斥锁,就可以提升所有者的优先级 . 互斥锁还提供删除安全性,其中不能意外删除持有互斥锁的进程 . 信号量不提供此信息 .

  • 6

    我的理解是,互斥体只能在单个进程中使用,但是在其多个线程中使用,而信号量可以跨多个进程使用,并跨越其相应的线程集 .

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

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

  • 1

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

    Lock:

    •通常一个非常简单的构造二进制文件在操作中被锁定或解锁

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

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

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

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

    File Locks:

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

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

    •示例:flock,fcntl等 .

    Mutex:

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

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

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

    •通常用于与条件变量关联,并作为参数传递给例如pthread_cond_signal,pthread_cond_wait等

    •某些UNIX系统允许多个进程使用互斥锁,但可能并未在所有系统上强制执行 .

    Semaphore:

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

    •它可用于同步进程 .

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

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

相关问题