ToB企服应用市场:ToB评测及商务社交产业平台

标题: 并发编程 - 线程同步(七)之互斥锁Monitor [打印本页]

作者: 络腮胡菲菲    时间: 2025-2-13 18:02
标题: 并发编程 - 线程同步(七)之互斥锁Monitor
通过前面对锁lock的基本使用以及注意事项的学习,相信大家对锁的同步机制有了大致了解,今天我们将继续学习——互斥锁Monitor。

lock是C#语言中的关键字,是语法糖,lock语句最终会由C#编译器解析成Monitor类实现相关语句。

例如以下lock语句:
  1. lock (obj)
  2. {
  3.     //同步代码块
  4. }
复制代码
最终会被解析成以下代码:
  1. Monitor.Enter(obj);
  2. try
  3. {
  4.     //同步代码块
  5. }
  6. finally
  7. {
  8.     Monitor.Exit(obj);
  9. }
复制代码
lock关键字简洁且易于使用,而Monitor类 则功能强盛,可以或许提供比lock关键字更细粒度、更灵活的控制以及更多的功能。
因为lock关键字是Monitor类的语法糖,因此lock关键字面临的问题,Monitor类同样也会面临。固然也会存在一些Monitor类特有的问题。
下面我们一起详细学习Monitor类的注意事项以及实现一个简朴的生产者-消费者模式示例代码。
01、避免锁定值类型

这是因为 Monitor.Enter方法的参数为Object类型,这就导致假如传递值类型会导致值类型被装箱,进而导致线程在已装箱的对象上获取锁,最终线程每次调用Monitor.Enter方法都在一个完全不同的对象上获取锁,导致锁失效,无法实现线程同步。
看看下面这个代码示例:
  1. public class LockValueTypeExample
  2. {
  3.     private static readonly int _lock = 88;
  4.     public void Method1()
  5.     {
  6.         try
  7.         {
  8.             Monitor.Enter(_lock);
  9.             var threadId = Thread.CurrentThread.ManagedThreadId;
  10.             Console.WriteLine($"线程 {threadId} 通过 lock(值类型) 锁进入 Method1");
  11.             Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}");
  12.             Console.WriteLine($"开始休眠 5 秒");
  13.             Console.WriteLine($"------------------------------------");
  14.             Thread.Sleep(5000);
  15.         }
  16.         finally
  17.         {
  18.             Console.WriteLine($"开始释放锁 {DateTime.Now:HH:mm:ss}");
  19.             Monitor.Exit(_lock);
  20.             Console.WriteLine($"完成锁释放 {DateTime.Now:HH:mm:ss}");
  21.         }
  22.     }
  23. }
  24. public static void LockValueTypeRun()
  25. {
  26.     var example = new LockValueTypeExample();
  27.     var thread1 = new Thread(example.Method1);
  28.     thread1.Start();
  29. }
复制代码
看看执行结果:

可以发现在释放锁的时间抛出异常,大致意思是:“对象同步方法在未同步的代码块中被调用。”,这就是因为锁定的地方和释放的地方锁已经不一样了。
02、小心try/finally

如上面的例子,Monitor.Enter方法是写在try块中,试想一下:假如在Monitor.Enter方法之前抛出了异常会怎样异常?看下面这段代码:
  1. public class LockBeforeExceptionExample
  2. {
  3.     private static readonly object _lock = new object();
  4.     public void Method1()
  5.     {
  6.         try
  7.         {
  8.             if (new Random().Next(2) == 1)
  9.             {
  10.                 Console.WriteLine($"在调用Monitor.Enter前发生异常");
  11.                 throw new Exception("在调用Monitor.Enter前发生异常");
  12.             }
  13.             Monitor.Enter(_lock);
  14.         }
  15.         catch (Exception ex)
  16.         {
  17.             Console.WriteLine($"捕捉到异常:{ex.Message}");
  18.         }
  19.         finally
  20.         {
  21.             Console.WriteLine($"开始释放锁 {DateTime.Now:HH:mm:ss}");
  22.             Monitor.Exit(_lock);
  23.             Console.WriteLine($"完成锁释放 {DateTime.Now:HH:mm:ss}");
  24.         }
  25.     }
  26. }
  27. public static void LockBeforeExceptionRun()
  28. {
  29.     var example = new LockBeforeExceptionExample();
  30.     var thread1 = new Thread(example.Method1);
  31.     thread1.Start();
  32. }
复制代码
上面代码是在调用Monitor.Enter方法前随机抛出异常,当发生异常后,可以在释放锁的时间和锁定值类型报了同样的错误,执行结果如下:

这是因为还没有执行锁定就抛出异常,导致释放一个没有锁定的锁。
那要如何解决这个问题呢?Monitor类已经考虑到了这种情况,并给出了解决办法——使用Monitor.Enter的第二个参数lockTaken,当获取锁定乐成则更改lockTaken为true。如此在finally的时间只需要判定lockTaken即可决定是否需要执行释放锁操作,具体代码如下:
  1. public class LockSolveBeforeExceptionExample
  2. {
  3.     private static readonly object _lock = new object();
  4.     public void Method1()
  5.     {
  6.         var lockTaken = false;
  7.         try
  8.         {
  9.             if (new Random().Next(2) == 1)
  10.             {
  11.                 Console.WriteLine($"在调用Monitor.Enter前发生异常");
  12.                 throw new Exception("在调用Monitor.Enter前发生异常");
  13.             }
  14.             Monitor.Enter(_lock,ref lockTaken);
  15.         }
  16.         catch (Exception ex)
  17.         {
  18.             Console.WriteLine($"捕捉到异常:{ex.Message}");
  19.         }
  20.         finally
  21.         {
  22.             if (lockTaken)
  23.             {
  24.                 Console.WriteLine($"开始释放锁 {DateTime.Now:HH:mm:ss}");
  25.                 Monitor.Exit(_lock);
  26.                 Console.WriteLine($"完成锁释放 {DateTime.Now:HH:mm:ss}");
  27.             }
  28.             else
  29.             {
  30.                 Console.WriteLine($"未执行锁定,无需释放锁");
  31.             }
  32.         }
  33.     }
  34. }
  35. public static void LockSolveBeforeExceptionRun()
  36. {
  37.     var example = new LockSolveBeforeExceptionExample();
  38.     var thread1 = new Thread(example.Method1);
  39.     thread1.Start();
  40. }
复制代码
执行结果如下:

03、善用TryEnter

我们知道使用锁应当避免长时间持有锁,长时间持有锁会阻塞其他线程,影响性能。我们可以通过Monitor.TryEnter指定超时时间,可以看看下面示例代码:
  1. public class LockTryEnterExample
  2. {
  3.     private static readonly object _lock = new object();
  4.     public void Method1()
  5.     {
  6.         try
  7.         {
  8.             Monitor.Enter(_lock);
  9.             Console.WriteLine($"Method1 | 获取锁成功,并锁定 5 秒");
  10.             Thread.Sleep(5000);
  11.         }
  12.         finally
  13.         {
  14.             Monitor.Exit(_lock);
  15.         }
  16.     }
  17.     public void Method2()
  18.     {
  19.         Console.WriteLine($"Method2 | 尝试获取锁");
  20.         if (Monitor.TryEnter(_lock, 3000))
  21.         {
  22.             try
  23.             {
  24.             }
  25.             finally
  26.             {
  27.             }
  28.         }
  29.         else
  30.         {
  31.             Console.WriteLine($"Method2 | 3 秒内未获取到锁,自动退出锁");
  32.         }
  33.     }
  34.     public void Method3()
  35.     {
  36.         Console.WriteLine($"Method3 | 尝试获取锁");
  37.         if (Monitor.TryEnter(_lock, 7000))
  38.         {
  39.             try
  40.             {
  41.                 Console.WriteLine($"Method3 | 7 秒内获取到锁");
  42.             }
  43.             finally
  44.             {
  45.                 Console.WriteLine($"Method3 |开始释放锁");
  46.                 Monitor.Exit(_lock);
  47.                 Console.WriteLine($"Method3 |完成锁释放");
  48.             }
  49.         }
  50.         else
  51.         {
  52.             Console.WriteLine($"Method3 | 7 秒内未获取到锁,自动退出锁");
  53.         }
  54.     }
  55. }
  56. public static void LockTryEnterRun()
  57. {
  58.     var example = new LockTryEnterExample();
  59.     var thread1 = new Thread(example.Method1);
  60.     var thread2 = new Thread(example.Method2);
  61.     var thread3 = new Thread(example.Method3);
  62.     thread1.Start();
  63.     thread2.Start();
  64.     thread3.Start();
  65. }
复制代码
执行结果如下:

可以发现当Method1锁定5秒后,Method2尝试3秒内获取锁,结果并未获取到自动退出;然后Method3尝试7秒内获取锁,结果获取到锁并正确释放锁。
04、实现生产者-消费者模式

除了上面先容的方法,Monitor类还有Wait、Pulse、PulseAll等方法。
Wait: 该方法用于将当前线程放入期待队列,直到收到其他线程的信号通知。
Pulse: 该方法用于唤醒期待队列中的一个线程。当一个线程调用 Pulse 时,它会通知一个正在期待该对象锁的线程继续执行。
PulseAll: 该方法用于唤醒期待队列中的所有线程。
然后我们利用Monitor类的这些功能来实现一个简朴的生产者-消费者模式。大致思路如下:
1.首先启动生产者线程,获取锁,然后生成数据;
2.当生产者生产的数据小于数据队列长度,则生产一条数据同时通知消费者线程进行消费,否则暂停当前线程期待消费者线程消费数据;
3.然后启动消费者线程,获取锁,然后消费数据;
4.当数据队列中有数据,则消费一条数据同时通知生产者线程可以生产数据了,否则暂停当前线程期待生产者线程生产数据;
具体代码如下:
  1. public class LockProducerConsumerExample
  2. {
  3.     private static Queue<int> queue = new Queue<int>();
  4.     private static object _lock = new object();
  5.     //生产者
  6.     public  void Producer()
  7.     {
  8.         while (true)
  9.         {
  10.             lock (_lock)
  11.             {
  12.                 Console.ForegroundColor = ConsoleColor.Red;
  13.                 if (queue.Count < 3)
  14.                 {
  15.                     var item = new Random().Next(100);
  16.                     queue.Enqueue(item);
  17.                     Console.WriteLine($"生产者,生产: {item}");
  18.                     //唤醒消费者
  19.                     Monitor.Pulse(_lock);  
  20.                 }
  21.                 else
  22.                 {
  23.                     //队列满时,生产者等待
  24.                     Console.WriteLine($"队列已满,生产者等待中……");
  25.                     Monitor.Wait(_lock);  
  26.                 }
  27.             }
  28.             Thread.Sleep(500);
  29.         }
  30.     }
  31.     // 消费者
  32.     public  void Consumer()
  33.     {
  34.         while (true)
  35.         {
  36.             lock (_lock)
  37.             {
  38.                 Console.ForegroundColor = ConsoleColor.Blue;
  39.                 if (queue.Count > 0)
  40.                 {
  41.                     var item = queue.Dequeue();
  42.                     Console.WriteLine($"消费者,消费: {item}");
  43.                     //唤醒生产者
  44.                     Monitor.Pulse(_lock);  
  45.                 }
  46.                 else
  47.                 {
  48.                     //队列空时,消费者等待
  49.                     Console.WriteLine($"队列已空,消费者等待中……");
  50.                     Monitor.Wait(_lock);  
  51.                 }
  52.             }
  53.             Thread.Sleep(10000);
  54.         }
  55.     }
  56. }
  57. public static void LockProducerConsumerRun()
  58. {
  59.     var example = new LockProducerConsumerExample();
  60.     var thread1 = new Thread(example.Producer);
  61.     var thread2 = new Thread(example.Consumer);
  62.     thread1.Start();
  63.     thread2.Start();
  64.     thread1.Join();
  65.     thread2.Join();
  66. }
复制代码
执行结果如下:

:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4