【问题标题】:When ThreadB Monitor.Pulse(_locker) which thread will get _locker first?当 ThreadB Monitor.Pulse(_locker) 哪个线程会先得到 _locker?
【发布时间】:2018-03-02 06:21:54
【问题描述】:

我看了这篇文章https://www.codeproject.com/Articles/28785/Thread-synchronization-Wait-and-Pulse-demystified#_articleTop

有一句话:

推荐模式

这些队列可能导致意外行为。当脉冲发生时,等待队列的头部被释放并被添加到就绪队列中。但是,如果就绪队列中还有其他线程,它们将在被释放的线程之前获得锁。这是一个问题,因为获取锁的线程可以改变脉冲线程所依赖的状态。

解决办法是在lock语句中使用while条件:

readonly object key = new object();
bool block = true;

// thread A
lock ( key )
{
  while ( block )
    Monitor.Wait( key );
  block = true;
}

// thread B
lock ( key )
{
  block = false;
  Monitor.Pulse( key );
}

作者说 ThreadC 会首先获得 _locker,但在我运行三个线程的演示中,我发现这不是真的。 ThreadC 最后获得 _locker。

这是我的代码:

class Program
{
    private static readonly object _locker = new object();
    public static void ThreadA()
    {

        new Thread(() =>
        {
            lock (_locker)
            {
                Console.WriteLine("Thread A acquire lock then wait threadId {0}", Thread.CurrentThread.ManagedThreadId);
                Monitor.Wait(_locker);
                Thread.Sleep(1000);
                Console.WriteLine("Thread A  Continue .. threadId {0}", Thread.CurrentThread.ManagedThreadId);
            }
        }).Start();
            
    }
    public static void ThreadB()
    {

        new Thread(() =>
        {
            lock (_locker)
            {
                Console.WriteLine("Thread B acquire lock...then pulse  threadId {0}",Thread.CurrentThread.ManagedThreadId);
                Monitor.Pulse(_locker);
                Thread.Sleep(1000);
                Console.WriteLine("Thread B sleep...then realse the locker threadId {0}", Thread.CurrentThread.ManagedThreadId);
            }
        }).Start();

    }

    public static void ThreadC()
    {

        new Thread(ThreadA).Start();
    }

    static void Main(string[] args)
    {


        ThreadA();

        Thread.Sleep(10); // ensure threadA get _locker firstly

        ThreadB();

        ThreadC();


    }
}

【问题讨论】:

  • 显示测试程序的输出。

标签: c# multithreading


【解决方案1】:

作者说ThreadC会先拿到_locker

我认为作者根本没有这么说。您发布的报价如下:

如果就绪队列中还有其他线程,它们会在被释放的线程之前获取锁

该语句中有一个非常重要的“if”。您的代码示例似乎不能保证线程 C 会在线程 A 之前处于就绪队列中。事实上,由于您的 ThreadC() 方法启动了一个全新的线程,然后又启动了 ThreadA() 线程,线程 C 更容易被延迟。

当然,线程执行完全取决于 Windows 线程调度程序。除非您添加同步(例如在调用 ThreadB() 之前有 10 毫秒的延迟),否则线程的执行顺序受线程调度程序的突发奇想。虽然通常线程会被安排轮询,但不能保证这一点。

但是,即使我们假设保证循环线程调度,我在您的代码中看不到任何需要甚至建议线程 C 会在线程 A 之前进入监视器的就绪队列的内容,更不用说您引用的作者的声明说应该是这种情况。

在 cmets 中,您会问:

threadC有没有机会抢先?

使用您发布的代码,这没有真正发生的机会,因为线程 C 必须通过两次线程启动才能尝试获取锁。线程 B 未能首先获取锁并释放线程 A 的可能性微乎其微。这在理论上是可能的,但我怀疑你在实践中见过它。即使没有线程 C 是两个线程,这似乎也不太可能(尽管至少更合理)。

也就是说,有趣的是,因为线程 C 只是尝试获取锁,而从未进入等待队列,所以您可以演示一个场景,即线程 C 在线程 A 开始等待后到达监视器的就绪队列,但是在线程 A 再次准备好之前,从而允许线程 C 在线程 A 再次获得锁之前获得锁。

我已经修改了您的原始代码以显示这一点,包括在输出中添加“经过的秒数”(以便更容易看到一切发生的时间),删除启动线程 C 的“包装器”线程,并将参数添加到帮助控制和识别线程:

using System.Diagnostics;
using System.Threading;
using static System.Console;

namespace TestSO46334766MonitorQueues
{
    class Program
    {
        private static readonly object _locker = new object();
        public static void ThreadA(string label, int delay, bool wait)
        {
            new Thread(() =>
            {
                LogMessage($"Thread {label} started, threadId {Thread.CurrentThread.ManagedThreadId}");
                Thread.Sleep(delay);
                LogMessage($"Thread {label} attempt to acquire lock, threadId {Thread.CurrentThread.ManagedThreadId}");
                lock (_locker)
                {
                    LogMessage($"Thread {label} acquire lock{(wait ? " then wait" : "")} threadId {Thread.CurrentThread.ManagedThreadId}");
                    if (wait) Monitor.Wait(_locker);
                    Thread.Sleep(1000);
                    LogMessage($"Thread {label} continue .. threadId {Thread.CurrentThread.ManagedThreadId}");
                }
            }).Start();
        }

        public static void ThreadB()
        {
            new Thread(() =>
            {
                LogMessage($"Thread B attempt to acquire lock, threadId {Thread.CurrentThread.ManagedThreadId}");
                lock (_locker)
                {
                    LogMessage($"Thread B acquire lock...sleep, then pulse threadId {Thread.CurrentThread.ManagedThreadId}");
                    Thread.Sleep(500);
                    Monitor.Pulse(_locker);
                    LogMessage($"Thread B sleep...then release the locker threadId {Thread.CurrentThread.ManagedThreadId}");
                    Thread.Sleep(1000);
                }
            }).Start();
        }

        public static void ThreadC()
        {
            //new Thread(() => ThreadA("C", 250, false)).Start();
            ThreadA("C", 250, false);
        }

        static void Main(string[] args)
        {
            ThreadA("A", 0, true);
            Thread.Sleep(10); // ensure threadA get _locker firstly
            ThreadB();
            ThreadC();
            ReadLine();
        }

        static Stopwatch sw = Stopwatch.StartNew();

        static void LogMessage(string message)
        {
            WriteLine($"{sw.Elapsed.TotalSeconds:0.0}: {message}");
        }
    }
}

这是一个输出示例:

0.1:线程 A 已启动,threadId 3
0.1:线程 A 尝试获取锁,threadId 3
0.1:线程 A 获取锁然后等待 threadId 3
0.2:线程 C 启动,threadId 5
0.2:线程 B 尝试获取锁,threadId 4
0.2:线程 B 获取锁...睡眠,然后脉冲 threadId 4
0.4:线程 C 尝试获取锁,threadId 5
0.7:线程 B 休眠...然后释放更衣室 threadId 4
1.7:线程C获取锁threadId 5
2.7: 线程 C 继续 .. threadId 5
3.7:线程A继续..threadId 3

请注意,尽管线程 B 已首先创建,但线程 C 实际上在线程 B 之前运行。这是 Windows 线程调度程序如何不保证线程执行顺序的示例。

还请注意,虽然线程 C 与线程 B 同时启动,但它所做的第一件事是在实际尝试获取锁之前休眠。这使线程 B 有机会首先获得锁。因此,当线程 C 确实尝试获取锁时,它不能。但它确实被放入就绪队列。线程 A 仍然在等待队列中,所以当线程 B 最终释放它时(通过调用 Pulse()),线程 A 在就绪队列中落后线程 C。一旦线程 B 最终离开锁,就绪队列中的线程就可以按顺序获取锁,从而导致它们实际上也按顺序运行

【讨论】:

  • 非常感谢你能回答这个问题,我只是想知道当 ThreadB 调用 Monitor.Pulse() 时哪个线程会获得 _locker,如果我在 ThreadB() 之后添加 Thread.sleep(100) 会怎样;确保 ThreadC 可以在就绪队列中;
  • threadC有没有机会先拿到?或者无法推测哪个线程可以先获取_locker
  • “threadC 有没有机会首先获得?” -- 不,不是真的......不是你引入的 10 毫秒延迟。如果您摆脱了这种延迟,那么线程 C 首先获得锁并因此成为等待队列中的第一个的机会非常小。请注意,由于 A 和 C 都使用 Wait(),因此它们根据首先调用 Wait() 进行排序。如果 C 只是简单地尝试获取锁,它可能会在 A 之前结束,即使它实际上是稍后安排的,因为 A 一直停留在等待队列中,直到 B 调用 Pulse()
  • 衷心感谢您的解释和演示,如果只有两个线程,问题就简单了,线程A在lock语句中调用Monitor.Wait();然后 ThreadB 调用 Monitor.Pulse();最后 ThreadA 简历;但是当有其他线程(如thread cdf g......等)时,问题会很复杂,当ThreadA调用Monitor.Wait()时,不能确保ThreadB首先获得_locker,当threadB调用Monitor.Pulse() 无法确保 ThreadA 先再次获取 _locker
  • 调用Pulse()的线程不应该关心哪个线程被唤醒。如果是这样,您几乎肯定是在滥用Monitor 类。通常,会有一个线程池可用于工作,Pulse() 用于唤醒一些任意选择的线程,即任何线程准备就绪并等待。所以虽然是的,如果你关心哪个线程被唤醒,它可能会令人困惑,你不应该关心,所以它不应该让人困惑。如果您确实关心,恕我直言,最简单的做法就是为每个可以唤醒的线程分配一个不同的同步对象。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-11
  • 1970-01-01
  • 1970-01-01
  • 2023-03-30
相关资源
最近更新 更多