关于多线程:什么是信号量?

What is a semaphore?

信号量是一种编程概念,经常用于解决多线程问题。 我对社区的问题:

什么是信号量,如何使用?


将信号量视为夜总会里的蹦蹦跳跳。一次有大量的人员被允许进入俱乐部。如果俱乐部已满,则不允许任何人进入,但是一旦一个人离开,另一个人可能会进入。

这只是限制特定资源使用者数量的一种方法。例如,限制同时调用应用程序中的数据库的次数。

这是C#中非常教学法的示例:-)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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)揭秘的互斥量和信号量文章是一个简短的简短介绍,介绍了互斥量和信号量与众不同的原因以及何时以及不应该使用它们。我在这里摘录了几个关键段落。

关键是互斥锁应用于保护共享资源,而信号灯应用于信令。通常,您不应使用信号量来保护共享资源,也不应使用互斥体来发信号。例如,在使用信号量来保护共享资源方面,保镖类比存在一些问题-您可以通过这种方式使用它们,但可能会导致难以诊断错误。

While mutexes and semaphores have some similarities in their implementation, they should always be used differently.

The most common (but nonetheless incorrect) answer to the question posed at the top is that mutexes and semaphores are very similar, with the only significant difference being that semaphores can count higher than one. Nearly all engineers seem to properly understand that a mutex is a binary flag used to protect a shared resource by ensuring mutual exclusion inside critical sections of code. But when asked to expand on how to use a"counting semaphore," most engineers—varying only in their degree of confidence—express some flavor of the textbook opinion that these are used to protect several equivalent resources.

...

在这一点上,使用浴室钥匙作为保护共享资源(浴室)的想法做出了一个有趣的类比。如果商店只有一个浴室,那么一个钥匙就足以保护该资源并防止多个人同时使用它。

如果有多个浴室,则可能会想像一个浴室那样对它们进行键控并做出多个键-这类似于信号灯被滥用。拥有钥匙后,您实际上并不知道哪个浴室可用,如果您沿着这条路走,您可能最终将使用互斥锁来提供该信息,并确保您不上已被占用的浴室。

信号量是保护几个基本相同的资源的错误工具,但这是许多人想到并使用它的人。保镖的类比明显不同-没有几种相同类型的资源,而是有一种资源可以接受多个同时用户。我想可以在这种情况下使用信号量,但在现实世界中很少有此类比喻真正成立的情况-经常有几种相同类型,但仍然有个别资源(例如浴室)无法使用这条路。

...

The correct use of a semaphore is for signaling from one task to another. A mutex is meant to be taken and released, always in that order, by each task that uses the shared resource it protects. By contrast, tasks that use semaphores either signal or wait—not both. For example, Task 1 may contain code to post (i.e., signal or increment) a particular semaphore when the"power" button is pressed and Task 2, which wakes the display, pends on that same semaphore. In this scenario, one task is the producer of the event signal; the other the consumer.

...

这里要指出的一点是,互斥锁会以一种不好的方式干扰实时操作系统,从而导致优先级倒置,由于资源共享,优先级较低的任务可能会在优先级较高的任务之前执行。简而言之,当优先级较低的任务使用互斥锁抢??占资源A,然后尝试抢夺B,但由于B不可用而暂停时,就会发生这种情况。在等待期间,出现了一个更高优先级的任务,需要A,但是它已经被捆绑,并且由于等待B而无法运行。有很多方法可以解决此问题,但是通常它是固定的通过更改互斥锁和任务管理器。在这种情况下,互斥锁比二进制信号量要复杂得多,并且在这种情况下使用信号量会导致优先级倒置,因为任务管理器没有意识到优先级倒置并且无法采取措施对其进行纠正。

...

The cause of the widespread modern confusion between mutexes and semaphores is historical, as it dates all the way back to the 1974 invention of the Semaphore (capital"S", in this article) by Djikstra. Prior to that date, none of the interrupt-safe task synchronization and signaling mechanisms known to computer scientists was efficiently scalable for use by more than two tasks. Dijkstra's revolutionary, safe-and-scalable Semaphore was applied in both critical section protection and signaling. And thus the confusion began.

However, it later became obvious to operating system developers, after the appearance of the priority-based preemptive RTOS (e.g., VRTX, ca. 1980), publication of academic papers establishing RMA and the problems caused by priority inversion, and a paper on priority inheritance protocols in 1990, 3 it became apparent that mutexes must be more than just semaphores with a binary counter.

互斥体:资源共享

信号量:信号

在未仔细考虑副作用的情况下,请勿将另一种药物用于另一种药物。


Mutex:对资源的独占成员访问

信号量:对资源的n成员访问

也就是说,互斥锁可用于同步访问计数器,文件,数据库等。

一个sempahore可以做同样的事情,但是支持固定数量的同时呼叫者。例如,我可以将数据库调用包装在semaphore(3)中,这样我的多线程应用程序最多可以同时连接3个数据库。所有尝试将阻塞,直到打开三个插槽之一。它们使像天真节流这样的事情变得非常非常容易。


考虑一下,一辆出租车可以容纳总共3(后)+2(前)人,包括驾驶员。因此,semaphore一次只能允许5个人进入车内。
mutex仅允许一个人坐在汽车的一个座位上。

因此,mutex允许对资源(如OS线程)的独占访问,而semaphore允许一次访问n个资源。


@克雷格:

A semaphore is a way to lock a
resource so that it is guaranteed that
while a piece of code is executed,
only this piece of code has access to
that resource. This keeps two threads
from concurrently accesing a resource,
which can cause problems.

这不仅限于一个线程。可以将信号量配置为允许固定数量的线程访问资源。


信号量也可以用作...信号量。
例如,如果您有多个进程将数据放入队列中,而只有一个任务在使用该队列中的数据。如果您不希望自己的消费任务不断在队列中轮询可用数据,则可以使用信号灯。

在此,信号灯不用作排除机制,而是用作信号传递机制。
消耗任务正在等待信号量
生产任务正在信号灯上发布。

这样,消费任务仅在有数据要出队时才运行


构建并发程序有两个基本概念-同步和互斥。我们将看到这两种类型的锁(信号灯通常是一种锁定机制)如何帮助我们实现同步和互斥。

好。

信号量是一种编程结构,可通过实现同步和互斥来帮助我们实现并发。信号量有两种类型,二进制和计数。

好。

信号量包括两个部分:一个计数器和一个等待访问特定资源的任务列表。信号量执行两项操作:wait(P)[就像获取锁一样],以及release(V)[类似于释放锁] –这是一个可以对信号量执行的仅有的两项操作。在二进制信号量中,计数器在逻辑上介于0和1之间。您可以认为它类似于具有两个值的锁:打开/关闭。计数信号量具有多个计数值。

好。

要理解的重要一点是,信号量计数器跟踪不必阻塞的任务数,即它们可以取得进展。任务被阻止,并且仅在计数器为零时才将其自己添加到信号量列表中。因此,如果任务无法进行,则将其添加到P()例程的列表中,然后使用V()例程"释放"任务。

好。

现在,很明显地看到如何使用二进制信号量来解决同步和互斥-它们本质上是锁。

好。

例如同步:

好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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。因此,我们实现了同步。

好。

现在,让我们看一下带有二进制信号量的互斥:

好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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之后要执行。您将如何设计同步?

好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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个线程(通过计数信号量)进入并尝试使用第一个实例怎么办?

好。

好。


信号量是包含自然数(即大于或等于零的整数)的对象,在其上定义了两个修改操作。一个操作V将自然数加1。另一个操作P将自然数减少1。这两个活动都是原子的(即,不能与VP同时执行其他操作)。

因为自然数0不能减少,所以在包含0的信号量上调用P会阻止调用进程(/ thread)的执行,直到某个时候该数字不再为0并且P可以成功(和原子)执行。

如其他答案中所述,信号量可用于将对特定资源的访问限制为最大(但可变)进程数。


我创建了可视化效果,应该可以帮助您理解这个想法。 信号量控制在多线程环境中对公共资源的访问。
enter image description here

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
ExecutorService executor = Executors.newFixedThreadPool(6);

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();

输出量

1
2
3
4
5
6
Semaphore acquired
Semaphore acquired
Semaphore acquired
Semaphore acquired
Could not acquire semaphore
Could not acquire semaphore

该文章的示例代码


硬件或软件标志。在多任务系统中,信号量是变量,其值指示公共资源的状态。需要该资源的进程检查该信号量以确定资源状态,然后决定如何进行操作。


信号量就像线程限制器一样。

示例:如果您有100个线程池,并且想要执行一些数据库操作。 如果在给定的时间有100个线程访问数据库,那么数据库中可能存在锁定问题,因此我们可以使用信号量,该信号量一次仅允许有限的线程。在下面的示例中,一次仅允许一个线程。 当线程调用acquire()方法时,它将获得访问权限,并且在调用release()方法之后,它将释放访问权限,以便下一个线程将获得访问权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
    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上查看讲义。


这是一个古老的问题,但是信号量最有趣的用途之一是读/写锁,并且没有明确提及。

R / W锁的工作方式很简单:一个读者消耗一个许可证,而作家则消耗所有许可证。
确实,一个简单的r / w锁实现方式,但是需要对读取的元数据进行修改(实际上是两次),这可能会成为瓶颈,仍然比互斥锁或锁好得多。

另一个缺点是,编写者也可以很容易地开始工作,除非信号量是公平的,或者写请求在多个请求中获得许可,在这种情况下,他们之间需要显式的互斥体。

进一步阅读:


信号量是一种锁定资源的方法,可以确保在执行一段代码时,只有该段代码可以访问该资源。这样可以防止两个线程同时访问资源,这可能会导致问题。


推荐阅读