需要了解SemaphoreSlim的使用方法

152

这里是我目前的代码,但我不理解SemaphoreSlim在做什么。

async Task WorkerMainAsync()
{
    SemaphoreSlim ss = new SemaphoreSlim(10);
    List<Task> trackedTasks = new List<Task>();
    while (DoMore())
    {
        await ss.WaitAsync();
        trackedTasks.Add(Task.Run(() =>
        {
            DoPollingThenWorkAsync();
            ss.Release();
        }));
    }
    await Task.WhenAll(trackedTasks);
}

void DoPollingThenWorkAsync()
{
    var msg = Poll();
    if (msg != null)
    {
        Thread.Sleep(2000); // process the long running CPU-bound job
    }
}
< p > < code > await ss.WaitAsync(); 和 < code > ss.Release(); 是什么? < p > 如果我同时运行50个线程,然后编写代码如< code > SemaphoreSlim ss = new SemaphoreSlim(10); ,那么它将强制在任何时候只执行10个活动线程。 < p > 当10个线程中的一个完成时,另一个线程将开始。如果我不正确,请帮助我用示例情况理解。 < p > 为什么需要连同 < code >ss.WaitAsync(); 使用 awaitss.WaitAsync();是做什么的?

11
需要注意的一点是,你真的应该在一个 "try { DoPollingThenWorkAsync(); } finally { ss.Release(); }" 中包装那个 "DoPollingThenWorkAsync();",否则异常将永久地使信号量饥饿。 - Austin Salgat
2
我感到有点奇怪,我们分别在任务内外获取和释放信号量。将 "await ss.WaitAsync()" 移动到任务内部会有什么区别吗? - Shane Lu
3个回答

325
在街角的幼儿园里,他们使用一个SemaphoreSlim控制PE房间内可以玩耍的小孩数量。
他们在房间外地面上画了5对脚印。
当孩子们到达时,他们把鞋子留在一对空闲的脚印上,然后进入房间。
一旦他们完成游戏,他们就会出来,拿回自己的鞋子,并“释放”一个位置给另一个孩子。
如果有孩子到达时没有剩余的脚印,他们会选择到别处游玩或者等待一段时间并每隔一段时间检查(即,没有FIFO优先级)。
当老师在场时,她会在走廊的另一侧“释放”另外一排5个脚印,以便另外5个孩子也能同时玩耍。
它也有SemaphoreSlim相同的“陷阱”...
如果一个孩子结束游戏并离开房间而没有取回鞋子(即未触发“释放”),则该位置仍然被占用,尽管理论上存在一个空位置。但是通常老师会训斥这个孩子。
有时候一两个狡猾的孩子会把他们的鞋子藏在其他地方,进入房间,即使所有的脚印已经被占用(即,SemaphoreSlim实际上并没有“真正”控制房间内的孩子数量)。
这通常不会有好结果,因为房间过于拥挤往往导致孩子们哭泣,老师也会完全关闭房间。

85
这些回答是我最喜欢的。 - My Stack Overfloweth
1
热爱它!来自上帝的一位教师 - ukie
3
请解释一下如何使用 SemaphoreSlim 实现这个功能?"SemaphoreSlim 实际上并不完全控制房间里孩子的数量"。 - hIpPy
4
如果您要锁定的是非托管资源,则 SemaphoreSlim 不会获取操作系统级别的锁(因此称为“slim”)。它只能直接阻止其控制的入口。 - Greg

100
那是正确的; 使用信号量Semaphore确保不会有超过10个工作者同时执行此项工作。
在Semaphore上调用WaitAsync会产生一个任务,该任务将在该线程被授予该令牌的“访问”权限时完成。等待该任务允许程序在“允许”时继续执行。拥有异步版本而不是调用Wait很重要,因为这样可以确保方法保持异步,而不是同步,并且处理了由于回调而导致异步方法可以跨多个线程执行代码的事实,因此Semaphore自然的线程关联性可能成为问题。
另外注意: DoPollingThenWorkAsync不应该有Async后缀,因为它实际上不是异步的,而是同步的。只需将其命名为DoPollingThenWork即可减少读者的困惑。

谢谢,但请告诉我当我们指定要运行的线程数为10时会发生什么。当其中一个10个线程完成后,那个线程会再次跳转以完成另一个任务还是返回到池中?这不是很清楚...所以请解释一下幕后发生了什么。 - Mou
@Mou,这段代码有什么不清楚的吗?它会等待当前运行的任务少于10个,然后再添加一个任务。当一个任务完成时,它会指示已经完成。就是这样。 - Servy
指定线程数运行的优势是什么?如果线程过多可能会影响性能,为什么会影响呢?如果我运行50个线程而不是10个线程,那么为什么性能会受到影响,请解释一下。谢谢。 - Thomas
4
如果同时进行的线程太多,那么这些线程花费的时间就会更多地用于上下文切换,而不是用于有效的工作。随着线程数的增加,吞吐量会降低,因为你所花费的时间越来越多地用于管理线程,而不是完成工作,至少在线程数量超过机器核心数之后是这样的。 - Servy
3
这是任务调度器的工作之一。任务不等同于线程。原始代码中的 Thread.Sleep 会破坏任务调度器。如果你不是异步的核心,那么你就不是异步的。 - Joseph Lennox
显示剩余21条评论

11

虽然我接受这个问题实际上与倒计时锁定场景有关,但是我认为值得分享一下我发现的链接,供那些希望使用SemaphoreSlim作为简单异步锁的人使用。它允许您使用using语句,这可能使编码更整洁、更安全。

http://www.tomdupont.net/2016/03/how-to-release-semaphore-with-using.html

在Dispose中,我交换了_isDisposed=true_semaphore.Release(),以防万一它被多次调用。

还需要注意的是,SemaphoreSlim不是可重入锁,这意味着如果同一线程多次调用WaitAsync,则信号量的计数会每次减少。简而言之,SemaphoreSlim不具备线程感知能力。

至于代码质量方面,最好将Release放在try-finally的finally中,以确保它始终被释放。


8
仅发布链接答案是不明智的,因为链接往往会随着时间的推移而失效,从而使答案变得毫无价值。如果可以的话,最好将关键要点或核心代码块概括在回答中。 - ProgrammingLlama

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接