Answers:
将信号量视为夜总会里的蹦蹦跳跳。一次有大量的人员被允许进入俱乐部。如果俱乐部已满,则不允许任何人进入,但是一旦一个人离开,另一个人可能会进入。
这只是限制特定资源使用者数量的一种方法。例如,限制同时调用应用程序中的数据库的次数。
这是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)揭秘的互斥量和信号量文章是一个简短的简短介绍,介绍了互斥量和信号量与众不同之处以及何时以及不应该使用它们。我在这里摘录了几个关键段落。
关键是互斥锁应用于保护共享资源,而信号灯应用于信令。通常,您不应使用信号量来保护共享资源,也不应使用互斥体来发信号。例如,在使用信号量来保护共享资源方面,使用保镖类比存在一些问题-您可以通过这种方式使用它们,但可能会导致难以诊断错误。
尽管互斥量和信号量在实现上有一些相似之处,但应始终以不同的方式使用它们。
最顶部的问题最常见(但仍然不正确)的答案是互斥量和信号量非常相似,唯一的区别是信号量可以大于一个。几乎所有工程师似乎都正确地理解了互斥锁是一种二进制标志,用于通过确保代码的关键部分之间的互斥来保护共享资源。但是,当被问及如何使用“计数信号量”时,大多数工程师(仅凭信心而定)就表达了教科书的某种观点,即它们被用来保护多个等效资源。
...
在这一点上,使用浴室钥匙作为保护共享资源(浴室)的想法做出了一个有趣的类比。如果商店只有一个浴室,那么一个钥匙就足以保护该资源并防止多个人同时使用它。
如果有多个浴室,则可能会想像将它们一样设置成多个键并进行多次键操作-这类似于信号灯被滥用。拥有钥匙后,您实际上并不知道哪间浴室可用,如果沿着这条路走,您可能最终将使用互斥锁来提供该信息,并确保您不使用已经被占用的浴室。 。
信号量是保护几个基本相同的资源的错误工具,但这是许多人想到并使用它的人。保镖的类比截然不同-没有几种相同类型的资源,而是有一种资源可以接受多个同时用户。我想可以在这种情况下使用信号量,但在现实世界中很少有此类比喻真正成立的情况-经常有几种相同类型,但仍然有个别资源(例如浴室)无法使用这条路。
...
信号量的正确用法是用于从一个任务向另一个任务发信号。互斥锁应始终由使用它保护的共享资源的每个任务按此顺序获取和释放。相比之下,使用信号量的任务要么发出信号,要么等待,而不是同时发出信号或等待。例如,任务1可能包含用于在按下“电源”按钮时张贴(即发出信号或递增信号)特定信号量的代码,而唤醒显示器的任务2则悬停在同一信号量上。在这种情况下,一个任务是事件信号的产生者;另一个消费者。
...
这里要指出的一点是,互斥锁会以一种不好的方式干扰实时操作系统,从而导致优先级倒置,由于资源共享,优先级较低的任务可能在较重要的任务之前执行。简而言之,当优先级较低的任务使用互斥锁抢占资源A,然后尝试抢夺B,但由于B不可用而暂停时,就会发生这种情况。在等待期间,出现了一个更高优先级的任务,需要A,但是它已经被捆绑,并且由于等待B而被一个甚至没有运行的进程所束缚。有很多方法可以解决此问题,但是通常它是固定的通过更改互斥锁和任务管理器。在这种情况下,互斥锁比二进制信号量要复杂得多,
...
互斥量和信号量之间现代广泛混淆的原因是历史性的,因为它可以追溯到1974年Djikstra发明的信号量(本文中为大写“ S”)。在此之前,计算机科学家已知的任何中断安全任务同步和信号机制都无法有效地扩展以用于两个以上的任务。Dijkstra的革命性,安全且可扩展的信号量被应用于关键部分的保护和信号发送。于是混乱就开始了。
但是,后来出现了基于优先级的抢占式RTOS(例如VRTX,约1980年),建立RMA的学术论文以及由优先级倒置引起的问题之后,对操作系统开发人员而言变得显而易见。在1990年的继承协议3中,很明显,互斥锁不仅是带有二进制计数器的信号量。
Mutex:资源共享
信号量:信号
在没有仔细考虑副作用的情况下,请勿将另一种用于其他目的。
Mutex:对资源的独占成员访问
信号量:对资源的n成员访问
也就是说,互斥锁可用于同步访问计数器,文件,数据库等。
一个sempahore可以做同样的事情,但是支持固定数量的同时呼叫者。例如,我可以将数据库调用包装在semaphore(3)中,这样我的多线程应用程序最多可以同时连接3个数据库。所有尝试将阻塞,直到打开三个插槽之一。它们使像天真节流这样的事情变得非常非常容易。
@克雷格:
信号量是一种锁定资源的方法,可以确保在执行一段代码时,只有该段代码可以访问该资源。这样可以防止两个线程同时访问资源,这可能会导致问题。
这不仅限于一个线程。可以将信号量配置为允许固定数量的线程访问资源。
构建并发程序有两个基本概念-同步和互斥。我们将看到这两种类型的锁(信号灯通常是一种锁定机制)如何帮助我们实现同步和互斥。
信号量是一种编程结构,可通过实现同步和互斥来帮助我们实现并发。信号量有两种类型,二进制和计数。
信号量包括两个部分:一个计数器和一个等待访问特定资源的任务列表。信号量执行两项操作: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个线程(通过计数信号量)进入并尝试使用第一个实例,该怎么办?
我已经创建了可视化效果,该可视化效果应该有助于理解该想法。信号量控制在多线程环境中对公共资源的访问。
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
本文中的示例代码
信号量就像线程限制器一样。
示例:如果您有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();
}
}
}
因此,想象每个人都在尝试去洗手间,而洗手间只有一定数量的钥匙。现在,如果没有足够的键,该人需要等待。因此,可以将信号量视为代表可用于浴室(系统资源)的那些键集,以供不同进程(浴室行进者)请求访问。
现在想象一下试图同时去洗手间的两个过程。那不是一个好情况,并且使用信号量来防止这种情况。不幸的是,信号量是一种自愿机制,过程(我们的洗手间)可以忽略它(即,即使有钥匙,仍然有人可以将门打开)。
二进制/互斥量和计数信号量之间也存在差异。
在http://www.cs.columbia.edu/~jae/4118/lect/L05-ipc.html上查看讲义。