本随笔续接:.NET同步与异步之相关背景知识(六)

在上一篇随笔中已经提到、解决竞争条件的典型方式就是加锁 ,那本篇随笔就重点来说一说.NET提供的最常用的锁 lock关键字 和 Monitor。

一、lock关键字Demo

  1. public object thisLock = new object();
  2. private long index;
  3.  
  4. public void AddIndex()
  5. {
  6. lock (this.thisLock)
  7. {
  8. this.index++;
  9.  
  10. if (this.index > long.MaxValue / )
  11. {
  12. this.index = ;
  13. }
             // 和 index 无关的大量操作
  14. }
  15. }
  16.  
  17. public long GetIndex()
  18. {
  19. return this.index;
  20. }

这一组demo,代码简洁,逻辑简单,一个 AddIndex 方法 保证字段 index 在 0到100之间,另外一个GetIndex方法用来获取字段index的值。

但是,这一组Demo却有不少问题,甚至可以说是错误,下面我将一一进行说明:

1、忘记同步——即读写操作都需要加锁

  GetIndex方法, 由于该方法没有加锁,所以通过该方法在任何时刻都可以访问字段index的值,也就是说会恰好在某个时间点获取到 101 这个值,这一点是和初衷相违背的。

2、读写撕裂

  如果说读写撕裂这个问题,这个demo可能不是很直观,但是Long类型确实存在读写撕裂。比如下面的例子:

  1. /// <summary>
  2. /// 测试原子性
  3. /// </summary>
  4. public void TestAtomicity()
  5. {
  6. long test = ;
  7.  
  8. long breakFlag = ;
  9. int index = ;
  10. Task.Run(() =>
  11. {
  12. base.PrintInfo("开始循环 写数据");
  13. while (true)
  14. {
  15. test = (index % == ) ? 0x0 : 0x1234567890abcdef;
  16.  
  17. index++;
  18.  
  19. if (Interlocked.Read(ref breakFlag) > )
  20. {
  21. break;
  22. }
  23. }
  24.  
  25. base.PrintInfo("退出循环 写数据");
  26. });
  27.  
  28. Task.Run(() =>
  29. {
  30. base.PrintInfo("开始循环 读数据");
  31. while (true)
  32. {
  33. long temp = test;
  34.  
  35. if (temp != && temp != 0x1234567890abcdef)
  36. {
  37. Interlocked.Increment(ref breakFlag);
  38. base.PrintInfo($"读写撕裂: { Convert.ToString(temp, 16)}");
  39. break;
  40. }
  41. }
  42.  
  43. base.PrintInfo("退出循环 读数据");
  44. });
  45. }

测试原子性操作

64位的数据结构 在32位的系统上(当然和CPU也有关系)是需要两个命令来实现读写操作的,也就是说、如果恰好在两个写命令中间发生了读取操作,就有可能读取到不完成的数据。故而要警惕读写撕裂。

3、粒度错误

  AddIndex 方法中,和 index 无关的大量操作 ,放在锁中是没有必要的,虽然没必要但是也不是错的,只能说这个锁的粒度过大,造成了没必要的并发上的性能影响。

下面举例一个错误的锁粒度:

  1. public class BankAccount
  2. {
  3. private long id;
  4. private decimal m_balance = 0.0M;
  5.  
  6. private object m_balanceLock = new object();
  7.  
  8. public void Deposit(decimal delta)
  9. {
  10. lock (m_balanceLock)
  11. {
  12. m_balance += delta;
  13. }
  14. }
  15.  
  16. public void Withdraw(decimal delta)
  17. {
  18. lock (m_balanceLock)
  19. {
  20. if (m_balance < delta)
  21. throw new Exception("Insufficient funds");
  22. m_balance -= delta;
  23. }
  24. }
  25.  
  26. public static void ErrorTransfer(BankAccount a, BankAccount b, decimal delta)
  27. {
  28. a.Withdraw(delta);
  29. b.Deposit(delta);
  30. }
  31.  
  32. public static void Transfer(BankAccount a, BankAccount b, decimal delta)
  33. {
  34. lock (a.m_balanceLock)
  35. {
  36. lock (b.m_balanceLock)
  37. {
  38. a.Withdraw(delta);
  39. b.Deposit(delta);
  40. }
  41. }
  42. }
  43.  
  44. public static void RightTransfer(BankAccount a, BankAccount b, decimal delta)
  45. {
  46. if (a.id < b.id)
  47. {
  48. Monitor.Enter(a.m_balanceLock); // A first
  49. Monitor.Enter(b.m_balanceLock); // ...and then B
  50. }
  51. else
  52. {
  53. Monitor.Enter(b.m_balanceLock); // B first
  54. Monitor.Enter(a.m_balanceLock); // ...and then A
  55. }
  56.  
  57. try
  58. {
  59. a.Withdraw(delta);
  60. b.Deposit(delta);
  61. }
  62. finally
  63. {
  64. Monitor.Exit(a.m_balanceLock);
  65. Monitor.Exit(b.m_balanceLock);
  66. }
  67. }
  68.  
  69. }

错误的锁粒度

在 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 编译]:

  1. .method public hidebysig instance void AddIndex() cil managed
  2. {
  3. // 代码大小 81 (0x51)
  4. .maxstack
  5. .locals init ([] object V_0,
  6. [] bool V_1,
  7. [] bool V_2)
  8. IL_0000: nop
  9. IL_0001: ldarg.
  10. IL_0002: ldfld object ParallelDemo.Demo.LockMonitorClass::thisLock
  11. IL_0007: stloc.
  12. IL_0008: ldc.i4.
  13. IL_0009: stloc.
  14. .try
  15. {
  16. IL_000a: ldloc.
  17. IL_000b: ldloca.s V_1
  18. IL_000d: call void [mscorlib]System.Threading.Monitor::Enter(object,
  19. bool&)
  20. IL_0012: nop
  21. IL_0013: nop
  22. IL_0014: ldarg.
  23. IL_0015: ldarg.
  24. IL_0016: ldfld int64 ParallelDemo.Demo.LockMonitorClass::index
  25. IL_001b: ldc.i4.
  26. IL_001c: conv.i8
  27. IL_001d: add
  28. IL_001e: stfld int64 ParallelDemo.Demo.LockMonitorClass::index
  29. IL_0023: ldarg.
  30. IL_0024: ldfld int64 ParallelDemo.Demo.LockMonitorClass::index
  31. IL_0029: ldc.i8 0x3fffffffffffffff
  32. IL_0032: cgt
  33. IL_0034: stloc.
  34. IL_0035: ldloc.
  35. IL_0036: brfalse.s IL_0042
  36. IL_0038: nop
  37. IL_0039: ldarg.
  38. IL_003a: ldc.i4.
  39. IL_003b: conv.i8
  40. IL_003c: stfld int64 ParallelDemo.Demo.LockMonitorClass::index
  41. IL_0041: nop
  42. IL_0042: nop
  43. IL_0043: leave.s IL_0050
  44. } // end .try
  45. finally
  46. {
  47. IL_0045: ldloc.
  48. IL_0046: brfalse.s IL_004f
  49. IL_0048: ldloc.
  50. IL_0049: call void [mscorlib]System.Threading.Monitor::Exit(object)
  51. IL_004e: nop
  52. IL_004f: endfinally
  53. } // end handler
  54. IL_0050: ret
  55. } // 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)(七)的更多相关文章

  1. .NET 同步与异步之锁(ReaderWriterLockSlim)(八)

    本随笔续接:.NET 同步与异步之锁(Lock.Monitor)(七) 由于锁 ( lock 和 Monitor ) 是线程独占式访问的,所以其对性能的影响还是蛮大的,那有没有一种方式可是实现:允许多 ...

  2. C# 异步锁 await async锁,lock,Monitor,SemaphoreSlim

    异步方法内无法使用Monitor 和lock 所以只能用System.Threading.SemaphoreSlim了 //Semaphore (int initialCount, int maxim ...

  3. python多线程编程—同步原语入门(锁Lock、信号量(Bounded)Semaphore)

    摘录python核心编程 一般的,多线程代码中,总有一些特定的函数或者代码块不希望(或不应该)被多个线程同时执行(比如两个线程运行的顺序发生变化,就可能造成代码的执行轨迹或者行为不相同,或者产生不一致 ...

  4. C# 同步锁 lock Monitor

    Lock关键字 C#提供lock关键字实现临界区,MSDN里给出的用法: Object thisLock = new Object();lock (thisLock){   // Critical c ...

  5. .NET 同步与异步 之 原子操作和自旋锁(Interlocked、SpinLock)(九)

    本随笔续接:.NET 同步与异步之锁(ReaderWriterLockSlim)(八) 之前的随笔已经说过.加锁虽然能很好的解决竞争条件,但也带来了负面影响:性能方面的负面影响.那有没有更好的解决方案 ...

  6. 并发、并行、同步、异步、全局解释锁GIL、同步锁Lock、死锁、递归锁、同步对象/条件、信号量、队列、生产者消费者、多进程模块、进程的调用、Process类、

    并发:是指系统具有处理多个任务/动作的能力. 并行:是指系统具有同时处理多个任务/动作的能力. 并行是并发的子集. 同步:当进程执行到一个IO(等待外部数据)的时候. 异步:当进程执行到一个IO不等到 ...

  7. 重新想象 Windows 8 Store Apps (46) - 多线程之线程同步: Lock, Monitor, Interlocked, Mutex, ReaderWriterLock

    [源码下载] 重新想象 Windows 8 Store Apps (46) - 多线程之线程同步: Lock, Monitor, Interlocked, Mutex, ReaderWriterLoc ...

  8. C# 多线程(lock,Monitor,Mutex,同步事件和等待句柄)

    本篇从 Monitor,Mutex,ManualResetEvent,AutoResetEvent,WaitHandler 的类关系图开始,希望通过本篇的介绍能对常见的线程同步方法有一个整体的认识,而 ...

  9. 【转】多线程:C#线程同步lock,Monitor,Mutex,同步事件和等待句柄(上)

    本篇从Monitor,Mutex,ManualResetEvent,AutoResetEvent,WaitHandler的类关系图开始,希望通过 本篇的介绍能对常见的线程同步方法有一个整体的认识,而对 ...

随机推荐

  1. php soap调用asp.net webservice

    原文:php soap调用asp.net webservice 首先做一下准备工作,找到安装环境里的php.ini把;extension=php_soap.dll去掉前面的;.我这里使用的是wamp, ...

  2. JQuery Smart UI

    JQuery Smart UI 个人开发的一套使用htm+js的开发框架 SmartUI2.0后续声明 摘要: 感谢很多朋友关注,因为今年一直在另外一个公司做顾问,网络环境管制相当严格,所以一直没有更 ...

  3. android 首开机会在数据链接图标的状态栏打开并自行消失主动

    请找到该文件ConnectivityService.java (alps\frameworks\base\services\java\com\android\server)  在connectivit ...

  4. sql材料分级统计及汇总案例参考

    --第一步:根据系统编号.列.单价分组求和 select CLBH,DJ,sum(SL) as SL,sum(JE) as JE,Lie into #TempSZCMX from #ShouZhiCu ...

  5. Roslyn 编译平台概述

    在Language Feature Status上面看到,其实更新的并不是特别多,为了不会误导看了C# 6.0 功能预览 (一)的园友,现在把官方的更新列表拿了过来,供大家参考 C# 6.0 功能预览 ...

  6. 一键部署mono 免费空间

    一键部署mono 免费空间支持c# 再也不担心伙食费换空间了 一直以来 部署mono 都是很头疼的事情 因为是我在是不熟悉非win环境,今天偶然发现这个项目,挺好的,分享下 https://githu ...

  7. c# in deep 之Lambda表达式于LINQ表达式结合后令人惊叹的简洁(2)

    当Lambda表达式和LINQ一起使用时,我们会发现原本冗长的代码会变得如此简单.比如我们要打印0-10之间的奇数,让其从高到低排列并求其平方根,现在只用一行代码即可完成其集合的生成,直接上代码: v ...

  8. C# 多线程学习总结

    C# 多线程学习总结 C#多线程学习(一) 多线程的相关概念 什么是进程? 当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源.而一个进程又是由多个线程所组成的. ...

  9. POJ 2337 输出欧拉路径

    太无语了. 这道题做了一整天. 主要还是我太弱了. 以后这个就当输出欧拉路径的模版吧. 题目中的输出字典序最小我有点搞不清楚,看了别人是这么写的.但是我发现我过不了后面DISCUSS里面的数据. 题意 ...

  10. Solr与MongoDB集成,实时增量索引

    Solr与MongoDB集成,实时增量索引 一. 概述 大量的数据存储在MongoDB上,需要快速搜索出目标内容,于是搭建Solr服务. 另外一点,用Solr索引数据后,可以把数据用在不同的项目当中, ...