使用 lock、Monitor Pulse 和 Wait 同步线程

Posted

技术标签:

【中文标题】使用 lock、Monitor Pulse 和 Wait 同步线程【英文标题】:Using lock, Monitor Pulse and Wait to synchronize threads 【发布时间】:2021-10-11 06:30:46 【问题描述】:

我已经阅读了官方文档和大约 25 个教程,但我仍然在为如何同步使用 Monitor Pulse()Wait() 方法以及使用 lock 对象的 3 个线程而苦恼。

(是的,我知道还有其他同步技术,但这应该是可行的,这让我很沮丧)

这是我想出的简单“概念证明”想法。

假设我有三个线程,每个线程都有一个任务:

    Thread1 运行一个任务,打印出所有可被 3 整除的整数 Thread2 运行一个任务,打印出余数为 1 除以 3 Thread3 运行一个任务,打印出余数为 2 除以 3

我希望最终输出为:0,1,2,3,4,5,6,... 直到我可能选择的任何整数限制,但我们可以说 50 或 100 - 它不是问题。

我想更全面地了解 lock 与 Monitor.Wait() 和 Monitor.Pulse() 的机制以及它们如何协同工作。

如果我理解正确,当线程遇到lock(someObject) ... 时,如果它是那里的第一个关键区域,它将获得对该关键区域的独占访问权。任何其他在同一对象上遇到锁的线程都会在其各自的代码中卡在该行(即lock(someObject))上,对吗?

如果线程1有lock(someObject)调用Monitor.Wait(someObject),那么线程1释放锁并进入等待队列,对吗?那么如果任何其他线程(例如,线程 2)调用Monitor.Pulse(someObject),它会将线程 1 移动到 ready 队列中?

无论我尝试什么,似乎代码一直在无限等待/阻塞。

我想我的总结性问题是:

    是否需要多个锁对象才能使用PulseWait 同步三个线程? WaitPulse 在此代码中的位置?在用于迭代我们要打印的值的循环周围的锁内?在锁内,仅放置在条件内(例如,if (i % 3 == 2))?等等。

感谢您提供任何有用的意见!

更新(2021 年 8 月 7 日):

事实证明,鉴于我在单个文件中设置锁的方式,将锁设为静态是必要的。我很生气,因为我之前没有注意到这一点,但建议的在线文档(来自 Joe Albahari 的网站)非常有用。

【问题讨论】:

你可以看到lock真的是here。 您有任何实际的用例吗?出于所有意图和目的,您正在序列化工作,那么为什么首先使用线程? Monitor现在已经接近20岁了,所以10岁的资源应该是完全有效的。 之所以没有不到10年的资源是因为没有人再使用这些同步机制了,有更多的现代实践和库 您的帖子中有 8 个问号。你能把你的问题限制在 1 个以内吗?或编号它们或其他什么?如果我说“是”,你知道我刚刚回答了哪个特定问题吗? @LasseV.Karlsen - 在接近尾声时,我列出了总结性问题,并且它们已编号。从技术上讲,第二个确实包含多个问题,但它列出了可能的解决方案,以证明我实际上已经花时间思考它——我只是还没有找到一个实际的解决方案。所以第二个问题不是是/否问题。答案可能是“等待发生在 _______ 之后,而脉冲发生在 ______”,或者是代码示例。 【参考方案1】:

这里是一个比较简单的Wait/PulseAll例子:

object locker = new();
int i = 0;
bool finished = false;
Thread[] threads = Enumerable.Range(0, 3).Select(remainder => new Thread(() =>

    lock (locker)
    
        try
        
            do
            
                while (!finished && i % 3 != remainder) Monitor.Wait(locker);
                if (finished) break;
                Console.WriteLine($"Worker #remainder produced i");
                Monitor.PulseAll(locker);
             while (++i < 20);
        
        finally  finished = true; Monitor.PulseAll(locker); 
    
)).ToArray();
Array.ForEach(threads, t => t.Start());
Array.ForEach(threads, t => t.Join());

创建了三个工作线程,由 remainder 参数标识,取值 0、1 和 2。每个工作线程负责生成模数 3 等于余数的数字。

int i 是循环变量,bool finished 是一个标志,当任何工作程序完成时变为true。这个标志确保在任何一个worker发生错误的情况下,其他worker不会死锁。

每个工作人员进入一个包含do-while 循环的临界区,这是一个产生数字和递增的循环。在发出一个数字之前,它必须等待轮到它。当i % 3 == remainder 时,轮到它了。否则它Waits。当轮到它时,它发出数字,它增加i,它Pulses 所有等待的工人,并继续下一次迭代。当循环结束时,它会在释放锁之前最后一次Pulses。

已选择PulseAll 而不是Pulse,因为我们不知道等待队列中的下一个工作人员是否是当前i 的正确工作人员,所以我们只是将它们全部唤醒。

输出:

Worker #0 produced 0
Worker #1 produced 1
Worker #2 produced 2
Worker #0 produced 3
Worker #1 produced 4
Worker #2 produced 5
Worker #0 produced 6
Worker #1 produced 7
Worker #2 produced 8
Worker #0 produced 9
Worker #1 produced 10
Worker #2 produced 11
Worker #0 produced 12
Worker #1 produced 13
Worker #2 produced 14
Worker #0 produced 15
Worker #1 produced 16
Worker #2 produced 17
Worker #0 produced 18
Worker #1 produced 19

Try it on fiddle.


注意:此答案的1st revision 中的示例存在问题,因为它创建了一个初始忙等待阶段,直到所有工作人员都准备好。

【讨论】:

以上是关于使用 lock、Monitor Pulse 和 Wait 同步线程的主要内容,如果未能解决你的问题,请参考以下文章

C# Monitor的Wait和Pulse方法使用详解

不了解 Monitor.Pulse() 的必要性

C#简单理解 Monitor.Wait 与 Monitor.Pulse

c# Monitor.wait() 经典实例

lock,Monitor,Mutex的区别

C#中Monitor和Lock以及区别