什么是信号量?

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

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

270161 次浏览

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

@Craig:

信号量是一种锁定信号的方法 资源,这样才有保证 当执行一段代码时, 只有这段代码可以访问 该资源。这保留了两个线程 从并发访问资源,

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

Michael Barr的文章互斥对象和信号量解密很好地介绍了互斥量和信号量的不同之处,以及什么时候应该和不应该使用它们。我在这里摘录了几个关键段落。

关键在于应该使用互斥来保护共享资源,而应该使用信号量来发送信号。通常不应该使用信号量来保护共享资源,也不应该使用互斥量来发送信号。例如,在使用信号量来保护共享资源方面,使用bouncer类比是有问题的——您可以这样使用它们,但这可能会导致难以诊断错误。

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

对于上面提出的问题,最常见(但仍然不正确)的答案是互斥量和信号量非常相似,唯一重要的区别是信号量的计数可以大于1。几乎所有的工程师似乎都理解互斥是一种二进制标志,用于通过确保代码关键部分内的互斥来保护共享资源。但是当被问及如何使用“计数信号量”时,大多数工程师——只是在他们的信心程度上有所不同——表达了一些教科书上的观点,即这些措施是用来保护几种等效资源的。

...

在这一点上,一个有趣的类比是使用浴室钥匙的想法来保护共享资源-浴室。如果一家商店只有一间浴室,那么一把钥匙就足以保护这一资源,防止多人同时使用。

如果有多个浴室,人们可能会试图用相同的键来设置多个键——这类似于误用信号量。一旦你有了一个键,你实际上不知道哪个浴室是可用的,如果你沿着这条路走下去,你可能最终会使用互斥锁来提供该信息,并确保你没有使用已经被占用的浴室。

对于保护几个本质上相同的资源来说,信号量是一种错误的工具,但这就是许多人对它的看法和使用方式。与bouncer的类比是截然不同的——没有几个相同类型的资源,而是有一个资源可以同时接受多个用户。我认为信号量可以在这种情况下使用,但在现实世界中,这种类比很少成立——更常见的情况是,有几个相同类型的资源,但仍然是单独的资源,比如浴室,不能以这种方式使用。

...

信号量的正确用法是用于从一个任务发送信号到另一个任务。互斥锁应该被使用它所保护的共享资源的每个任务按照一定的顺序接收和释放。相比之下,使用信号量的任务要么发出信号,要么等待——不是两者都等待。例如,Task 1可能包含发送(即发出信号或增加)特定信号量的代码,当“power”;按钮被按下,Task 2(唤醒显示)将挂起相同的信号量。在这个场景中,一个任务是事件信号的生产者;另一个是消费者。

...

这里有一点很重要,互斥锁会严重干扰实时操作系统,导致优先级倒置,由于资源共享,一个不太重要的任务可能会在一个更重要的任务之前执行。简而言之,当一个低优先级的任务使用互斥来获取资源a,然后试图获取B,但由于B不可用而暂停时,就会发生这种情况。当它在等待的时候,一个更高优先级的任务来了,它需要a,但它已经被一个甚至没有运行的进程占用了,因为它在等待b。有很多方法来解决这个问题,但通常是通过改变互斥量和任务管理器来解决。在这些情况下,互斥量要比二进制信号量复杂得多,在这种情况下使用信号量将导致优先级反转,因为任务管理器不知道优先级反转,也无法采取行动纠正它。

...

互斥量和信号量之间在现代广泛混淆的原因是历史的,因为它可以追溯到1974年由Djikstra发明的信号量(在本文中是大写字母" ")。在此之前,计算机科学家所知道的中断安全任务同步和信号机制都不能有效地扩展到两个以上的任务。Dijkstra革命性的、安全和可扩展的信号量被应用于临界区保护和信号。于是,混乱开始了。

然而,在基于优先级的抢占式RTOS(例如,VRTX,约1980年)的出现、建立RMA的学术论文的发表以及优先级反转引起的问题以及1990年关于优先级继承协议的论文的发表之后,操作系统开发人员很明显地意识到互斥量必须不仅仅是带有二进制计数器的信号量。

互斥:资源共享

信号:信号

在没有仔细考虑副作用的情况下,不要用一种药物来替代另一种药物。

互斥:对资源的独占成员访问

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

也就是说,互斥可以用来同步对计数器、文件、数据库等的访问。

信号量可以做同样的事情,但支持固定数量的同时调用者。例如,我可以将我的数据库调用包装在一个信号量(3)中,这样我的多线程应用程序将最多3个同时连接到数据库。所有的尝试都将被阻塞,直到三个插槽中的一个打开。它们让简单的节流变得非常简单。

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

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

正如在其他回答中提到的,信号量可用于将对某个资源的访问限制为最大(但可变的)进程数。

把信号量想象成夜总会的保镖。同时允许进入俱乐部的人数是固定的。如果俱乐部满了,任何人都不允许进入,但只要一个人离开,另一个人就可以进入。

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

下面是一个非常适合教学的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);
}
}
}
信号量也可以用作…信号量。 例如,如果有多个进程将数据排队到队列中,而只有一个任务使用队列中的数据。如果您不希望您的消费任务不断地轮询队列以获取可用数据,您可以使用semaphore 这里信号量不用作排除机制,而是用作信号机制。 消费任务正在等待信号量

.生产任务正在发送信号量

这样,当且仅当有数据要退出队列时,消费任务才会运行

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

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

r/w锁的工作方式很简单:为读取器消耗一个许可证,为写入器消耗所有许可证。 事实上,一个简单的r/w锁实现,但需要在读取时修改元数据(实际上是两次),这可能会成为一个瓶颈,但仍然比互斥锁或锁要好得多

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

进一步:

想象一下,每个人都想上厕所而浴室的钥匙是有限的。如果剩下的钥匙不够多,那个人就需要等待。因此,可以将信号量看作是表示不同进程(上厕所的人)可以请求访问的卫生间(系统资源)可用的一组键。

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

二进制/互斥和amp;计数信号量。

http://www.cs.columbia.edu/~jae/4118/lect/L05-ipc.html上查看课堂笔记。

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

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

信号量有两个部分:计数器和等待访问特定资源的任务列表。一个信号量执行两个操作:等待(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——然后完成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。他们被释放的顺序是什么?取决于信号量列表的实现。可以是先进先出,可以基于某些特定的优先级,等等。(注意:如果你想要t1和t2以某种特定的顺序执行,并且如果你不知道信号量的实现,那么考虑一下你将如何安排你的P和V;s)

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

使用计数信号量:我希望您为此构造自己的伪代码(使您更好地理解事情!)——但基本概念是这样的:counter = N的计数信号量允许N个任务自由进入临界区。这意味着你有N个任务(或者线程,如果你喜欢的话)进入临界区,但是第N+1个任务被阻塞(进入我们最喜欢的阻塞任务列表),只有当某个V是信号量至少一次时才允许通过。因此,信号量计数器不再在0和1之间摇摆,而是在0和N之间,允许N个任务自由进出,不阻塞任何人!

天啊,你为什么需要这么愚蠢的东西?互斥的全部意义不就是不让多于一个人访问资源吗?(提示提示…你的电脑里并不总是只有一个驱动器,对吧?)

想想看:互斥是通过单独有一个计数信号量实现的吗?如果您有一个资源的10个实例,并且有10个线程(通过计数信号量)进入并试图使用第一个实例,该怎么办?

考虑,一辆出租车,包括司机在内,总共可以容纳3()+2(前面)人。所以,semaphore一次只允许5个人在车里。 而mutex只允许1人坐在汽车的一个座位上

因此,Mutex是允许对资源(就像一个操作系统线程)的独占访问,而Semaphore是允许一次访问n数量的资源。

我已经创建了可视化,这应该有助于理解的想法。信号量控制多线程环境中对公共资源的访问。 enter image description here < / p >
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

文章 . xml文件中的示例代码

信号量的作用类似于线程限制器。

例子:如果你有一个100个线程的线程池,你想执行一些DB操作。如果在给定的时间有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();
}
}
}

互斥量只是一个布尔值,而信号量是一个计数器

两者都用于锁定部分代码,这样就不会有太多线程访问它。

例子

lock.set()
a += 1
lock.unset()

现在,如果lock是一个互斥量,这意味着无论有多少线程尝试访问受保护的代码片段,它将始终被锁定或解锁(表面下的布尔值)。当被锁定时,任何其他线程都会等待它被前一个线程解锁/取消设置。

现在想象一下,如果lock是一个预定义MAX值的计数器(在我们的例子中是2)。然后,如果有两个线程试图访问该资源,那么lock的值将增加到2。如果第三个线程试图访问它,它就会等待计数器低于2,以此类推。

如果lock作为一个信号量的最大值为1,那么它将完全作为一个互斥量。