并发编程 - 线程同步(七)之互斥锁Monitor
通过前面对锁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的更多相关文章
- Python并发编程-线程同步(线程安全)
Python并发编程-线程同步(线程安全) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 线程同步,线程间协调,通过某种技术,让一个线程访问某些数据时,其它线程不能访问这些数据,直 ...
- python并发编程之多进程(二):互斥锁(同步锁)&进程其他属性&进程间通信(queue)&生产者消费者模型
一,互斥锁,同步锁 进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的, 竞争带来的结果就是错乱,如何控制,就是加锁处理 part1:多个进程共享同一打印终 ...
- 并发编程-线程-死锁现象-GIL全局锁-线程池
一堆锁 死锁现象 (重点) 死锁指的是某个资源被占用后,一直得不到释放,导致其他需要这个资源的线程进入阻塞状态. 产生死锁的情况 对同一把互斥锁加了多次 一个共享资源,要访问必须同时具备多把锁,但是这 ...
- Java并发编程实战(3)- 互斥锁
我们在这篇文章中主要讨论如何使用互斥锁来解决并发编程中的原子性问题. 目录 概述 互斥锁模型 互斥锁简易模型 互斥锁改进模型 Java世界中的互斥锁 synchronized中的锁和锁对象 synch ...
- python并发编程之守护进程、互斥锁以及生产者和消费者模型
一.守护进程 主进程创建守护进程 守护进程其实就是'子进程' 一.守护进程内无法在开启子进程,否则会报错二.进程之间代码是相互独立的,主进程代码运行完毕,守护进程也会随机结束 守护进程简单实例: fr ...
- 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 非阻塞 ...
- python 之 并发编程(守护进程、互斥锁、IPC通信机制)
9.5 守护进程 主进程创建守护进程 其一:守护进程会在主进程代码执行结束后就立即终止 其二:守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic process ...
- java并发编程实战《四》互斥锁(下)
互斥锁(下):如何用一把锁保护多个资源? 一把锁可以保护多个资源,但是不能用多把锁来保护一个资源. 那如何保护多个资源? 当我们要保护多个资源时,首先要区分这些资源是否存在关联关系. 如下代码 ...
- java并发编程实战《三》互斥锁(上)
互斥锁(上):解决原子性问题 原子性问题的源头是线程切换,操作系统做线程切换是依赖 CPU 中断的,所以禁止 CPU 发生中断就能够禁止线程切换. 在早期单核 CPU 时代,这个方案的确是可行的,而且 ...
- 线程私有数据TSD——一键多值技术,线程同步中的互斥锁和条件变量
一:线程私有数据: 线程是轻量级进程,进程在fork()之后,子进程不继承父进程的锁和警告,别的基本上都会继承,而vfork()与fork()不同的地方在于vfork()之后的进程会共享父进程的地址空 ...
随机推荐
- pikachu平台XXE漏洞通关教程详解
声明! 如涉及侵权马上删除文章,笔记只是方便各位师傅的学习和探讨,文章所提到的网站以及内容,只做学习交流,其他均与本人无关,切勿触碰法律底线,否则后果自负!!!! 文章来自个人csdn博客,地址:ht ...
- HASHCTF2024
第一届山东大学HASHCTF部分Misc题解 下面是我在本次比赛出的题目的WriteUp Secret of Keyboard 签到脚本题,有些同学的脚本解出来大小写不正确可能是由于脚本无法识别shi ...
- ShellProgressBar控制台中漂亮的显示进度条
ShellProgressBar控制台中漂亮的显示进度条 ShellProgressBar库的使用 控制台程序有时也需要进度条来显示任务执行的详细进度,最近就发现了一个第三方的类库叫ShellProg ...
- electron项目build报资源包下载出错
前情 公司有个桌面端项目是基于Electron开发的. 坑 上一次遇到npm install时安装electron无法安装,通过配置.npmrc文件解决了,今天在在执行npm run build的时候 ...
- OS之《机械硬盘》
数据的组织 一个磁盘设备 ---->多个物理盘片 一个物理盘片---->正反两面存储面 一个存储面---->多个磁道(每个磁道上存储容量时一样的,可存储相同数目的二进制位),所以,内 ...
- 【相邻不同型贪心】LeetCode767 重构字符串
题解 通常直接思考最佳策略是十分困难的,我们不妨思考每一种情况需要如何处理: 整个字符串只有一种字符 若字符串长度为 \(1\),那么字符串本身即为答案: 若字符串长度大于等于 \(2\),那么不存在 ...
- openEuler欧拉使用rc.local实现开机自启动
设置权限 chmod 775 /etc/rc.local 普通的单条是,直接写在rc.local里 /usr/local/nacos/bin/startup.sh -m standalone 复杂点 ...
- 在 d2js 使用多种数据源如spring数据源
不少人误以为 d2js 只能通过 database.js 指定一个数据库,是一个单数据库方案.实际上 d2js 也可以使用多个数据库. 在 WEB-INF/jslib/d2js/base.js 的末尾 ...
- T 语言语法设计方案总结
早在 2015,我就已经精通了 C++.C#.JS,也用过其它语言,比如 PHP.Python.Java 做过一些项目,就觉得这些语言设计得太过复杂.坑多.麻烦,所以就开始设计一门新语言,暂且叫 T ...
- localhost 宿主机访问
ssh -N -f -L localhost8888:localhost:8888 hostname