信号量是一个经常用于解决多线程问题的编程概念。我对社区的问题:
什么是信号量以及如何使用它?
将信号量视为夜总会的保镖。俱乐部允许有特定数量的人同时进入。如果俱乐部已满,则不允许任何人进入,但一旦一个人离开,另一个人可能会进入。
这只是一种限制特定资源的消费者数量的方法。例如,限制应用程序中同时调用数据库的数量。
这是 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);
}
}
}
Michael Barr 的文章 Mutexes and Semaphores Demystified 非常简短地介绍了互斥锁和信号量的不同之处,以及何时应该使用和不应该使用它们。我在这里摘录了几个关键段落。
关键是互斥体应该用于保护共享资源,而信号量应该用于信令。您通常不应该使用信号量来保护共享资源,也不应该使用互斥体来发出信号。例如,在使用信号量来保护共享资源方面,保镖类比存在一些问题——你可以这样使用它们,但它可能会导致难以诊断错误。
虽然互斥量和信号量在它们的实现上有一些相似之处,但它们应该始终以不同的方式使用。对顶部提出的问题的最常见(但仍然不正确)的答案是互斥锁和信号量非常相似,唯一显着的区别是信号量可以计数大于 1。几乎所有工程师似乎都正确理解互斥锁是一个二进制标志,用于通过确保代码关键部分内的互斥来保护共享资源。但是,当被要求扩展如何使用“计数信号量”时,大多数工程师——只是他们的信心程度不同——表达了一些教科书的观点,即这些信号量是用来保护几个等效资源的。
...
在这一点上,一个有趣的类比是使用浴室钥匙的想法来保护共享资源 - 浴室。如果一家商店只有一间浴室,那么一把钥匙就足以保护该资源并防止多人同时使用它。
如果有多个浴室,人们可能会想用相同的键设置它们并制作多个键——这类似于信号量被误用。一旦你有了钥匙,你实际上并不知道哪个浴室可用,如果你沿着这条路走,你可能最终会使用互斥锁来提供该信息,并确保你不会使用已经被占用的浴室.
信号量是保护多个基本相同资源的错误工具,但这是许多人想到并使用它的原因。保镖类比明显不同 - 没有几个相同类型的资源,而是有一个资源可以同时接受多个用户。我想在这种情况下可以使用信号量,但在现实世界中很少有这种类比真正成立的情况——更常见的是有几个相同类型但仍然是单独的资源,比如浴室,不能使用这边走。
...
信号量的正确使用是用于从一个任务到另一个任务的信号。互斥锁意味着每个使用它所保护的共享资源的任务总是按照这个顺序来获取和释放。相比之下,使用信号量的任务要么发出信号,要么等待——而不是两者兼而有之。例如,当“电源”按钮被按下时,任务 1 可能包含发布(即,发出信号或增加)特定信号量的代码,而唤醒显示器的任务 2 挂起在同一信号量上。在这种情况下,一个任务是事件信号的生产者;另一个消费者。
...
这里有一点很重要,即互斥体以一种不好的方式干扰实时操作系统,导致优先级反转,由于资源共享,一个不太重要的任务可能在一个更重要的任务之前执行。简而言之,当较低优先级的任务使用互斥锁来获取资源 A,然后尝试获取 B,但由于 B 不可用而被暂停时,就会发生这种情况。在它等待的时候,一个更高优先级的任务出现了,需要 A,但它已经被占用了,并且由于它正在等待 B,它甚至没有运行的进程。有很多方法可以解决这个问题,但它通常是固定的通过更改互斥锁和任务管理器。在这些情况下,互斥体比二进制信号量复杂得多,在这种情况下使用信号量会导致优先级反转,因为任务管理器不知道优先级反转并且无法采取行动来纠正它。
...
现代互斥量和信号量之间广泛混淆的原因是历史性的,因为它可以追溯到 1974 年 Djikstra 发明的信号量(本文中的大写“S”)。在此之前,计算机科学家已知的中断安全任务同步和信号机制都不能有效地扩展以供两个以上的任务使用。 Dijkstra 的革命性、安全和可扩展的信号量被应用于关键部分保护和信号传输。于是混乱开始了。然而,在基于优先级的抢占式 RTOS(例如,VRTX,ca. 1980)的出现、建立 RMA 的学术论文和优先级倒置引起的问题以及一篇关于优先级的论文发表之后,操作系统开发人员意识到了这一点。在 1990 年的继承协议 3 中,很明显,互斥锁必须不仅仅是带有二进制计数器的信号量。
互斥:资源共享
信号量:信号
在没有仔细考虑副作用的情况下,不要将其中一种用于另一种。
互斥锁:对资源的独占成员访问
信号量:对资源的 n 成员访问
也就是说,互斥锁可用于同步对计数器、文件、数据库等的访问。
信号量可以做同样的事情,但支持固定数量的同时呼叫者。例如,我可以将我的数据库调用包装在一个 semaphore(3) 中,这样我的多线程应用程序最多可以同时连接 3 个连接来访问数据库。所有尝试都将被阻止,直到三个插槽之一打开。它们使诸如天真的节流之类的事情变得非常非常容易。
考虑一下,一辆出租车可以容纳包括司机在内的总共 3(rear)+2(front) 人。因此,semaphore
一次只允许 5 人进入车内。 mutex
只允许一个人坐在汽车的一个座位上。
因此,Mutex
是允许对资源的独占访问(类似于操作系统线程),而 Semaphore
是允许对资源的访问一次 n 个资源。
@克雷格:
信号量是一种锁定资源的方法,以保证在执行一段代码时,只有这段代码可以访问该资源。这可以防止两个线程同时访问一个资源,这可能会导致问题。
这不仅限于一个线程。可以将信号量配置为允许固定数量的线程访问资源。
信号量也可以用作...信号量。例如,如果您有多个进程将数据排入队列,并且只有一个任务使用队列中的数据。如果您不希望您的消费任务不断轮询队列以获取可用数据,您可以使用信号量。
这里信号量不是用作排除机制,而是用作信号机制。消费任务在信号量上等待生产任务在信号量上发布。
这样,当且仅当有数据要出队时,消费任务才会运行
构建并发程序有两个基本概念——同步和互斥。我们将看到这两种类型的锁(信号量更普遍地是一种锁定机制)如何帮助我们实现同步和互斥。
信号量是一种编程结构,它通过实现同步和互斥来帮助我们实现并发。信号量有两种类型,二进制和计数。
信号量有两部分:一个计数器和一个等待访问特定资源的任务列表。一个信号量执行两个操作:等待(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。他们以什么顺序被释放?取决于信号量列表的实现。可以是 FIFO,可以基于某些特定的优先级等。 (注意:如果您希望 t1 和 t2 以某种特定顺序执行,并且您不知道信号量的实现,请考虑如何安排您的 P 和 V;s)
(找出:如果 V 的数量大于 P 的数量会发生什么?)
使用计数信号量的互斥:我希望您为此构建自己的伪代码(让您更好地理解事物!) - 但基本概念是:counter = N 的计数信号量允许 N 个任务自由进入临界区.这意味着您有 N 个任务(或线程,如果您愿意)进入临界区,但第 N+1 个任务被阻塞(进入我们最喜欢的阻塞任务列表),并且只有当某人 V 是信号量时才允许通过至少一次。所以信号量计数器,而不是在 0 和 1 之间摆动,现在在 0 和 N 之间移动,允许 N 个任务自由进出,没有人阻塞!
现在,天哪,你为什么需要这么愚蠢的东西?互斥的全部意义不在于不允许多个人访问资源吗? (提示提示...您的计算机中并不总是只有一个驱动器,是吗...?)
想一想:只有一个计数信号量就可以实现互斥吗?如果您有 10 个资源实例,并且有 10 个线程进入(通过计数信号量)并尝试使用第一个实例怎么办?
https://i.stack.imgur.com/cDXIV.png
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
article 中的示例代码
信号量是包含一个自然数(即大于或等于零的整数)的对象,在该对象上定义了两个修改操作。一个操作 V
将自然值加 1。另一个操作 P
将自然数减 1。两个活动都是原子的(即没有其他操作可以与 V
或 P
同时执行)。
因为自然数 0 不能减少,所以在包含 0 的信号量上调用 P
将阻塞调用进程(/线程)的执行,直到该数字不再是 0 并且 P
可以成功(和原子地)执行。
如其他答案中所述,信号量可用于将对某个资源的访问限制为最大(但可变)进程数。
硬件或软件标志。在多任务系统中,信号量是一个变量,其值指示公共资源的状态。需要资源的进程检查信号量以确定资源状态,然后决定如何进行。
所以想象每个人都想去洗手间,而洗手间只有一定数量的钥匙。现在如果没有足够的钥匙,那个人需要等待。因此,可以将信号量视为代表浴室(系统资源)可用的一组密钥,不同的进程(上厕所的人)可以请求访问。
现在想象一下两个进程试图同时去洗手间。这不是一个好的情况,信号量被用来防止这种情况。不幸的是,信号量是一种自愿机制,并且进程(我们的上厕所的人)可以忽略它(即即使有钥匙,仍然有人可以踢门)。
二进制/互斥体和计数信号量之间也存在差异。
查看 http://www.cs.columbia.edu/~jae/4118/lect/L05-ipc.html 上的讲义。
信号量就像线程限制器一样。
示例:如果您有一个包含 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();
}
}
}
这是一个老问题,但信号量最有趣的用途之一是读/写锁,它没有被明确提及。
r/w 锁的工作方式很简单:读取器使用一个许可,写入器使用所有许可。实际上,ar/w 锁的一个简单实现,但需要在读取时修改元数据(实际上是两次),这可能成为瓶颈,仍然比互斥锁或锁好得多。
另一个缺点是写入器也可以很容易地启动,除非信号量是公平的,或者写入器在多个请求中获得许可,在这种情况下,它们之间需要一个明确的互斥体。
进一步read:
互斥量只是一个布尔值,而信号量是一个计数器。
两者都用于锁定部分代码,因此它不会被太多线程访问。
例子
lock.set()
a += 1
lock.unset()
现在,如果 lock
是一个互斥体,则意味着无论有多少线程尝试访问受保护的代码片段,它都将始终被锁定或解锁(表面下的布尔值)。在锁定时,任何其他线程都会等到它被前一个线程解锁/取消设置。
现在想象一下,如果 lock
是一个具有预定义 MAX 值的计数器(例如我们的示例中的 2)。然后,如果 2 个线程尝试访问该资源,则 lock 的值将增加到 2。如果第 3 个线程尝试访问它,它将简单地等待计数器低于 2,依此类推。
如果锁作为信号量的最大值为 1,那么它将完全充当互斥锁。
信号量是一种锁定资源的方法,以保证在执行一段代码时,只有这段代码可以访问该资源。这可以防止两个线程同时访问一个资源,这可能会导致问题。