信号量是一个经常被用来解决多线程问题的编程概念。我的问题是:
信号量是什么,如何使用?
信号量是一个经常被用来解决多线程问题的编程概念。我的问题是:
信号量是什么,如何使用?
将信号量视为夜店的保安。一次只允许进入一定数量的人。如果夜店已满,则不允许任何人进入,但是只要有一个人离开,另一个人就可以进入。
这只是一种限制特定资源消费者数量的方法。例如,为了限制应用程序中对数据库的同时调用数量。
以下是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);
}
}
}
这篇文章Mutexes and Semaphores Demystified由Michael Barr撰写,是对互斥锁和信号量之间的区别以及何时应该使用它们进行了简短介绍。我在此摘录了几个关键段落。
关键点在于,互斥锁应该用于保护共享资源,而信号量应该用于信号传递。通常不应该使用信号量来保护共享资源,也不应该使用互斥锁进行信号传递。例如,在使用信号量来保护共享资源方面存在问题,比如使用这种方法可能会导致难以诊断的错误。
虽然互斥锁和信号量在实现上有些相似之处,但它们应该始终以不同的方式使用。
对于置顶问题的最常见(但仍不正确)答案是,互斥锁和信号量非常相似,唯一的显著区别是信号量可以计数高于1。几乎所有工程师似乎都正确地理解,互斥锁是用于通过确保代码的临界部分内的互斥排斥来保护共享资源的二进制标志。但是,当被问及如何使用“计数信号量”时,大多数工程师(只是在信心程度上有所不同)表达了一些文本书的观点,即这些信号量用于保护几个等效资源。
...
此时,我们可以使用一个有趣的类比来解释保护共享资源的想法——浴室钥匙。如果商店只有一个浴室,那么一把钥匙就足以保护这个资源,防止多人同时使用。...
信号量的正确使用是用于在一个任务和另一个任务之间发出信号。互斥锁则是被每个使用它所保护的共享资源的任务按顺序获取和释放。与此相反,使用信号量的任务只会发送或等待,不会同时进行这两个操作。例如,任务1可能包含代码,当按下“电源”按钮时,将发布(即发出或递增)特定的信号量,而唤醒显示屏的任务2则等待同一信号量。在这种情况下,一个任务是事件信号的生产者,另一个任务是消费者。...
这里提出了一个重要观点,即互斥锁对实时操作系统有不良影响,会导致优先级反转,即较不重要的任务可能在更重要的任务之前执行,因为它们共享资源。简而言之,当较低优先级的任务使用互斥锁获取资源A,然后尝试获取B时,由于B不可用而被暂停。在等待期间,一个更高优先级的任务需要A,但A已经被占用,并且是由于等待B的进程占用的。有很多解决方法,但通常通过修改互斥锁和任务管理器来解决。在这些情况下,互斥锁比二进制信号量复杂得多,如果在这种情况下使用信号量,将导致优先级反转,因为任务管理器不知道优先级反转并且无法进行纠正。...
历史原因导致了现代对于互斥锁和信号量之间普遍的混淆。这可以追溯到1974年Djikstra发明信号量(本文中大写"S")。在此之前,计算机科学家所知道的所有可中断任务同步和信号机制都无法有效地扩展以用于超过两个任务。Dijkstra革命性、安全和可扩展的信号量被应用于关键部分保护和信号传递。从那时起,混淆就开始了。Mutex:独占式成员访问资源
Semaphore:n-成员访问资源
也就是说,Mutex可用于同步对计数器、文件、数据库等的访问。
Semaphore可以做同样的事情,但支持固定数量的同时调用者。例如,我可以将我的数据库调用包裹在semaphore(3)中,以便我的多线程应用程序最多使用三个并发连接访问数据库。所有尝试都将被阻塞,直到其中一个三个插槽之一被打开。它们使得像执行简单限流这样的操作变得非常容易。
@Craig:
信号量是一种锁定资源的方式,确保在执行一段代码时,只有这段代码可以访问该资源。这可以防止两个线程同时访问一个资源,从而导致问题。
信号量不仅限于单个线程。信号量可以配置为允许固定数量的线程访问资源。
信号量也可以被用作一种信号机制,比如如果你有多个进程将数据入队到一个队列中,但只有一个任务从队列中消耗数据。如果你不想让消耗任务不断地轮询可用数据,那么你可以使用信号量。
在这种情况下,信号量不是作为互斥机制而被使用,而是作为一种信号机制。即消耗任务在等待信号量,而生产任务在发布信号量。
这样一来,只有当队列中有数据需要出队时,消耗任务才会运行。
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 - 然后线程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操作)
(找出:如果V的数量大于P的数量会发生什么?)
使用计数信号量进行互斥:我想让您构建自己的伪代码(这样可以更好地理解!)-但根本概念是:计数信号量的计数器= N允许N个任务自由进入临界区。这意味着您有N个任务(或线程,如果您喜欢)进入临界区,但第N + 1个任务被阻止(进入我们最喜欢的阻塞任务列表),只有当有人至少V一次信号量时才能通过。因此,信号量计数器不是在0和1之间摆动,而是在0和N之间摆动,允许N个任务自由进出,不阻塞任何人!
现在,为什么需要这样一个愚蠢的东西呢?难道互斥的整个重点不是防止多个人访问资源吗?(提示提示...您的计算机上不总是只有一个驱动器,对吧...?)
思考一下:仅通过使用计数信号量就可以实现互斥吗?如果您有10个资源实例,并且有10个线程(通过计数信号量)尝试使用第一个实例,会发生什么?
我已经创建了一种可视化方式,可以帮助理解这个想法。 Semaphore 在多线程环境中控制对共享资源的访问。
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
这是一篇文章中的示例代码。
V
将自然数加1。另一个操作 P
将自然数减1。这两个活动都是原子性的(即在执行 V
或 P
时不能执行其他操作)。P
将阻塞调用进程/线程的执行,直到某个时刻该数字不再为0,并且可以成功(并原子地)执行 P
。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();
}
}
}