什么是信号量?
信号量是经常用于解决multithreading问题的编程概念。 我对社区的问题:
什么是信号量,你如何使用它?
把信号灯想象成夜总会的保镖。 在俱乐部里有一个专用的人数。 如果俱乐部已满,则不得进入,但只要一个人离开另一个人就可以进入。
这只是一种限制特定资源消费者数量的方法。 例如,限制在应用程序中同时调用数据库的次数。
这是一个非常教学的例子在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这篇文章简单介绍了互斥体和信号量的不同之处,以及它们是否应该被使用。 我在这里摘录了几个关键段落。
关键在于互斥信号应该用来保护共享资源,而信号量则用于信号传输。 您通常不应该使用信号来保护共享资源,也不能使用信号互斥。 举例来说,在使用信号量来保护共享资源方面,保镖类比存在一些问题 – 您可以这样使用它们,但是可能很难诊断错误。
虽然互斥体和信号量在实现上有一些相似之处,但它们的使用方式总是不同的。
上面提出的问题最常见的(但是是不正确的)答案是互斥体和信号量是非常相似的,唯一显着的区别是信号量可以高于一个。 几乎所有的工程师似乎都明白互斥是一种二进制标志,通过确保代码的关键部分之间的相互排斥来保护共享资源。 但是当被要求扩展如何使用“计数信号量”时,大多数工程师只是在信心程度上有所不同,expression了一些教科书意见的味道,这些意见是用来保护几个同等资源的。
…
在这一点上,一个有趣的比喻是使用浴室钥匙的想法来保护共享资源 – 卫生间。 如果一个商店有一间浴室,那么一个密钥就足以保护这个资源,并防止多个人同时使用它。
如果有多个卫生间,可能会试图locking他们并制作多个密钥 – 这与信号量被误用相似。 一旦你有了钥匙,你实际上并不知道哪个浴室是可用的,如果你沿着这条路走下去,你可能最终会使用互斥体来提供这些信息,并确保你不要占用已经占用的浴室。
信号量是保护几个本质上相同的资源的错误工具,但这是多less人想到它并使用它。 保镖类比明显不同 – 没有几个相同types的资源,而是有一个资源可以接受多个并发用户。 我认为在这种情况下可以使用信号量,但很less有类比实际存在的现实世界情况 – 更常见的是有几个相同的types,但仍然是单独的资源,如浴室,不能使用这条路。
…
正确使用信号量是从一个任务到另一个任务的信令。 每一个使用它所保护的共享资源的任务都要按照这个顺序进行互斥。 相比之下,使用信号量的任务不是信号就是等待 – 不是两者都有。 例如,任务1可以包含用于在按下“电源”button时发布(即,发信号或递增)特定信号量的代码,并且唤醒显示器的任务2依赖于相同的信号量。 在这种情况下,一个任务是事件信号的生产者; 其他的消费者。
…
这里重要的一点是,互斥体以不好的方式干扰实时操作系统,造成优先级反转,在重要的任务之前执行不太重要的任务,因为资源共享。 简而言之,当优先级较低的任务使用互斥锁来获取资源A时,会发生这种情况,然后尝试抓取B,但由于B不可用而被暂停。 当它在等待时,一个更高优先级的任务就会出现,并且需要A,但是它已经被绑定了,而且一个甚至没有运行的进程,因为它正在等待B.有很多方法可以解决这个问题,但是它通常是固定的通过改变互斥和任务pipe理器。 在这些情况下,互斥信号要比二元信号量复杂得多,在这种情况下使用信号量会导致优先级反转,因为任务pipe理器不知道优先级反转,不能采取措施纠正它。
…
互斥体和信号量之间广泛的现代混淆的原因是历史性的,因为它可以追溯到1974年Djikstra发明的信号量(本文中为“S”)。 在此之前,计算机科学家已知的中断安全任务同步和信号传输机制都不能有效地扩展以供两个以上的任务使用。 Dijkstra的革命性的,安全可扩展的Semaphore应用于临界区保护和信号传输。 于是混乱开始了。
然而,随后出现基于优先级的抢占式RTOS(例如,VRTX,大约1980),出版build立RMA的学术论文以及由优先级倒置引起的问题之后,操作系统开发人员显而易见,inheritance协议在1990年,3显而易见的是,互斥体必须不仅仅是具有二进制计数器的信号量。
互斥:资源共享
信号量:信号
如果没有仔细考虑副作用,不要使用其中一种。
互斥体:对资源的独占成员访问
信号量:n个成员访问资源
也就是说,可以使用互斥锁来同步对计数器,文件,数据库等的访问。
一个sempahore可以做同样的事情,但支持固定数量的同时呼叫者。 例如,我可以将数据库调用包装在一个信号量(3)中,以便multithreading应用程序最多可以同时连接3个数据库。 所有尝试都将阻塞,直到三个插槽中的一个打开。 他们让事情像做天真的节stream真的很容易。
@Craig:
信号量是一种locking资源的方式,以保证在执行一段代码时,只有这段代码才能访问该资源。 这使得两个线程同时访问一个资源,这可能会导致问题。
这不仅限于一个线程。 信号量可以configuration为允许固定数量的线程访问资源。
信号量也可以用作信号量。 例如,如果有多个进程将数据排入队列,并且只有一个任务使用队列中的数据。 如果您不希望消耗任务持续轮询队列中的可用数据,则可以使用信号量。
这里信号量不是用作排斥机制,而是用作信号机制。 消费任务正在等待信号量生产任务正在信号量上发布。
这样,当且仅当存在要出队的数据时,消费任务才运行
构build并发程序有两个基本概念 – 同步和互斥。 我们将看到这两种types的锁(信号量更通常是一种锁机制)如何帮助我们实现同步和互斥。
信号量是一种编程结构,通过实现同步和互斥,帮助我们实现并发。 信号量有二进制和计数两种types。
信号量有两部分:计数器和等待访问特定资源的任务列表。 一个信号量执行两个操作:wait(P)[这就像是获取一个锁],释放(V)(类似于释放一个锁) – 这是信号量中唯一可以执行的两个操作。 在二进制信号量中,计数器在逻辑上介于0和1之间。你可以把它看作与具有两个值的锁相似:打开/closures。 计数信号具有多个计数值。
重要的是,信号量计数器跟踪不必阻塞的任务数量,即可以取得进展。 任务阻塞,只有当计数器为零时才将自己添加到信号量列表中。 因此,如果任务无法进行,则会将任务添加到P()例程的列表中,并使用V()例程“释放”任务。
现在,看看如何使用二进制信号来解决同步和互斥问题是相当明显的 – 它们本质上是locking的。
恩。 同步:
thread A{ semaphore &s; //locks/semaphores are passed by reference! think about why this is so. A(semaphore &s): s(s){} //constructor foo(){ ... sP(); ;// some block of code B2 ... } //thread B{ semaphore &s; B(semaphore &s): s(s){} //constructor foo(){ ... ... // some block of code B1 sV(); .. } main(){ semaphore s(0); // we start the semaphore at 0 (closed) A a(s); B b(s); }
在上面的例子中,B2只能在B1完成执行后才能执行。 比方说,线程A来执行第一个 – 获取到sem.P(),并等待,因为计数器是0(closures)。 线程B出现,完成B1,然后释放线程A – 然后完成B2。 所以我们实现同步。
现在我们来看看一个二进制信号量的互斥问题:
thread mutual_ex{ semaphore &s; mutual_ex(semaphore &s): s(s){} //constructor foo(){ ... sP(); //critical section sV(); ... ... sP(); //critical section sV(); ... } main(){ semaphore s(1); mutual_ex m1(s); mutual_ex m2(s); }
互斥也很简单 – m1和m2不能同时进入临界区。 所以每个线程都使用相同的信号量为其两个关键部分提供相互排斥。 现在,是否有可能有更大的并发? 取决于关键部分。 (想想如何使用信号量来实现互斥……提示提示:我是否只需要使用一个信号量?)
计数信号量:具有多个值的信号量。 让我们看看这是什么意思 – 一个多个值的锁? 所以打开,closures,…嗯。 在互斥或同步中使用多级locking的是什么?
让我们更容易的两个:
使用计数信号量进行同步:假设您有3个任务 – 您要在3之后执行的#1和2。您将如何devise您的同步?
thread t1{ ... sP(); //block of code B1 thread t2{ ... sP(); //block of code B2 thread t3{ ... //block of code B3 sV(); sV(); }
所以如果你的信号量开始closures,你确保t1和t2块,被添加到信号量的列表。 然后,所有重要的t3,完成其业务,并释放t1和t2。 他们释放了什么命令? 取决于信号量列表的执行情况。 可能是FIFO,可能是基于某些特定的优先级等。 (注意:想想如何安排你的P和V;如果你想以某种顺序执行t1和t2,并且如果你不知道信号量的实现)
(发现:如果V的数量大于P的数量,会发生什么?)
相互排斥使用计数信号量:我希望你为此构build自己的伪代码(让你更好地理解事物!) – 但是基本的概念是这样的:counter = N的计数信号允许N个任务自由进入临界区。 这意味着你有N个任务(或线程,如果你喜欢的话)进入关键部分,但是第N + 1个任务被阻塞(进入我们最喜欢的阻塞任务列表),只有当有人V的信号量至less一次。 所以信号量计数器,而不是0和1之间的摆动,现在在0和N之间,允许N个任务自由进出,阻止任何人!
现在天哪,你为什么需要这样一个愚蠢的东西? 互相排斥的关键不是让一个人获得资源吗? (提示提示…你并不总是只有一个驱动器在你的电脑,你呢?)
想一想 :只有计算信号量才能实现互斥? 如果你有10个资源实例,10个线程进入(通过计数信号量)并尝试使用第一个实例?
信号量是包含自然数(即大于或等于零的整数)的对象,在该对象上定义了两个修改操作。 一个操作V
将自然数加1。 另一个操作P
将自然数减less1.这两个活动都是primefaces的(即没有其他操作可以与V
或P
同时执行)。
因为自然数0不能减less,所以在包含0的信号量上调用P
会阻塞调用进程(/线程)的执行,直到数字不再为0的时刻,并且P
可以被成功(和primefaces地)执行。
正如其他答案中所提到的,信号量可以用来将对特定资源的访问限制为最大(但是可变)的进程数量。
硬件或软件标志。 在多任务处理系统中,信号量是可变的,具有指示公共资源状态的值。需要资源的进程检查信号量以确定资源状态,然后决定如何进行。
所以想象每个人都想去卫生间,浴室只有一定数量的钥匙。 现在如果没有足够的钥匙,那个人需要等待。 所以把信号量看作代表不同进程(卫浴业者)可以请求访问的浴室(系统资源)可用键集。
现在设想两个进程试图同时去卫生间。 这不是一个好的情况,信号灯用来防止这种情况。 不幸的是,信号量是一个自愿的机制和过程(我们的浴室的人)可以忽略它(即使有钥匙,有人仍然可以打开门)。
二进制/互斥和计数信号也有区别。
查看http://www.cs.columbia.edu/~jae/4118/lect/L05-ipc.html上的讲义。;
考虑一下,一辆出租车。 出租车最多可容纳3名(后方)+2(前方)人员,包括司机。 所以,信号量一次只允许5个人在车内。 而一个互斥体只允许一个人坐在车内的一个座位上。 所以,互斥是允许一个资源的独占访问。 信号量允许一次访问n个资源。
在编程中,特别是在Unix系统中,信号量是一种协调或同步多个进程竞争相同操作系统资源的活动的技术。
信号量是操作系统存储器中指定位置的值,每个进程都可以检查并更改。 根据find的值,进程可以使用资源或者意识到它已经被使用,并且在再次尝试之前等待一段时间。
信号可以是二进制(0或1),也可以有其他值。 通常,使用信号量的进程会检查该值,然后如果使用该资源,则更改该值以反映该值,以便随后的信号量用户知道要等待。
信号量通常用于两个目的:1)共享一个共同的内存空间2)共享访问文件。
信号量是进程间通信(IPC)的技术之一。
C编程语言为pipe理信号量提供了一组接口或“function”。
信号量是一种locking资源的方式,以保证在执行一段代码时,只有这段代码才能访问该资源。 这使得两个线程同时访问一个资源,这可能会导致问题。
这是一个老问题,但信号量的最有趣的用途之一是读/写锁,并没有明确提到。
读写锁以简单的方式工作:为读者准备一张许可证,并为作者准备所有许可证。 事实上,一个简单的ar / w锁实现,但需要读取元数据(实际上是两次)修改,这可能成为一个瓶颈,仍然比一个互斥锁或锁更好。
另一个缺点是作者可以很容易地启动,除非信号量是公平的,或者写入获得多个请求的许可,在这种情况下,他们之间需要明确的互斥。
进一步阅读 :
这里有一些关于信号量的更有趣的知识:
信号量是由EW Dijkstra在20世纪60年代末期devise的编程构造。 迪克斯特拉的模式是铁路运营:考虑一段铁路,在这条铁路上有一条单一的轨道,一次只允许有一列火车。
守护这条赛道是一个信号量。 在进入单轨之前,列车必须等待,直到信号灯处于允许行驶的状态。 当列车进入轨道时,信号灯改变状态,防止其他列车进入轨道。 离开这段轨道的列车必须再次改变信号灯的状态,以允许另一列火车进入。
在计算机版本中,信号量似乎是一个简单的整数。 一个线程等待进程的许可,然后通过在信号量上执行一个P操作来发出信号。
操作的语义是这样的,线程必须等到信号量的值为正,然后通过从信号量中减去1来改变信号量的值。 当完成时,线程执行一个V操作,通过加一个信号量来改变信号量的值。 这些操作是以primefaces方式进行是至关重要的 – 它们不能被细分为在信号量之间可以发生其他动作的部分。 在P操作中,信号量的值在递减之前必须是正的(导致保证为非负的值,并且比递减前的值小1)。
在P和V操作中,算术必须在没有干扰的情况下进行。 如果两个V操作在同一个信号量上同时执行,则净效应应该是信号量的新值比它大两倍。
因为Dijkstra是荷兰人,所以P和V的记忆意义对世界大多数人来说是不清楚的。 然而,为了获得真正的奖学金:P代表prolagen,这是一个来源于探索者的构词,意思是试图减less。 V代表verhogen,意思是增加。 这在Dijkstra的技术笔记之一EWD 74中有讨论。
sem_wait(3RT)和sem_post(3RT)对应于Dijkstra的P和V操作。 sem_trywait(3RT)是P操作的条件forms:如果调用线程无法等待信号量减less,则调用立即返回非零值。
有两种基本types的信号量:二进制信号量,它从来不会取零或一的值,计数信号量,可以取任意的非负值。 二进制信号量在逻辑上就像一个互斥量。
但是,虽然不强制执行,但只有持有锁的线程才能解锁互斥锁。 没有“持有信号量的线程”的概念,所以任何线程都可以执行V(或sem_post(3RT))操作。
计数信号量与条件variables(与互斥量一起使用)一样强大。 在许多情况下,使用计数信号量而不是使用条件variables来实现时,代码可能会更简单(如下几个示例所示)。
但是,当一个互斥体与条件variables一起使用时,会隐含一个包围,它清楚程序的哪一部分正在被保护。 这不一定是信号量的情况,这可能被称为并发编程 – 它是强大的,但非常容易以非结构化,不确定的方式使用。