通过前面对锁lock的基本使用以及注意事项的学习,相信大家对锁的同步机制有了大致了解,今天我们将继续学习——互斥锁Monitor。

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

例如以下lock语句:

lock (obj)
{
//同步代码块
}

最终会被解析成以下代码:

Monitor.Enter(obj);
try
{
//同步代码块
}
finally
{
Monitor.Exit(obj);
}

lock关键字简洁且易于使用,而Monitor类 则功能强大,能够提供比lock关键字更细粒度、更灵活的控制以及更多的功能。

因为lock关键字是Monitor类的语法糖,因此lock关键字面临的问题,Monitor类同样也会面临。当然也会存在一些Monitor类特有的问题。

下面我们一起详细学习Monitor类的注意事项以及实现一个简单的生产者-消费者模式示例代码。

01、避免锁定值类型

这是因为 Monitor.Enter方法的参数为Object类型,这就导致如果传递值类型会导致值类型被装箱,进而导致线程在已装箱的对象上获取锁,最终线程每次调用Monitor.Enter方法都在一个完全不同的对象上获取锁,导致锁失效,无法实现线程同步。

看看下面这个代码示例:

public class LockValueTypeExample
{
private static readonly int _lock = 88;
public void Method1()
{
try
{
Monitor.Enter(_lock);
var threadId = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine($"线程 {threadId} 通过 lock(值类型) 锁进入 Method1");
Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}");
Console.WriteLine($"开始休眠 5 秒");
Console.WriteLine($"------------------------------------");
Thread.Sleep(5000);
}
finally
{
Console.WriteLine($"开始释放锁 {DateTime.Now:HH:mm:ss}");
Monitor.Exit(_lock);
Console.WriteLine($"完成锁释放 {DateTime.Now:HH:mm:ss}");
}
}
}
public static void LockValueTypeRun()
{
var example = new LockValueTypeExample();
var thread1 = new Thread(example.Method1);
thread1.Start();
}

看看执行结果:

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

02、小心try/finally

如上面的例子,Monitor.Enter方法是写在try块中,试想一下:如果在Monitor.Enter方法之前抛出了异常会怎样异常?看下面这段代码:

public class LockBeforeExceptionExample
{
private static readonly object _lock = new object();
public void Method1()
{
try
{
if (new Random().Next(2) == 1)
{
Console.WriteLine($"在调用Monitor.Enter前发生异常");
throw new Exception("在调用Monitor.Enter前发生异常");
}
Monitor.Enter(_lock);
}
catch (Exception ex)
{
Console.WriteLine($"捕捉到异常:{ex.Message}");
}
finally
{
Console.WriteLine($"开始释放锁 {DateTime.Now:HH:mm:ss}");
Monitor.Exit(_lock);
Console.WriteLine($"完成锁释放 {DateTime.Now:HH:mm:ss}");
}
}
}
public static void LockBeforeExceptionRun()
{
var example = new LockBeforeExceptionExample();
var thread1 = new Thread(example.Method1);
thread1.Start();
}

上面代码是在调用Monitor.Enter方法前随机抛出异常,当发生异常后,可以在释放锁的时候和锁定值类型报了同样的错误,执行结果如下:

这是因为还没有执行锁定就抛出异常,导致释放一个没有锁定的锁。

那要如何解决这个问题呢?Monitor类已经考虑到了这种情况,并给出了解决办法——使用Monitor.Enter的第二个参数lockTaken,当获取锁定成功则更改lockTaken为true。如此在finally的时候只需要判断lockTaken即可决定是否需要执行释放锁操作,具体代码如下:

public class LockSolveBeforeExceptionExample
{
private static readonly object _lock = new object();
public void Method1()
{
var lockTaken = false;
try
{
if (new Random().Next(2) == 1)
{
Console.WriteLine($"在调用Monitor.Enter前发生异常");
throw new Exception("在调用Monitor.Enter前发生异常");
}
Monitor.Enter(_lock,ref lockTaken);
}
catch (Exception ex)
{
Console.WriteLine($"捕捉到异常:{ex.Message}");
}
finally
{
if (lockTaken)
{
Console.WriteLine($"开始释放锁 {DateTime.Now:HH:mm:ss}");
Monitor.Exit(_lock);
Console.WriteLine($"完成锁释放 {DateTime.Now:HH:mm:ss}");
}
else
{
Console.WriteLine($"未执行锁定,无需释放锁");
}
}
}
}
public static void LockSolveBeforeExceptionRun()
{
var example = new LockSolveBeforeExceptionExample();
var thread1 = new Thread(example.Method1);
thread1.Start();
}

执行结果如下:

03、善用TryEnter

我们知道使用锁应当避免长时间持有锁,长时间持有锁会阻塞其他线程,影响性能。我们可以通过Monitor.TryEnter指定超时时间,可以看看下面示例代码:

public class LockTryEnterExample
{
private static readonly object _lock = new object();
public void Method1()
{
try
{
Monitor.Enter(_lock);
Console.WriteLine($"Method1 | 获取锁成功,并锁定 5 秒");
Thread.Sleep(5000);
}
finally
{
Monitor.Exit(_lock);
}
}
public void Method2()
{
Console.WriteLine($"Method2 | 尝试获取锁");
if (Monitor.TryEnter(_lock, 3000))
{
try
{
}
finally
{
}
}
else
{
Console.WriteLine($"Method2 | 3 秒内未获取到锁,自动退出锁");
}
}
public void Method3()
{
Console.WriteLine($"Method3 | 尝试获取锁");
if (Monitor.TryEnter(_lock, 7000))
{
try
{
Console.WriteLine($"Method3 | 7 秒内获取到锁");
}
finally
{
Console.WriteLine($"Method3 |开始释放锁");
Monitor.Exit(_lock);
Console.WriteLine($"Method3 |完成锁释放");
}
}
else
{
Console.WriteLine($"Method3 | 7 秒内未获取到锁,自动退出锁");
}
}
}
public static void LockTryEnterRun()
{
var example = new LockTryEnterExample();
var thread1 = new Thread(example.Method1);
var thread2 = new Thread(example.Method2);
var thread3 = new Thread(example.Method3);
thread1.Start();
thread2.Start();
thread3.Start();
}

执行结果如下:

可以发现当Method1锁定5秒后,Method2尝试3秒内获取锁,结果并未获取到自动退出;然后Method3尝试7秒内获取锁,结果获取到锁并正确释放锁。

04、实现生产者-消费者模式

除了上面介绍的方法,Monitor类还有Wait、Pulse、PulseAll等方法。

Wait: 该方法用于将当前线程放入等待队列,直到收到其他线程的信号通知。

Pulse: 该方法用于唤醒等待队列中的一个线程。当一个线程调用 Pulse 时,它会通知一个正在等待该对象锁的线程继续执行。

PulseAll: 该方法用于唤醒等待队列中的所有线程。

然后我们利用Monitor类的这些功能来实现一个简单的生产者-消费者模式。大致思路如下:

1.首先启动生产者线程,获取锁,然后生成数据;

2.当生产者生产的数据小于数据队列长度,则生产一条数据同时通知消费者线程进行消费,否则暂停当前线程等待消费者线程消费数据;

3.然后启动消费者线程,获取锁,然后消费数据;

4.当数据队列中有数据,则消费一条数据同时通知生产者线程可以生产数据了,否则暂停当前线程等待生产者线程生产数据;

具体代码如下:

public class LockProducerConsumerExample
{
private static Queue<int> queue = new Queue<int>();
private static object _lock = new object();
//生产者
public void Producer()
{
while (true)
{
lock (_lock)
{
Console.ForegroundColor = ConsoleColor.Red;
if (queue.Count < 3)
{
var item = new Random().Next(100);
queue.Enqueue(item);
Console.WriteLine($"生产者,生产: {item}");
//唤醒消费者
Monitor.Pulse(_lock);
}
else
{
//队列满时,生产者等待
Console.WriteLine($"队列已满,生产者等待中……");
Monitor.Wait(_lock);
}
}
Thread.Sleep(500);
}
}
// 消费者
public void Consumer()
{
while (true)
{
lock (_lock)
{
Console.ForegroundColor = ConsoleColor.Blue;
if (queue.Count > 0)
{
var item = queue.Dequeue();
Console.WriteLine($"消费者,消费: {item}");
//唤醒生产者
Monitor.Pulse(_lock);
}
else
{
//队列空时,消费者等待
Console.WriteLine($"队列已空,消费者等待中……");
Monitor.Wait(_lock);
}
}
Thread.Sleep(10000);
}
}
}
public static void LockProducerConsumerRun()
{
var example = new LockProducerConsumerExample();
var thread1 = new Thread(example.Producer);
var thread2 = new Thread(example.Consumer);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
}

执行结果如下:

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

并发编程 - 线程同步(七)之互斥锁Monitor的更多相关文章

  1. Python并发编程-线程同步(线程安全)

    Python并发编程-线程同步(线程安全) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 线程同步,线程间协调,通过某种技术,让一个线程访问某些数据时,其它线程不能访问这些数据,直 ...

  2. python并发编程之多进程(二):互斥锁(同步锁)&进程其他属性&进程间通信(queue)&生产者消费者模型

    一,互斥锁,同步锁 进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的, 竞争带来的结果就是错乱,如何控制,就是加锁处理 part1:多个进程共享同一打印终 ...

  3. 并发编程-线程-死锁现象-GIL全局锁-线程池

    一堆锁 死锁现象 (重点) 死锁指的是某个资源被占用后,一直得不到释放,导致其他需要这个资源的线程进入阻塞状态. 产生死锁的情况 对同一把互斥锁加了多次 一个共享资源,要访问必须同时具备多把锁,但是这 ...

  4. Java并发编程实战(3)- 互斥锁

    我们在这篇文章中主要讨论如何使用互斥锁来解决并发编程中的原子性问题. 目录 概述 互斥锁模型 互斥锁简易模型 互斥锁改进模型 Java世界中的互斥锁 synchronized中的锁和锁对象 synch ...

  5. python并发编程之守护进程、互斥锁以及生产者和消费者模型

    一.守护进程 主进程创建守护进程 守护进程其实就是'子进程' 一.守护进程内无法在开启子进程,否则会报错二.进程之间代码是相互独立的,主进程代码运行完毕,守护进程也会随机结束 守护进程简单实例: fr ...

  6. 33 - 并发编程-线程同步-Event-lock

    目录 1 线程同步 1.1 Event 1.1.1 什么是Flag? 1.1.2 Event原理 1.1.3 吃包子 1.2 Lock 1.2.1 lock方法 1.2.2 计数器 1.2.3 非阻塞 ...

  7. python 之 并发编程(守护进程、互斥锁、IPC通信机制)

    9.5 守护进程 主进程创建守护进程 其一:守护进程会在主进程代码执行结束后就立即终止 其二:守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic process ...

  8. java并发编程实战《四》互斥锁(下)

    互斥锁(下):如何用一把锁保护多个资源?    一把锁可以保护多个资源,但是不能用多把锁来保护一个资源. 那如何保护多个资源? 当我们要保护多个资源时,首先要区分这些资源是否存在关联关系. 如下代码 ...

  9. java并发编程实战《三》互斥锁(上)

    互斥锁(上):解决原子性问题 原子性问题的源头是线程切换,操作系统做线程切换是依赖 CPU 中断的,所以禁止 CPU 发生中断就能够禁止线程切换. 在早期单核 CPU 时代,这个方案的确是可行的,而且 ...

  10. 线程私有数据TSD——一键多值技术,线程同步中的互斥锁和条件变量

    一:线程私有数据: 线程是轻量级进程,进程在fork()之后,子进程不继承父进程的锁和警告,别的基本上都会继承,而vfork()与fork()不同的地方在于vfork()之后的进程会共享父进程的地址空 ...

随机推荐

  1. 抓包工具之Fiddler(详解)

    Fiddle简介 Fiddler是最强大最好用的Web调试工具之一,它能记录所有客户端和服务器的http和https请求,允许你监视,设置断点,甚至修改输入输出数据,Fiddler包含了一个强大的基于 ...

  2. 0-JavaScript入门

    1.1 JS用途 JavaScript属于一门脚本语言,可用于前端给网页添加一些动态效果. 也可用于node平台,开发服务器 React.Vue框架底层也是通过JS实现 1.2 JS组成 前端分为三层 ...

  3. ant 组件全局设置中文 vue

    //引入中文组件import zhCN from 'ant-design-vue/es/locale/zh_CN';//定义 const  locale = zhCN   //包裹根组件 <a- ...

  4. DTL事务控制语言--sql事务

    DTL事务控制语言体格sql语句就是一个事务事务可以保证 一组sql语句要么都成功,要么都失败默认自动提交一可以关闭 set autocommit=0关闭自动提交最后 插入或者修改时 只有commit ...

  5. Flutter 错误The argument type 'Color' can't be assigned to the parameter type 'MaterialStateProperty<Color?>?'.dart(argument_type_not_assignable)

    MaterialStateProperty<Color?>?和Color 当为TextButton等button添加颜色时,使用ButtonStyle为其添加颜色 TextButton( ...

  6. bytecode 生成器

    基础 objectweb asm 很难用,找了几个高级点的. activej codegen 这个库很像 .net DLR 风格,采用 Expression 抽象,例如 Expressions.add ...

  7. 【Spring】Sring基础概念(黑马SSM学习笔记)

    目录 Spring简介 Spring是什么 Spring发展 Spring优势 Spring体系结构 Spring快速入门 Spring程序开发步骤 不用Spring的一般步骤 使用Spring框架 ...

  8. Mac下SSH Key配置

    1 .检查.ssh文件夹是否存在 $ ls -al ~/.ssh 2.如果不存在新建.ssh文件平 $ mkdir ~/.ssh 3.生成KEY在命令行中输入,your_email@example.c ...

  9. Qt编写地图综合应用46-设备点位(添加、删除、清空、重置)

    一.前言 在学习JS语法的时候发现其实程序都大同小异,正所谓一通百通,熟悉各大概的语法以后基本都可以上手,和C++最大的不同就是他没有数据类型的概念,作为解释性的语言,是在执行的时候自动去转换数据类型 ...

  10. Qt编写安防视频监控系统48-视频参数

    一.前言 视频参数之前在基本参数中,后面越来越多,直接独立了出来,甚至还拆分出来了视频参数1.视频参数2,参数越来越多分组也越来越多的时候,你会发现分组名称都不够用或者不方便命名,不能直观的表示该分组 ...