什么是信号量?

426

信号量是一个经常被用来解决多线程问题的编程概念。我的问题是:

信号量是什么,如何使用?


25
一个布尔标志,其值基于整数计数器是否达到了其指定的上限。 - Sam
15个回答

442

将信号量视为夜店的保安。一次只允许进入一定数量的人。如果夜店已满,则不允许任何人进入,但是只要有一个人离开,另一个人就可以进入。

这只是一种限制特定资源消费者数量的方法。例如,为了限制应用程序中对数据库的同时调用数量。

以下是C#中非常易懂的示例 :-)

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

7
如果像夜店保安一样控制,应该按顺序让客人进入,但我尝试后发现是随机的。例如,客人40先于客人39进入了。我们有什么办法可以控制这个吗? - TNA
2
@TNA:是的,这与此示例中启动新线程的方式有关,而不是答案的范围。 - Patrik Svensson
7
保安类比确实很史诗级,但有趣的是它已经被使用过了:http://www.albahari.com/threading/part2.aspx#_Semaphore - Igor Brejc
在分布式系统中,信号量提供了什么价值? - csandreas1
它是否仅限于线程,还是也可以与进程一起使用? - RonTLV
这是一个很好的示例,可以教育人们如何使用信号量。我很好奇社区是否可以分享测试此功能的最佳实践示例或测试示例。 - cafed00d

255

这篇文章Mutexes and Semaphores Demystified由Michael Barr撰写,是对互斥锁和信号量之间的区别以及何时应该使用它们进行了简短介绍。我在此摘录了几个关键段落。

关键点在于,互斥锁应该用于保护共享资源,而信号量应该用于信号传递。通常不应该使用信号量来保护共享资源,也不应该使用互斥锁进行信号传递。例如,在使用信号量来保护共享资源方面存在问题,比如使用这种方法可能会导致难以诊断的错误。

虽然互斥锁和信号量在实现上有些相似之处,但它们应该始终以不同的方式使用。

对于置顶问题的最常见(但仍不正确)答案是,互斥锁和信号量非常相似,唯一的显著区别是信号量可以计数高于1。几乎所有工程师似乎都正确地理解,互斥锁是用于通过确保代码的临界部分内的互斥排斥来保护共享资源的二进制标志。但是,当被问及如何使用“计数信号量”时,大多数工程师(只是在信心程度上有所不同)表达了一些文本书的观点,即这些信号量用于保护几个等效资源。

...

此时,我们可以使用一个有趣的类比来解释保护共享资源的想法——浴室钥匙。如果商店只有一个浴室,那么一把钥匙就足以保护这个资源,防止多人同时使用。
如果有多个浴室,则可能会倾向于将它们锁成一样,制作多把钥匙——这类似于错误使用信号量。一旦你拿到了钥匙,你实际上不知道哪个浴室可用,如果你走上这条路,你可能最终会使用互斥锁来提供信息,并确保你不会使用已经被占用的浴室。
信号量是保护几乎相同的多个资源的错误工具,但这是许多人所认为和使用的方式。而保安员的类比则完全不同——没有几个相同类型的资源,而是一个资源可以接受多个同时用户。我想信号量可以在这种情况下使用,但很少有现实世界的情况真正符合这个类比——更常见的是有几个相同类型但仍然是单独的资源,例如浴室,不能用这种方式。

...

信号量的正确使用是用于在一个任务和另一个任务之间发出信号。互斥锁则是被每个使用它所保护的共享资源的任务按顺序获取和释放。与此相反,使用信号量的任务只会发送或等待,不会同时进行这两个操作。例如,任务1可能包含代码,当按下“电源”按钮时,将发布(即发出或递增)特定的信号量,而唤醒显示屏的任务2则等待同一信号量。在这种情况下,一个任务是事件信号的生产者,另一个任务是消费者。

...

这里提出了一个重要观点,即互斥锁对实时操作系统有不良影响,会导致优先级反转,即较不重要的任务可能在更重要的任务之前执行,因为它们共享资源。简而言之,当较低优先级的任务使用互斥锁获取资源A,然后尝试获取B时,由于B不可用而被暂停。在等待期间,一个更高优先级的任务需要A,但A已经被占用,并且是由于等待B的进程占用的。有很多解决方法,但通常通过修改互斥锁和任务管理器来解决。在这些情况下,互斥锁比二进制信号量复杂得多,如果在这种情况下使用信号量,将导致优先级反转,因为任务管理器不知道优先级反转并且无法进行纠正。

...

历史原因导致了现代对于互斥锁和信号量之间普遍的混淆。这可以追溯到1974年Djikstra发明信号量(本文中大写"S")。在此之前,计算机科学家所知道的所有可中断任务同步和信号机制都无法有效地扩展以用于超过两个任务。Dijkstra革命性、安全和可扩展的信号量被应用于关键部分保护和信号传递。从那时起,混淆就开始了。
然而,在出现基于优先级的抢占式RTOS(例如VRTX,约1980年)、发表关于RMA以及由优先级反转引起的问题的学术论文以及1990年有关优先级继承协议的论文后,操作系统开发人员后来意识到互斥锁必须不仅仅是带有二进制计数器的信号量。
互斥锁:资源共享
信号量:信号传递
不要在没有仔细考虑副作用的情况下将它们混用。

12
请看这份斯坦福并发性PDF文档,翻到第8页。上面的解释会更有意义。 https://see.stanford.edu/materials/icsppcs107/23-Concurrency-Examples.pdf - Kris Subramanian
4
《信号量小书》是一本关于这些问题的有价值的读物。 - G. Bach
@KrisSubramanian,谢谢提供链接。但是,该文档讨论了信号量,没有提及互斥锁。不过,你的意思是说示例中的共享缓冲区可以使用互斥锁来保护?而不是使用两个信号量emptyBuffers和fullBuffers吗? - talekeDskobeDa
1
@Pramod 确实如此。该链接没有添加任何互斥相关的注释。我添加了该链接,以便 Semaphore 方面的东西对 SO 读者变得清晰。 :)有趣的是,在这种情况下,缓冲区在没有锁的情况下被使用,因为它是以顺序和循环格式访问的。即写入器将写入 0 并通知读取器从 0 读取。如果读取器不从 0 读取并通知写入器,则写入器将阻塞。因此,无需使用互斥锁来锁定公共资源。这与上面给出的浴室类比不同。 - Kris Subramanian
@Kris Subramanian:文档不错,但有一些小错误:第3页开始说明“每个锁定信号量的线程都应该小心地解锁它”- 它们可以被任何线程解锁。如果您在同一线程中执行此操作,则只是将其用作“破损的互斥锁”。 “破损”是因为它仍然可能会被其他线程无意中解锁 - 错误会发生 - 并破坏您的逻辑。还是很好的文档。 - Rustam A.

75

Mutex:独占式成员访问资源

Semaphore:n-成员访问资源

也就是说,Mutex可用于同步对计数器、文件、数据库等的访问。

Semaphore可以做同样的事情,但支持固定数量的同时调用者。例如,我可以将我的数据库调用包裹在semaphore(3)中,以便我的多线程应用程序最多使用三个并发连接访问数据库。所有尝试都将被阻塞,直到其中一个三个插槽之一被打开。它们使得像执行简单限流这样的操作变得非常容易。


24
据Richard W. Stevens所说,mutex实际上是一个二进制信号量,只有两个可能的值:0和1。 - Qiang Xu
32
根据William Stallings的《操作系统内部原理与设计原理》一书中@QiangXu所述,二进制信号量在一个非常重要的方面与互斥锁不同。引用他的话:“互斥锁和二进制信号量之间的一个关键区别是锁定互斥锁的进程必须是解锁它的进程。相比之下,一个进程可以锁定二进制信号量,而另一个进程可以解锁它。” - Electric Coffee
5
评论一个过时的帖子可能有风险,但这并不正确。正如@AdamDavis在上面提到的,Semaphore 不应该(必须不)用于n成员访问资源 - 这仍然应该通过使用 Mutex 来完成。考虑在咖啡店中使用多个人等待访问或者有多个类似钥匙的浴室的情况下进行比喻。相反,Semaphore 应该用于任务之间的信号传递。 - cspider

33
考虑一个能容纳3人(包括司机)的出租车。因此,一个信号量(semaphore)一次只允许5个人进入车内(3后座+2前座)。而互斥锁(mutex)只允许一个人坐在车的一个座位上。
因此,互斥锁是为一个资源(如操作系统线程)提供独占性访问,而信号量则是允许同时访问n个资源。

18

@Craig:

信号量是一种锁定资源的方式,确保在执行一段代码时,只有这段代码可以访问该资源。这可以防止两个线程同时访问一个资源,从而导致问题。

信号量不仅限于单个线程。信号量可以配置为允许固定数量的线程访问资源。


7
这是一篇评论,而不是一个答案。 - kaspersky
13
是的,但我想在 Stack Overflow 添加评论之前就写下了这篇文章。或者我没有写过,不太记得了。不过这次我在评论中回答了 :-)。 - Mats Fredriksson

17

信号量也可以被用作一种信号机制,比如如果你有多个进程将数据入队到一个队列中,但只有一个任务从队列中消耗数据。如果你不想让消耗任务不断地轮询可用数据,那么你可以使用信号量。

在这种情况下,信号量不是作为互斥机制而被使用,而是作为一种信号机制。即消耗任务在等待信号量,而生产任务在发布信号量。

这样一来,只有当队列中有数据需要出队时,消耗任务才会运行。


13
构建并发程序的两个基本概念是同步和互斥。我们将看到这两种类型的锁(信号量通常是一种锁定机制)如何帮助我们实现同步和互斥。
信号量是一种编程构造,通过实现同步和互斥来帮助我们实现并发。信号量有两种类型:二进制和计数。
信号量由两个部分组成:计数器和等待访问特定资源的任务列表。信号量执行两个操作:等待(P)[类似于获取锁]和释放(V)[类似于释放锁] - 这是可以在信号量上执行的仅有的两个操作。在二进制信号量中,计数器逻辑上在0和1之间变化。您可以将其视为具有两个值(打开/关闭)的锁。计数信号量具有多个计数值。
重要的是要理解信号量计数器跟踪不必阻塞的任务数量,即它们可以继续进行。当计数器为零时,任务会被阻塞并添加到信号量的列表中。因此,如果任务无法前进,则在P()例程中将其添加到列表中,并使用V()例程进行"释放"。
现在,很明显可以看出二进制信号量如何用于解决同步和互斥 - 它们本质上是锁。
例如,同步:
thread A{
semaphore &s; //locks/semaphores are passed by reference! think about why this is so.
A(semaphore &s): s(s){} //constructor
foo(){
...
s.P();
;// some block of code B2
...
}

//thread B{
semaphore &s;
B(semaphore &s): s(s){} //constructor
foo(){
...
...
// some block of code B1
s.V();
..
}

main(){
semaphore s(0); // we start the semaphore at 0 (closed)
A a(s);
B b(s);
}
在上面的例子中,B2只能在B1执行完成后才能执行。假设线程A先执行-sem.P(),并等待,因为计数器为0(关闭)。然后线程B出现,完成B1,然后释放线程A - 然后线程A完成B2。所以我们实现了同步。
现在让我们看一下使用二元信号量的互斥。
thread mutual_ex{
semaphore &s;
mutual_ex(semaphore &s): s(s){} //constructor
foo(){
...
s.P();
//critical section
s.V();
...
...
s.P();
//critical section
s.V();
...

}

main(){
semaphore s(1);
mutual_ex m1(s);
mutual_ex m2(s);
}

互斥锁也很简单——m1和m2不能同时进入关键部分。因此,每个线程都使用同一个信号量来为其两个关键部分提供互斥保护。那么,是否可能具有更大的并发性?这取决于关键部分。(想想如何使用信号量来实现互斥……提示:我必须一定只使用一个信号量吗?)

计数信号量:带有多个值的信号量。让我们看看这意味着什么——有多个值的锁?? 所以开放,关闭,还有......嗯。在互斥或同步中,多级锁有什么用处?

让我们先看看其中较容易的一种:

使用计数信号量进行同步:假设您有3个任务——要求#1和2在执行之前必须执行3。如何设计同步过程?

thread t1{
...
s.P();
//block of code B1

thread t2{
...
s.P();
//block of code B2

thread t3{
...
//block of code B3
s.V();
s.V();
}

如果您的信号量初始为关闭状态,您可以确保t1和t2被阻塞并添加到信号量列表中。然后出现了非常重要的t3,完成它的任务并释放t1和t2。它们以什么顺序释放?取决于信号量列表的实现方式。可能是FIFO,也可能基于某种特定优先级等。(请注意:考虑一下如果您想按照特定顺序执行t1和t2,但不知道信号量的实现方式,您将如何安排P和V操作)

(找出:如果V的数量大于P的数量会发生什么?)

使用计数信号量进行互斥:我想让您构建自己的伪代码(这样可以更好地理解!)-但根本概念是:计数信号量的计数器= N允许N个任务自由进入临界区。这意味着您有N个任务(或线程,如果您喜欢)进入临界区,但第N + 1个任务被阻止(进入我们最喜欢的阻塞任务列表),只有当有人至少V一次信号量时才能通过。因此,信号量计数器不是在0和1之间摆动,而是在0和N之间摆动,允许N个任务自由进出,不阻塞任何人!

现在,为什么需要这样一个愚蠢的东西呢?难道互斥的整个重点不是防止多个人访问资源吗?(提示提示...您的计算机上不总是只有一个驱动器,对吧...?)

思考一下:仅通过使用计数信号量就可以实现互斥吗?如果您有10个资源实例,并且有10个线程(通过计数信号量)尝试使用第一个实例,会发生什么?


10

我已经创建了一种可视化方式,可以帮助理解这个想法。 Semaphore 在多线程环境中控制对共享资源的访问。

enter image description here
ExecutorService executor = Executors.newFixedThreadPool(7);

Semaphore semaphore = new Semaphore(4);

Runnable longRunningTask = () -> {
    boolean permit = false;
    try {
        permit = semaphore.tryAcquire(1, TimeUnit.SECONDS);
        if (permit) {
            System.out.println("Semaphore acquired");
            Thread.sleep(5);
        } else {
            System.out.println("Could not acquire semaphore");
        }
    } catch (InterruptedException e) {
        throw new IllegalStateException(e);
    } finally {
        if (permit) {
            semaphore.release();
        }
    }
};

// execute tasks
for (int j = 0; j < 10; j++) {
    executor.submit(longRunningTask);
}
executor.shutdown();

输出

Semaphore acquired
Semaphore acquired
Semaphore acquired
Semaphore acquired
Could not acquire semaphore
Could not acquire semaphore
Could not acquire semaphore

这是一篇文章中的示例代码。


7
一个信号量是一个包含自然数(即大于或等于零的整数)的对象,其定义了两个修改操作。其中一个操作 V 将自然数加1。另一个操作 P 将自然数减1。这两个活动都是原子性的(即在执行 VP 时不能执行其他操作)。
因为自然数0无法减少,所以在包含0的信号量上调用 P 将阻塞调用进程/线程的执行,直到某个时刻该数字不再为0,并且可以成功(并原子地)执行 P
如其他答案中所述,信号量可用于将对某个资源的访问限制为最多(但可变)数量的进程。

4
信号量就像是线程限制器。
例如:假设您有一个包含100个线程的池,并且您想要执行一些数据库操作。如果100个线程同时访问数据库,那么可能会出现锁定问题,因此我们可以使用信号量来允许仅有限数量的线程同时访问。下面的示例允许每次只有一个线程访问。当一个线程调用 acquire() 方法时,它将获得访问权限;在调用 release() 方法后,它将释放访问权限以便下一个线程可以获得访问权限。
    package practice;
    import java.util.concurrent.Semaphore;

    public class SemaphoreExample {
        public static void main(String[] args) {
            Semaphore s = new Semaphore(1);
            semaphoreTask s1 = new semaphoreTask(s);
            semaphoreTask s2 = new semaphoreTask(s);
            semaphoreTask s3 = new semaphoreTask(s);
            semaphoreTask s4 = new semaphoreTask(s);
            semaphoreTask s5 = new semaphoreTask(s);
            s1.start();
            s2.start();
            s3.start();
            s4.start();
            s5.start();
        }
    }

    class semaphoreTask extends Thread {
        Semaphore s;
        public semaphoreTask(Semaphore s) {
            this.s = s;
        }
        @Override
        public void run() {
            try {
                s.acquire();
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName()+" Going to perform some operation");
                s.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } 
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接