信号量是一种经常用于解决多线程问题的编程概念.我向社区提出的问题:
什么是信号量,你如何使用它?
把信号量想象成夜总会的保镖.俱乐部一次允许有一定数量的人.如果俱乐部已经满员,则不允许任何人进入,但只要一个人离开另一个人就可以进入.
它只是一种限制特定资源的消费者数量的方法.例如,限制应用程序中对数据库的同时调用次数.
这是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和Semaphores Demystified这篇文章非常简短地介绍了什么使互斥锁和信号量不同,何时应该和不应该使用它们.我在这里摘录了几个关键段落.
关键是应该使用互斥锁来保护共享资源,而信号量应该用于信令.您通常不应该使用信号量来保护共享资源,也不应该使用互斥信号来保护信令.例如,在使用信号量来保护共享资源方面,有些问题与保镖类比有关 - 您可以这样使用它们,但这可能导致难以诊断错误.
虽然互斥锁和信号量在实现方面有一些相似之处,但它们应始终以不同的方式使用.
对顶部提出的问题最常见(但仍然不正确)的答案是互斥体和信号量非常相似,唯一的显着差异是信号量可以高于1.几乎所有工程师似乎都正确理解互斥锁是一个二进制标志,用于通过确保代码的关键部分内的互斥来保护共享资源.但当被要求扩展如何使用"计数信号量"时,大多数工程师 - 仅仅在他们的信任程度上有所不同 - 表达了教科书意见的一些风格,这些用于保护几个等效资源.
...
在这一点上,一个有趣的比喻是使用浴室钥匙的概念来保护共享资源 - 浴室.如果商店有一个单独的浴室,那么一把钥匙就足以保护该资源并防止多人同时使用它.
如果有多个浴室,人们可能会想要对它们进行密钥设置并制作多个密钥 - 这类似于信号量被误用.一旦你有了钥匙,你实际上并不知道哪个浴室可用,如果沿着这条路走下去,你可能最终会使用互斥锁来提供这些信息,并确保你没有带一个已经被占用的浴室.
信号量是保护几个基本相同资源的错误工具,但这是有多少人想到它并使用它.保镖的类比明显不同 - 没有几种相同类型的资源,而是有一种资源可以接受多个同时发生的用户.我想在这种情况下可以使用信号量,但很少有真实情况下类比实际存在 - 更常见的是有几种相同的类型,但仍然是个别资源,如浴室,不能使用这条路.
...
信号量的正确使用是用于从一个任务到另一个任务的信令.互斥体意味着每个使用它保护的共享资源的任务始终以该顺序获取和释放.相比之下,使用信号量的任务要么发信号要么等待,而不是两者.例如,任务1可以包含在按下"电源"按钮时发布(即,发信号或递增)特定信号量的代码,并且唤醒显示的任务2在相同的信号量上挂起.在这种情况下,一个任务是事件信号的生产者; 另一个是消费者.
...
这里有一个重要的观点,即互斥体以一种糟糕的方式干扰实时操作系统,导致优先级倒置,其中由于资源共享,可能在更重要的任务之前执行不太重要的任务.简而言之,当优先级较低的任务使用互斥锁获取资源A时,会发生这种情况,然后尝试抓取B,但由于B不可用而暂停.在它等待的时候,一个更高优先级的任务出现并且需要A,但它已经被捆绑,并且由于它正在等待B而没有运行的进程.有很多方法可以解决这个问题,但它通常是修复的通过更改互斥锁和任务管理器.在这些情况下,互斥锁比二进制信号量复杂得多,
...
互斥体和信号量之间广泛的现代混淆的原因是历史性的,因为它可以追溯到1974年由Djikstra发明的Semaphore(资本"S",在本文中).在该日期之前,计算机科学家所知的中断安全任务同步和信令机制都不能有效地扩展以供两个以上任务使用.Dijkstra革命性的,安全可扩展的Semaphore应用于关键部分保护和信令.因此混乱开始了.
然而,在基于优先级的抢占式RTOS(例如,VRTX,大约1980年)出现,发布建立RMA的学术论文和优先级倒置引起的问题之后,操作系统开发人员后来变得明显,以及关于优先级的论文继承协议在1990年,3显而易见,互斥量必须不仅仅是具有二进制计数器的信号量.
互斥:资源共享
信号量:信令
如果不仔细考虑副作用,请不要将其中一种用于另一种.
互斥锁:独占成员访问资源
信号量:n成员访问资源
也就是说,互斥锁可用于同步对计数器,文件,数据库等的访问.
sempahore可以做同样的事情,但支持固定数量的同时呼叫者.例如,我可以将数据库调用包装在信号量(3)中,这样我的多线程应用程序将以最多3个同时连接命中数据库.所有尝试都将阻止,直到三个插槽中的一个打开.他们做的事情就像做天真的节流真的很容易.
@Craig:
信号量是一种锁定资源的方法,以确保在执行一段代码时,只有这段代码才能访问该资源.这使得两个线程不会同时访问资源,这可能会导致问题.
这不仅限于一个线程.信号量可以配置为允许固定数量的线程访问资源.
考虑一下,可以容纳总共3个(后方)+2(前方)人员的出租车,包括司机.因此,一次semaphore
只允许5个人在车内.并且mutex
只允许一个人坐在汽车的一个座位上.
因此,Mutex
允许对资源(如OS线程)进行独占访问,同时Semaphore
允许一次访问n个资源.
信号量也可以用作...信号量.例如,如果您有多个进程将数据排入队列,并且只有一个任务从队列中消耗数据.如果您不希望您的使用任务不断轮询队列以获取可用数据,则可以使用信号量.
这里信号量不是用作排除机制,而是用作信令机制.消耗任务正在等待信号量生成任务正在信号量上发布.
这样,当且仅当有数据要出列时,消耗任务才会运行
构建并发程序有两个基本概念 - 同步和互斥.我们将看到这两种类型的锁(信号量通常是一种锁定机制)如何帮助我们实现同步和互斥.
信号量是一种编程结构,它通过实现同步和互斥来帮助我们实现并发.信号量有两种类型,Binary和Counting.
信号量有两部分:计数器和等待访问特定资源的任务列表.信号量执行两个操作:wait(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个任务 - 您希望在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,可以基于某些特定的优先级等.(注意:考虑如何安排你的P和V;如果你想要以某种特定的顺序执行t1和t2,并且你不知道信号量的实现)
(找出:如果V的数量大于P的数量,会发生什么?)
相互排斥使用计数信号量:我希望你为此构建自己的伪代码(让你更好地理解事物!) - 但基本概念是这样的:计数器的计数信号量= N允许N个任务自由地进入临界区.这意味着你有N个任务(或线程,如果你愿意)进入关键部分,但是第N + 1个任务被阻止(进入我们最喜欢的被阻止任务列表),只有当有人V是信号量时才通过至少一次.所以信号量计数器,而不是在0和1之间摆动,现在介于0和N之间,允许N个任务自由进入和退出,阻止任何人!
天哪,你为什么需要这么蠢的东西?是不是要让不止一个人访问资源的全部互斥点?(提示提示......你的计算机中并不总是只有一个驱动器,你......?)
想一想:单独计算信号量可以实现互斥吗?如果您有10个资源实例,并且有10个线程进入(通过计数信号量)并尝试使用第一个实例,该怎么办?
信号量是包含自然数(即大于或等于零的整数)的对象,在该自然数上定义了两个修改操作.一次操作,V
给自然增加1.另一个操作,P
将自然数减少1.两个活动都是原子的(即没有其他操作可以与a V
或a 同时执行P
).
因为自然数0不能减少,所以调用P
包含0的信号量将阻止调用进程(/ thread)的执行,直到该数字不再为0并且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
本文中的示例代码