什么是信号量?


349

信号量是一种编程概念,经常用于解决多线程问题。我对社区的问题:

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


14
一个布尔标志,其值基于整数计数器是否已达到其指定的上限。迷惑到最大!
山姆

Answers:


399

将信号量视为夜总会里的蹦蹦跳跳。一次有大量的人员被允许进入俱乐部。如果俱乐部已满,则不允许任何人进入,但是一旦一个人离开,另一个人可能会进入。

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

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

6
如果就像夜总会里的蹦床一样,它应该让客人按顺序进入,但是当我尝试时,它是随机的。例如。来宾40首先在来宾39之前进入。我们能做些什么来控制这一点?
TNA 2014年

2
@TNA:是的,这与本示例中启动新线程的方式有关,而实际上不在答案范围之内。
Patrik Svensson 2014年

5
Bouncer类比确实是史诗般的,但有趣的是,它已经被使用: albahari.com/threading/part2.aspx#_Semaphore
Igor

信号量在分布式系统中提供什么价值?
csandreas17年

198

迈克尔·巴尔(Michael Barr)揭秘互斥量和信号量文章是一个简短的简短介绍,介绍了互斥量和信号量与众不同之处以及何时以及不应该使用它们。我在这里摘录了几个关键段落。

关键是互斥锁应用于保护共享资源,而信号灯应用于信令。通常,您不应使用信号量来保护共享资源,也不应使用互斥体来发信号。例如,在使用信号量来保护共享资源方面,使用保镖类比存在一些问题-您可以通过这种方式使用它们,但可能会导致难以诊断错误。

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

最顶部的问题最常见(但仍然不正确)的答案是互斥量和信号量非常相似,唯一的区别是信号量可以大于一个。几乎所有工程师似乎都正确地理解了互斥锁是一种二进制标志,用于通过确保代码的关键部分之间的互斥来保护共享资源。但是,当被问及如何使用“计数信号量”时,大多数工程师(仅凭信心而定)就表达了教科书的某种观点,即它们被用来保护多个等效资源。

...

在这一点上,使用浴室钥匙作为保护共享资源(浴室)的想法做出了一个有趣的类比。如果商店只有一个浴室,那么一个钥匙就足以保护该资源并防止多个人同时使用它。

如果有多个浴室,则可能会想像将它们一样设置成多个键并进行多次键操作-这类似于信号灯被滥用。拥有钥匙后,您实际上并不知道哪间浴室可用,如果沿着这条路走,您可能最终将使用互斥锁来提供该信息,并确保您不使用已经被占用的浴室。 。

信号量是保护几个基本相同的资源的错误工具,但这是许多人想到并使用它的人。保镖的类比截然不同-没有几种相同类型的资源,而是有一种资源可以接受多个同时用户。我想可以在这种情况下使用信号量,但在现实世界中很少有此类比喻真正成立的情况-经常有几种相同类型,但仍然有个别资源(例如浴室)无法使用这条路。

...

信号量的正确用法是用于从一个任务向另一个任务发信号。互斥锁应始终由使用它保护的共享资源的每个任务按此顺序获取和释放。相比之下,使用信号量的任务要么发出信号,要么等待,而不是同时发出信号或等待。例如,任务1可能包含用于在按下“电源”按钮时张贴(即发出信号或递增信号)特定信号量的代码,而唤醒显示器的任务2则悬停在同一信号量上。在这种情况下,一个任务是事件信号的产生者;另一个消费者。

...

这里要指出的一点是,互斥锁会以一种不好的方式干扰实时操作系统,从而导致优先级倒置,由于资源共享,优先级较低的任务可能在较重要的任务之前执行。简而言之,当优先级较低的任务使用互斥锁抢​​占资源A,然后尝试抢夺B,但由于B不可用而暂停时,就会发生这种情况。在等待期间,出现了一个更高优先级的任务,需要A,但是它已经被捆绑,并且由于等待B而被一个甚至没有运行的进程所束缚。有很多方法可以解决此问题,但是通常它是固定的通过更改互斥锁和任务管理器。在这种情况下,互斥锁比二进制信号量要复杂得多,

...

互斥量和信号量之间现代广泛混淆的原因是历史性的,因为它可以追溯到1974年Djikstra发明的信号量(本文中为大写“ S”)。在此之前,计算机科学家已知的任何中断安全任务同步和信号机制都无法有效地扩展以用于两个以上的任务。Dijkstra的革命性,安全且可扩展的信号量被应用于关键部分的保护和信号发送。于是混乱就开始了。

但是,后来出现了基于优先级的抢占式RTOS(例如VRTX,约1980年),建立RMA的学术论文以及由优先级倒置引起的问题之后,对操作系统开发人员而言变得显而易见。在1990年的继承协议3中,很明显,互斥锁不仅是带有二进制计数器的信号量。

Mutex:资源共享

信号量:信号

在没有仔细考虑副作用的情况下,请勿将另一种用于其他目的。


10
查看此斯坦福并发PDF文档。看8页以上,说明会更有意义的话.. see.stanford.edu/materials/icsppcs107/...
克里斯·萨勃拉曼尼亚

3
本关于信号量小书是有关这些问题的有价值的读物。
G. Bach'17年

@KrisSubramanian感谢您的链接。但是,该文档讨论了有关信号量的问题,而没有讨论互斥量。但是,您是否意味着可以使用Mutex保护示例中的共享缓冲区?而不是具有2个信号量emptyBuffers和fullBuffers
talekeDskobeDa

1
@普拉莫德真的。该链接未添加任何与Mutex相关的注释。我添加了链接,以便SO读者可以清楚了解信号量方面。:)在这种情况下有趣的是,由于缓冲区是按顺序并以循环格式访问的,因此缓冲区没有任何锁被使用。也就是说,Writer将写入0并发信号通知读取器从0开始读取。如果读取器未从0读取并发信号通知写入器,则写入器将阻塞。因此,不需要使用互斥锁来锁定公共资源。这不同于上面给出的浴室类比。
克里斯·斯伯拉曼尼亚

@Kris Subramanian:不错的文档,但包含一些小误区:第3页开始说“锁定信号量的每个线程都应谨慎解锁”-可以用任何线程将其解锁。如果您在同一线程中执行此操作,则仅将其用作“破碎互斥体”。“ Brocken”,因为它仍然可以无意中从其他线程解锁-发生错误-破坏了您的逻辑。思想仍然不错。
Rustam A.19年

70

Mutex:对资源的独占成员访问

信号量:对资源的n成员访问

也就是说,互斥锁可用于同步访问计数器,文件,数据库等。

一个sempahore可以做同样的事情,但是支持固定数量的同时呼叫者。例如,我可以将数据库调用包装在semaphore(3)中,这样我的多线程应用程序最多可以同时连接3个数据库。所有尝试将阻塞,直到打开三个插槽之一。它们使像天真节流这样的事情变得非常非常容易。


20
据理查德W.史蒂文斯,一个互斥体实际上是一个二进制信号,只有两个可能的值:0和1
徐强

19
William Stallings 在《操作系统内部和设计原理》中的@QiangXu中,二进制信号量与互斥量在一个非常重要的方面不同,我引用:“互斥量和二进制信号量之间的关键区别在于,锁定互斥量的过程必须是一个用来解锁它的程序。相反,一个进程可能会锁定一个二进制信号量,而另一个进程可能会对其进行解锁。”
电动咖啡

3
冒着评论过时的线程的风险,这是不正确的。正如@AdamDavis前面提到的那样,信号量不应(必须?)不用于对资源的n成员访问-仍应使用Mutex来完成。考虑一下在Coffeeshop中的浴室类比,其中有多个人等待进入,或者多个浴室具有与浴室相似的钥匙。相反,信号量应用于任务之间的信令。
cspider

21

考虑一下,一辆出租车可以容纳总共3()+2()人,包括驾驶员。因此,一次semaphore只能允许5个人进入车内。并且一个mutex只允许一个人坐在汽车的一个座位上。

因此,Mutexa允许对资源(例如OS线程)的独占访问,而a Semaphore则允许一次访问n个资源。


19

@克雷格:

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

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


7
这是评论,不是答案。
卡巴斯基

11
是的,但是我想我是在注释添加到Stack Overflow之前写的。或者我没有,真的不记得了。这次我在评论中回答了。:-)
Mats Fredriksson

16

信号量也可以用作...信号量。例如,如果您有多个进程将数据放入队列中,而只有一个任务在使用该队列中的数据。如果您不希望自己的消费任务不断在队列中轮询可用数据,则可以使用信号灯。

在此,信号灯不用作排除机制,而是用作信号传递机制。消费任务正在等待信号量生产任务正在等待信号量。

这样,消费任务仅在有数据要出队时才运行


11

构建并发程序有两个基本概念-同步和互斥。我们将看到这两种类型的锁(信号灯通常是一种锁定机制)如何帮助我们实现同步和互斥。

信号量是一种编程结构,可通过实现同步和互斥来帮助我们实现并发。信号量有两种类型,二进制和计数。

信号量包括两个部分:一个计数器和一个等待访问特定资源的任务列表。信号量执行两项操作:wait(P)[就像获取锁一样],以及release(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-然后完成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个任务-在3之后要执行的#1和2。您将如何设计同步?

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的数量会发生什么?)

互斥使用计数信号量:我希望您为此构建自己的伪代码(使您更好地理解!)-但是基本概念是:counter = N的计数信号量使N个任务可以自由进入关键部分。这意味着您有N个任务(或线程,如果您愿意)进入关键部分,但第N + 1个任务被阻止(进入我们最喜欢的阻止任务列表),并且只有当有人V成为信号量时才通过至少一次。因此,信号量计数器现在不再在0和1之间摆动,而是在0和N之间摆动,从而允许N个任务自由进出,阻止任何人!

现在,天哪,你为什么需要这么愚蠢的东西?互斥的全部目的不是让一个以上的人访问资源吗?(提示提示...您的计算机并不总是只有一个驱动器,对吗??)

考虑一下:是否可以通过单独使用计数信号量来实现互斥?如果您有10个资源实例,并且有10个线程(通过计数信号量)进入并尝试使用第一个实例,该怎么办?


7

信号量是包含自然数(即大于或等于零的整数)的对象,在其上定义了两个修改操作。一个操作,V自然加1。另一个操作P将自然数减少1。这两个活动都是原子的(即,不能与a V或a 同时执行其他操作P)。

因为自然数0不能减少,所以调用P包含0的信号量将阻塞调用进程(/ thread)的执行,直到该数字不再为0并且P可以成功(原子地)执行为止。

如其他答案中所述,信号量可用于将对特定资源的访问限制为最大(但可变)进程数。


7

我已经创建了可视化效果,该可视化效果应该有助于理解该想法。信号量控制在多线程环境中对公共资源的访问。 在此处输入图片说明

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

本文中的示例代码


3

硬件或软件标志。在多任务系统中,信号量是一个变量,其值指示公共资源的状态。需要该资源的过程会检查该信号量以确定资源状态,然后决定如何进行操作。


2

信号量就像线程限制器一样。

示例:如果您有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();
            }
        } 
    }

1

因此,想象每个人都在尝试去洗手间,而洗手间只有一定数量的钥匙。现在,如果没有足够的键,该人需要等待。因此,可以将信号量视为代表可用于浴室(系统资源)的那些键集,以供不同进程(浴室行进者)请求访问。

现在想象一下试图同时去洗手间的两个过程。那不是一个好情况,并且使用信号量来防止这种情况。不幸的是,信号量是一种自愿机制,过程(我们的洗手间)可以忽略它(即,即使有钥匙,仍然有人可以将门打开)。

二进制/互斥量和计数信号量之间也存在差异。

http://www.cs.columbia.edu/~jae/4118/lect/L05-ipc.html上查看讲义。


0

这是一个古老的问题,但是信号量最有趣的用途之一是读/写锁定,并且没有明确提及。

r / w锁的工作方式很简单:一个读者消耗一个许可证,而作家则消耗所有许可证。确实,ar / w锁的简单实现,但是需要对读取的元数据进行修改(实际上是两次),这可能会成为瓶颈,仍然比互斥锁或锁好得多。

另一个缺点是,编写者也可以很容易地开始工作,除非信号量是公平的,或者写请求在多个请求中获得许可,在这种情况下,他们之间需要显式的互斥体。

进一步阅读


-3

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


13
听起来像是互斥体而不是信号量
山姆
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.