.NET 同步与异步之锁(Lock、Monitor)(七)
本随笔续接:.NET同步与异步之相关背景知识(六)
在上一篇随笔中已经提到、解决竞争条件的典型方式就是加锁 ,那本篇随笔就重点来说一说.NET提供的最常用的锁 lock关键字 和 Monitor。
一、lock关键字Demo
- public object thisLock = new object();
- private long index;
- public void AddIndex()
- {
- lock (this.thisLock)
- {
- this.index++;
- if (this.index > long.MaxValue / )
- {
- this.index = ;
- }
// 和 index 无关的大量操作- }
- }
- public long GetIndex()
- {
- return this.index;
- }
这一组demo,代码简洁,逻辑简单,一个 AddIndex 方法 保证字段 index 在 0到100之间,另外一个GetIndex方法用来获取字段index的值。
但是,这一组Demo却有不少问题,甚至可以说是错误,下面我将一一进行说明:
1、忘记同步——即读写操作都需要加锁
GetIndex方法, 由于该方法没有加锁,所以通过该方法在任何时刻都可以访问字段index的值,也就是说会恰好在某个时间点获取到 101 这个值,这一点是和初衷相违背的。
2、读写撕裂
如果说读写撕裂这个问题,这个demo可能不是很直观,但是Long类型确实存在读写撕裂。比如下面的例子:
- /// <summary>
- /// 测试原子性
- /// </summary>
- public void TestAtomicity()
- {
- long test = ;
- long breakFlag = ;
- int index = ;
- Task.Run(() =>
- {
- base.PrintInfo("开始循环 写数据");
- while (true)
- {
- test = (index % == ) ? 0x0 : 0x1234567890abcdef;
- index++;
- if (Interlocked.Read(ref breakFlag) > )
- {
- break;
- }
- }
- base.PrintInfo("退出循环 写数据");
- });
- Task.Run(() =>
- {
- base.PrintInfo("开始循环 读数据");
- while (true)
- {
- long temp = test;
- if (temp != && temp != 0x1234567890abcdef)
- {
- Interlocked.Increment(ref breakFlag);
- base.PrintInfo($"读写撕裂: { Convert.ToString(temp, 16)}");
- break;
- }
- }
- base.PrintInfo("退出循环 读数据");
- });
- }
测试原子性操作
64位的数据结构 在32位的系统上(当然和CPU也有关系)是需要两个命令来实现读写操作的,也就是说、如果恰好在两个写命令中间发生了读取操作,就有可能读取到不完成的数据。故而要警惕读写撕裂。
3、粒度错误
AddIndex 方法中,和 index 无关的大量操作 ,放在锁中是没有必要的,虽然没必要但是也不是错的,只能说这个锁的粒度过大,造成了没必要的并发上的性能影响。
下面举例一个错误的锁粒度:
- public class BankAccount
- {
- private long id;
- private decimal m_balance = 0.0M;
- private object m_balanceLock = new object();
- public void Deposit(decimal delta)
- {
- lock (m_balanceLock)
- {
- m_balance += delta;
- }
- }
- public void Withdraw(decimal delta)
- {
- lock (m_balanceLock)
- {
- if (m_balance < delta)
- throw new Exception("Insufficient funds");
- m_balance -= delta;
- }
- }
- public static void ErrorTransfer(BankAccount a, BankAccount b, decimal delta)
- {
- a.Withdraw(delta);
- b.Deposit(delta);
- }
- public static void Transfer(BankAccount a, BankAccount b, decimal delta)
- {
- lock (a.m_balanceLock)
- {
- lock (b.m_balanceLock)
- {
- a.Withdraw(delta);
- b.Deposit(delta);
- }
- }
- }
- public static void RightTransfer(BankAccount a, BankAccount b, decimal delta)
- {
- if (a.id < b.id)
- {
- Monitor.Enter(a.m_balanceLock); // A first
- Monitor.Enter(b.m_balanceLock); // ...and then B
- }
- else
- {
- Monitor.Enter(b.m_balanceLock); // B first
- Monitor.Enter(a.m_balanceLock); // ...and then A
- }
- try
- {
- a.Withdraw(delta);
- b.Deposit(delta);
- }
- finally
- {
- Monitor.Exit(a.m_balanceLock);
- Monitor.Exit(b.m_balanceLock);
- }
- }
- }
错误的锁粒度
在 ErrorTransfer 方法中,在转账的两个方法中间的时间点上,转账金额属于无主状态,这时锁的粒度就过小了 。
在 Transfer 方法中,虽然粒度正确了,但是此时容易死锁。而比较恰当的方式可以是:RightTransfer 。
4、不合理的lock方式
锁定非私有类型的对象是一种危险的行为,因为非私有类型被暴露给外界、外界也可以对被暴露的对象进行加锁,这种情况下很容造成死锁 或者 错误的锁粒度。
较为合理的方式是 将 thislock 改为 private .
由上述进行类推:
1、lock(this):如果当前类型为外界可访问的也会有类似问题。
2、lock(typeof(T)): 因为Type对象,是整个进程域中是唯一的。所以,如果T为外界可访问的类型也会有类似问题。
3、lock("字符串"):因为String类型的特殊性(内存驻留机制),多个字符串其实有可能是同一把锁,所以、一不小心就容易掉入陷阱、造成死锁 或者错误的锁粒度。
二、通过 IL 代码看本质
下面是 AddIndex 方法的全部il代码 [使用 .NET 4.5类库,VS2015 编译]:
- .method public hidebysig instance void AddIndex() cil managed
- {
- // 代码大小 81 (0x51)
- .maxstack
- .locals init ([] object V_0,
- [] bool V_1,
- [] bool V_2)
- IL_0000: nop
- IL_0001: ldarg.
- IL_0002: ldfld object ParallelDemo.Demo.LockMonitorClass::thisLock
- IL_0007: stloc.
- IL_0008: ldc.i4.
- IL_0009: stloc.
- .try
- {
- IL_000a: ldloc.
- IL_000b: ldloca.s V_1
- IL_000d: call void [mscorlib]System.Threading.Monitor::Enter(object,
- bool&)
- IL_0012: nop
- IL_0013: nop
- IL_0014: ldarg.
- IL_0015: ldarg.
- IL_0016: ldfld int64 ParallelDemo.Demo.LockMonitorClass::index
- IL_001b: ldc.i4.
- IL_001c: conv.i8
- IL_001d: add
- IL_001e: stfld int64 ParallelDemo.Demo.LockMonitorClass::index
- IL_0023: ldarg.
- IL_0024: ldfld int64 ParallelDemo.Demo.LockMonitorClass::index
- IL_0029: ldc.i8 0x3fffffffffffffff
- IL_0032: cgt
- IL_0034: stloc.
- IL_0035: ldloc.
- IL_0036: brfalse.s IL_0042
- IL_0038: nop
- IL_0039: ldarg.
- IL_003a: ldc.i4.
- IL_003b: conv.i8
- IL_003c: stfld int64 ParallelDemo.Demo.LockMonitorClass::index
- IL_0041: nop
- IL_0042: nop
- IL_0043: leave.s IL_0050
- } // end .try
- finally
- {
- IL_0045: ldloc.
- IL_0046: brfalse.s IL_004f
- IL_0048: ldloc.
- IL_0049: call void [mscorlib]System.Threading.Monitor::Exit(object)
- IL_004e: nop
- IL_004f: endfinally
- } // end handler
- IL_0050: ret
- } // end of method LockMonitorClass::AddIndex
IL
当然你没必要完全看懂,你只需要注意到三个细节就可以了:
1、调用 [mscorlib]System.Threading.Monitor::Enter(object, bool&) 方法,其中第二个入参为 索引为1的local变量 [查类库后发现该参数是 ref 传递引用]。
2、如果索引为1的local变量 不为 false,则 调用 [mscorlib]System.Threading.Monitor::Exit(object) 方法
3、try... finally 语句块
换句话,也就是说 lock关键字其实本质上就是 Monitor 类的简化实现方式,为了安全、进行了try...finally处理。
三、Monitor 的 wait和 Pulse
因为进入锁(Enter)和离开锁(Exit)都是有一定的性能损耗的,所以,当有频繁的没有必要的锁操作的时候,性能影响更大。
比如:在生产者消费者模式中,如果没有需要消费的数据时,对锁的频繁操作是没有必要的(轮询模式,不是推送)。
在这种情况下, wait方法就派上用场了。如下是MSDN中的一句备注:
当前拥有对指定对象的锁的线程调用此方法以释放该对象,以便另一个线程可以访问它。 等待重新获取锁时阻止调用方。 当调用方需要等待另一个线程操作后将发生状态更改时,调用此方法。
wait 和 pulse 方法一笔带过,这对方法、笔者用的也不多。
随笔暂告一段落、下一篇随笔介绍: 锁(ReaderWriterLockSlim)(预计1篇随笔)
附,Demo : http://files.cnblogs.com/files/08shiyan/ParallelDemo.zip
参见更多:随笔导读:同步与异步
(未完待续...)
.NET 同步与异步之锁(Lock、Monitor)(七)的更多相关文章
- .NET 同步与异步之锁(ReaderWriterLockSlim)(八)
本随笔续接:.NET 同步与异步之锁(Lock.Monitor)(七) 由于锁 ( lock 和 Monitor ) 是线程独占式访问的,所以其对性能的影响还是蛮大的,那有没有一种方式可是实现:允许多 ...
- C# 异步锁 await async锁,lock,Monitor,SemaphoreSlim
异步方法内无法使用Monitor 和lock 所以只能用System.Threading.SemaphoreSlim了 //Semaphore (int initialCount, int maxim ...
- python多线程编程—同步原语入门(锁Lock、信号量(Bounded)Semaphore)
摘录python核心编程 一般的,多线程代码中,总有一些特定的函数或者代码块不希望(或不应该)被多个线程同时执行(比如两个线程运行的顺序发生变化,就可能造成代码的执行轨迹或者行为不相同,或者产生不一致 ...
- C# 同步锁 lock Monitor
Lock关键字 C#提供lock关键字实现临界区,MSDN里给出的用法: Object thisLock = new Object();lock (thisLock){ // Critical c ...
- .NET 同步与异步 之 原子操作和自旋锁(Interlocked、SpinLock)(九)
本随笔续接:.NET 同步与异步之锁(ReaderWriterLockSlim)(八) 之前的随笔已经说过.加锁虽然能很好的解决竞争条件,但也带来了负面影响:性能方面的负面影响.那有没有更好的解决方案 ...
- 并发、并行、同步、异步、全局解释锁GIL、同步锁Lock、死锁、递归锁、同步对象/条件、信号量、队列、生产者消费者、多进程模块、进程的调用、Process类、
并发:是指系统具有处理多个任务/动作的能力. 并行:是指系统具有同时处理多个任务/动作的能力. 并行是并发的子集. 同步:当进程执行到一个IO(等待外部数据)的时候. 异步:当进程执行到一个IO不等到 ...
- 重新想象 Windows 8 Store Apps (46) - 多线程之线程同步: Lock, Monitor, Interlocked, Mutex, ReaderWriterLock
[源码下载] 重新想象 Windows 8 Store Apps (46) - 多线程之线程同步: Lock, Monitor, Interlocked, Mutex, ReaderWriterLoc ...
- C# 多线程(lock,Monitor,Mutex,同步事件和等待句柄)
本篇从 Monitor,Mutex,ManualResetEvent,AutoResetEvent,WaitHandler 的类关系图开始,希望通过本篇的介绍能对常见的线程同步方法有一个整体的认识,而 ...
- 【转】多线程:C#线程同步lock,Monitor,Mutex,同步事件和等待句柄(上)
本篇从Monitor,Mutex,ManualResetEvent,AutoResetEvent,WaitHandler的类关系图开始,希望通过 本篇的介绍能对常见的线程同步方法有一个整体的认识,而对 ...
随机推荐
- php soap调用asp.net webservice
原文:php soap调用asp.net webservice 首先做一下准备工作,找到安装环境里的php.ini把;extension=php_soap.dll去掉前面的;.我这里使用的是wamp, ...
- JQuery Smart UI
JQuery Smart UI 个人开发的一套使用htm+js的开发框架 SmartUI2.0后续声明 摘要: 感谢很多朋友关注,因为今年一直在另外一个公司做顾问,网络环境管制相当严格,所以一直没有更 ...
- android 首开机会在数据链接图标的状态栏打开并自行消失主动
请找到该文件ConnectivityService.java (alps\frameworks\base\services\java\com\android\server) 在connectivit ...
- sql材料分级统计及汇总案例参考
--第一步:根据系统编号.列.单价分组求和 select CLBH,DJ,sum(SL) as SL,sum(JE) as JE,Lie into #TempSZCMX from #ShouZhiCu ...
- Roslyn 编译平台概述
在Language Feature Status上面看到,其实更新的并不是特别多,为了不会误导看了C# 6.0 功能预览 (一)的园友,现在把官方的更新列表拿了过来,供大家参考 C# 6.0 功能预览 ...
- 一键部署mono 免费空间
一键部署mono 免费空间支持c# 再也不担心伙食费换空间了 一直以来 部署mono 都是很头疼的事情 因为是我在是不熟悉非win环境,今天偶然发现这个项目,挺好的,分享下 https://githu ...
- c# in deep 之Lambda表达式于LINQ表达式结合后令人惊叹的简洁(2)
当Lambda表达式和LINQ一起使用时,我们会发现原本冗长的代码会变得如此简单.比如我们要打印0-10之间的奇数,让其从高到低排列并求其平方根,现在只用一行代码即可完成其集合的生成,直接上代码: v ...
- C# 多线程学习总结
C# 多线程学习总结 C#多线程学习(一) 多线程的相关概念 什么是进程? 当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源.而一个进程又是由多个线程所组成的. ...
- POJ 2337 输出欧拉路径
太无语了. 这道题做了一整天. 主要还是我太弱了. 以后这个就当输出欧拉路径的模版吧. 题目中的输出字典序最小我有点搞不清楚,看了别人是这么写的.但是我发现我过不了后面DISCUSS里面的数据. 题意 ...
- Solr与MongoDB集成,实时增量索引
Solr与MongoDB集成,实时增量索引 一. 概述 大量的数据存储在MongoDB上,需要快速搜索出目标内容,于是搭建Solr服务. 另外一点,用Solr索引数据后,可以把数据用在不同的项目当中, ...