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

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


当前回答

Michael Barr的文章《互斥量和信号量揭秘》很好地介绍了互斥量和信号量的不同之处,以及什么时候应该和不应该使用它们。我在这里摘录了几个关键段落。

关键在于应该使用互斥来保护共享资源,而应该使用信号量来发送信号。通常不应该使用信号量来保护共享资源,也不应该使用互斥量来发送信号。例如,在使用信号量来保护共享资源方面,使用bouncer类比是有问题的——您可以这样使用它们,但这可能会导致难以诊断错误。

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.

...

在这一点上,一个有趣的类比是使用浴室钥匙的想法来保护共享资源-浴室。如果一家商店只有一间浴室,那么一把钥匙就足以保护这一资源,防止多人同时使用。

如果有多个浴室,人们可能会试图用相同的键来设置多个键——这类似于误用信号量。一旦你有了一个键,你实际上不知道哪个浴室是可用的,如果你沿着这条路走下去,你可能最终会使用互斥锁来提供该信息,并确保你没有使用已经被占用的浴室。

A semaphore is the wrong tool to protect several of the essentially same resource, but this is how many people think of it and use it. The bouncer analogy is distinctly different - there aren't several of the same type of resource, instead there is one resource which can accept multiple simultaneous users. I suppose a semaphore can be used in such situations, but rarely are there real-world situations where the analogy actually holds - it's more often that there are several of the same type, but still individual resources, like the bathrooms, which cannot be used this way.

...

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.

...

Here an important point is made that mutexes interfere with real time operating systems in a bad way, causing priority inversion where a less important task may be executed before a more important task because of resource sharing. In short, this happens when a lower priority task uses a mutex to grab a resource, A, then tries to grab B, but is paused because B is unavailable. While it's waiting, a higher priority task comes along and needs A, but it's already tied up, and by a process that isn't even running because it's waiting for B. There are many ways to resolve this, but it most often is fixed by altering the mutex and task manager. The mutex is much more complex in these cases than a binary semaphore, and using a semaphore in such an instance will cause priority inversions because the task manager is unaware of the priority inversion and cannot act to correct it.

...

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.

互斥:资源共享

信号:信号

在没有仔细考虑副作用的情况下,不要用一种药物来替代另一种药物。

其他回答

信号量也可以用作…信号量。 例如,如果有多个进程将数据排队到队列中,而只有一个任务使用队列中的数据。如果您不希望您的消费任务不断地轮询队列以获取可用数据,您可以使用信号量。

在这里,信号量不是用作排除机制,而是用作信号机制。 消费任务正在等待信号量 生产任务正在发送信号量。

这样,当且仅当有数据要退出队列时,消费任务才会运行

互斥:对资源的独占成员访问

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

也就是说,互斥可以用来同步对计数器、文件、数据库等的访问。

信号量可以做同样的事情,但支持固定数量的同时调用者。例如,我可以将我的数据库调用包装在一个信号量(3)中,这样我的多线程应用程序将最多3个同时连接到数据库。所有的尝试都将被阻塞,直到三个插槽中的一个打开。它们让简单的节流变得非常简单。

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

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

信号量有两个部分:计数器和等待访问特定资源的任务列表。一个信号量执行两个操作:等待(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个任务——#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();
}

So if your semaphore starts off closed, you ensure that t1 and t2 block, get added to the semaphore's list. Then along comes all important t3, finishes its business and frees t1 and t2. What order are they freed in? Depends on the implementation of the semaphore's list. Could be FIFO, could be based some particular priority,etc. (Note : think about how you would arrange your P's and V;s if you wanted t1 and t2 to be executed in some particular order, and if you weren't aware of the implementation of the semaphore)

(找出:如果V的数量大于P的数量会发生什么?)

Mutual Exclusion Using counting semaphores: I'd like you to construct your own pseudocode for this (makes you understand things better!) - but the fundamental concept is this : a counting semaphore of counter = N allows N tasks to enter the critical section freely. What this means is you have N tasks (or threads, if you like) enter the critical section, but the N+1th task gets blocked (goes on our favorite blocked-task list), and only is let through when somebody V's the semaphore at least once. So the semaphore counter, instead of swinging between 0 and 1, now goes between 0 and N, allowing N tasks to freely enter and exit, blocking nobody!

天啊,你为什么需要这么愚蠢的东西?互斥的全部意义不就是不让多于一个人访问资源吗?(提示提示…你的电脑里并不总是只有一个驱动器,对吧?)

思考一下:互斥是通过单独使用计数信号量实现的吗?如果您有一个资源的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

本文中的示例代码

把信号量想象成夜总会的保镖。同时允许进入俱乐部的人数是固定的。如果俱乐部满了,任何人都不允许进入,但只要一个人离开,另一个人就可以进入。

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

下面是一个非常适合教学的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);
        }
    }
}