【holm】C#线程监视器Monitor类使用指南

线程的同步

概念

线程同步是指控制多个线程的相对执行顺序,避免在使用共享资源时可能出现的问题。

线程同步可用的方法

  • 轮询(不推荐):通过反复检查Thread类IsAlive属性判断调用状态。
  • Thread.Join():将一个线程加入到本线程中,本线程的执行会等待另一线程执行完毕。适合管理少量线程,不适用于复杂情况。
  • lock语句(Monitor类)

Monitor概述

Monitor类主要用于防止多个线程同时操作一个对象产生冲突。 (无法实例化)
主要功能:

  • 可以从任何上下文调用
  • 加锁:当第一个线程访问加锁资源时,此资源会被锁住,其他想要访问此资源的线程进入锁等待状态,直到第一个线程结束访问。
  • 解锁
    涉及操作有:lock语句/Monitor.Enter()/Monitor.Exit()/Monitor.Wait()/Monitor.Pulse()/Monitor.PulseAll()

对于每个同步对象来维护以下信息:

  • 对当前拥有锁的线程的引用。
  • 对就绪的队列,其中包含就可以获得的锁的线程的引用。
  • 对等待队列,其中包含有关的锁定的对象的状态更改通知等待的线程的引用。

Monitor将锁定对象(即引用类型)而非值类型。 在您将值类型传递给 EnterExit 时,它会针对每个调用分别装箱。 由于每个调用都创建一个单独的对象,所以 Enter 从不拦截,并且其旨在保护的代码并未真正同步。 此外,传递给 Exit 的对象不同于传递给 Enter 的对象,所以 Monitor 将引发 SynchronizationLockException,并显示消息“从不同步的代码块中调用了对象同步方法”。

lock语句 /Monitor.Enter()/Monitor.Exit()

语法:

lock(对象或表达式)//引用类型,可用this,静态类使用typeof()
{
    ...
}

当程序进入lock区域时,首先获取指定对象的互斥锁(类似Threading命名空间下的Mutex的作用),此语句执行期间给指定对象上锁,语句执行完成后进行解锁。
等同于:

System.Threading.Monitor.Enter(对象或表达式);
try{
    ...
}
finally{
    System.Threading.Monitor.Exit(对象或表达式);
}

以下是一个实例,每个进程分别让x,y自增并打印自增后的值。如果不加锁的话x,y的值可能不同。

    class Program
    {
        static int x = 0, y = 0;
        public static void Main(string[] args)
        {

            Thread thread1 = new Thread(TestEqual);
            Thread thread2 = new Thread(TestEqual);
            thread1.Start();
            thread2.Start();
        }
        static void TestEqual()
        {
            for (int i = 0; i < 5; i++)
            {
                //lock (typeof(Program))
                {
                    x++;
                    Thread.Sleep(new Random().Next(10));
                    y++;
                    Console.WriteLine($"x={x},y={y}");
                }
            }
        }
    }
    //运行结果:
    //x=2,y=1
    //x=2,y=2
    //x=4,y=4
    //x=4,y=4
    //x=6,y=6
    //x=6,y=6
    //x=8,y=8
    //x=8,y=8
    //x=10,y=10
    //x=10,y=9

此外:在锁内也可被Thread.Interrupt()中断,将会引发ThreadInterruptedException异常,在另一进程中使用中断进程的Join()方法恢复运行。

Monitor.Wait()/Monitor.Pulse()/Monitor.PulseAll()

  • Monitor.Wait():释放对象上的锁并阻止当前线程,直到它重新获取该锁。
  • Monitor.Pulse()/Monitor.PulseAll():当前拥有指定对象的锁的线程调用此方法,以向第一个线程发出锁。 接收到脉冲后,等待线程会移动到就绪队列。 如果调用的线程Pulse释放该锁,则就绪队列(不一定是发出脉冲的线程)中的下一个线程将获取该锁。如果调用的线程PulseAll释放该锁,则就绪队列中的所有线程将依次获取该锁。

注意:

  • 必须从同步的代码块内调用 PulsePulseAllWait 方法。
  • 若要向多个线程发出信号,请使用 PulseAll 方法。

实例:

   class Program
    {
        public static void Main(string[] args)
        {
            Thread thread1 = new Thread(Test);
            thread1.Name = "thread1";
            Thread thread2 = new Thread(Test);
            thread2.Name = "thread2";
            Thread pulseThread = new Thread(Pulse);
            thread1.Start();
            thread2.Start();
            pulseThread.Start();
        }
        private static void Pulse()
        {
            lock (typeof(Program))
            {
                Thread.Sleep(1500);
                x++;
                Monitor.Pulse(typeof(Program));//唤醒先进入就绪队列的进程
                //Monitor.PulseAll(typeof(Program)); //唤醒全部就绪队列的进程
            }
        }
        static void Test()
        {
            lock (typeof(Program))
            {
                Monitor.Wait(typeof(Program));
                Thread.Sleep(400);
                Console.WriteLine($"Thread name:{Thread.CurrentThread.Name}");
            }
        }
    }

Monitor.TryEnter()/Monitor.IsEntered()

  • 使用Monitor.IsEntered()确定当前线程是否保留指定对象上的锁。
  • Monitor.TryEnter()类似于Enter但它永远不会阻止当前线程。如果该线程不能输入而不会阻塞,则该方法将返回false,和线程不进入关键节。

参考资料