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

Using lock, Monitor Pulse and Wait to synchronize threads

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

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

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

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

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

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

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

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

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

无论我尝试什么,代码似乎都只是 waiting/blocking 无限。

我想我的总结问题是:

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

非常感谢任何有用的输入!

更新(2021 年 8 月 7 日):

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

下面是一个比较简单的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。此标志确保在任何工作人员出错的情况下,其他工作人员不会死锁。

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

选择了 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 中的示例是有问题的,因为它创建了一个初始的忙等待阶段,直到所有工作人员都准备好了。