原文地址:http://www.cnblogs.com/lxblog/archive/2013/03/07/2947182.html

今天我们总结一下 C#线程同步 中的 Monitor 类 和 Lock 关键字进行一下总结。

首先来看看他们有什么异同(相信对此熟悉的朋友们都很清楚):

1、他们都是在指定对象上获取排他锁,用于同步代码区
2、Lock关键字是Monitor的一种替换用法,lock在IL代码中会被翻译成Monitor.

lock(obj){
//代码段

就等同于 
Monitor.Enter(obj); 
//代码段
Monitor.Exit(obj);

所以lock能做的,Monitor肯定能做,Monitor能做的,lock不一定能做,我们今天就主要说的就是Monitor 类。

Monitor 类 通过Enter(Object) 在指定对象上获取排他锁,通过Exit 方法释放指定对象上的排他锁。
Enter方法:使用 Enter 获取作为参数传递的对象上的 Monitor。如果其他线程已对该对象执行了 Enter,但尚未执行对应的 Exit,则当前线程将阻止,直到对方线程释放该对象

Exit方法:调用线程必须拥有 obj 参数上的锁。如果调用线程拥有指定对象上的锁并为该对象进行了相同次数的 Exit 和 Enter 调用,则该锁将被释放。如果调用线程调用 Exit 与调用 Enter 的次数不同,则该锁不会被释放。

我们来做一个游戏杀怪的例子来演示一下吧:建立一个控制台程序,并增加一个怪物类(Monster),代码如下:

public class Monster
{public Monster(int blood){this.Blood = blood;            Console.WriteLine(string.Format("我是怪物,我有 {0} 滴血!\r\n", blood));}public int Blood { get; set; }
}

然后呢,我们在增加一个Player 类,里面有个物理工具的方法,此方法没有采取任何线程同步的措施:

 public class Player{//姓名public string Name { get; set; }//武器public string Weapon { get; set; }//攻击力public int Power { get; set; }//物理攻击public void PhysAttack(Object monster){Monster m = monster as Monster;while (m.Blood > 0){Console.WriteLine("当前玩家 【{0}】,使用{1}攻击怪物!", this.Name, this.Weapon);if (m.Blood >= this.Power){m.Blood -= this.Power;}else{m.Blood = 0;}     Console.WriteLine("怪物剩余血量:{0}\r\n", m.Blood);}
}

在主函数中,我们实例化两个玩家角色,一个游侠,一个野蛮人,并开启两个线程来调用一下他们的物理攻击方法,攻击同一个怪物。

static void Main(string[] args)
{            Monster monster = new Monster(1000);Player YouXia = new Player() { Name = "游侠", Weapon = "宝剑", Power = 150 };Player YeManRen = new Player() { Name = "野蛮人", Weapon = "链锤", Power = 250 };Thread t1 = new Thread(new ParameterizedThreadStart(YouXia.PhysAttack));t1.Start(monster);Thread t2 = new Thread(new ParameterizedThreadStart(YeManRen.PhysAttack));t2.Start(monster);t1.Join();t2.Join();Console.ReadKey();}

由于没有采取线程同步的措施,运行结果可想而知,当然不同的计算机运行结果是不一样的,我的如下图:

这种结果肯定不是我们想要的,我们来对Player 类中的物理攻击方法,修改一下,用Monitor 类来实现一下线程同步,当然也可以用Lock 关键字,修改的代码如下:

 //物理攻击public void PhysAttack(Object monster)
{Monster m = monster as Monster;while (m.Blood > 0) //异步读{Monitor.Enter(monster);if (m.Blood > 0) //同步读{Console.WriteLine("当前玩家 【{0}】,使用{1}攻击怪物!", this.Name, this.Weapon);if (m.Blood >= this.Power){m.Blood -= this.Power;}else{m.Blood = 0;}Console.WriteLine("怪物剩余血量:{0}\r\n", m.Blood);}Thread.Sleep(500);Monitor.Exit(monster);}}

由于我们加上了Monitor.Enter(monster) 和 Monitor.Exit(monster); 期间的代码段是线程同步的。假如程序启动后,游侠所在的线程 先进入了Monitor.Enter(monster),这时候游侠线程拥有对monster实例的排他锁,其他的线程必须等待,野蛮人线程运行到Monitor.Enter(monster)的时候,就会发生阻塞,直到游侠线程执行 Monitor.Exit(monster);之后,释放了排他锁,野蛮人线程才能进行杀怪的操作,此时野蛮人线程拥有排他锁的控制权,游侠线程就必须等待。运行结果如下:

将上面的代码中的 Monitor.Enter(monster); 和 Monitor.Exit(monster); 替换成lock(monster);是会得到同样的效果的,那么我们再来看lock 没有的功能。Monitor类中的Wait(object) 和Pulse 方法。

 Wait(object)方法:释放对象上的锁并阻止当前线程,直到它重新获取该锁,该线程进入等待队列。
 Pulse方法:只有锁的当前所有者可以使用 Pulse 向等待对象发出信号,当前拥有指定对象上的锁的线程调用此方法以便向队列中的下一个线程发出锁的信号。接收到脉冲后,等待线程就被移动到就绪队列中。在调用 Pulse 的线程释放锁后,就绪队列中的下一个线程(不一定是接收到脉冲的线程)将获得该锁。
另外:Wait 和 Pulse 方法必须写在 Monitor.Enter 和Moniter.Exit 之间。

不明白MSDN的解释,没有关系,不明白什么是等待队列和就绪队列也没有关系,来继续我们的实例。为了好演示,为Player类又增加两方法一个是 魔法攻击,一个是闪电攻击,两者的代码是一样的,只不过分别加上了Monitor.Wait 和 Monitor.Exit 方法。

 //魔法攻击public void MigcAttack(Object monster){Monster m = monster as Monster;Monitor.Enter(monster);Console.WriteLine("当前玩家 {0} 进入战斗\r\n",this.Name);while (m.Blood > 0){Monitor.Wait(monster);Console.WriteLine("当前玩家 {0} 获得攻击权限", this.Name);Console.WriteLine("当前玩家 {0},使用 魔法 攻击怪物!", this.Name, this.Weapon);m.Blood = (m.Blood >= this.Power) ? m.Blood - this.Power : 0;Console.WriteLine("怪物剩余血量:{0}\r\n", m.Blood);Thread.Sleep(500);Monitor.Pulse(monster);}Monitor.Exit(monster);}//闪电攻击public void LightAttack(Object monster){Monster m = monster as Monster;            Monitor.Enter(monster);Console.WriteLine("当前玩家 {0} 进入战斗\r\n", this.Name);while (m.Blood > 0){Monitor.Pulse(monster);Console.WriteLine("当前玩家 {0} 获得攻击权限", this.Name);Console.WriteLine("当前玩家 {0},使用 闪电 攻击怪物!", this.Name);m.Blood = (m.Blood >= this.Power) ? m.Blood - this.Power : 0;Console.WriteLine("怪物剩余血量:{0}\r\n", m.Blood);Thread.Sleep(500);Monitor.Wait(monster);}Monitor.Exit(monster);}

并在Main方法中开两个线程进行调用:

static void Main(string[] args)
{Monster monster = new Monster(1500);Player Cike = new Player() { Name = "刺客", Power = 250 };Player Mofashi = new Player() { Name = "魔法师", Power = 350 };
   Thread t1 = new Thread(new ParameterizedThreadStart(Cike.LightAttack));t1.Start(monster);Thread t2 = new Thread(new ParameterizedThreadStart(Mofashi.MigcAttack));t2.Start(monster);
   t1.Join();t2.Join();Console.ReadKey();
}

先不看上面代码的对与错,我们先认为理论上是正确的。

我们分析一下:有这样一种可能,程序运行后,魔法师线程先进入了 Monitor.Enter(monster), 获得了对monser实例的排他锁控制权,然后魔法师线程继续运行,当运行到了Monitor.Wait(monster)的时候,发生了阻塞,魔法师线程释放了排他锁的控制权,进入了等待队列。

这时候,刺客线程才刚刚获得CPU分给的时间片刚刚运行,由于魔法师线程已经释放了排他锁,因此刺客线程顺利的进入了Monitor.Enter(monster),并获得了对monser实例的排他锁控制权,然后 运行到了Monitor.Pulse(monster); 发送了个Pulse信号,告诉魔法师线程,你就绪吧,等我进入Wait之后,你可以杀怪了。

因此刺客线程运行到Wait 之后,魔法师线程可以继续运行。这样两线程的 Wait 和 Pulse 就形成了一个循环,就会出现,刺客用闪电攻击一次,魔法师用魔法攻击一次的情况,直到怪物被干掉。

结果如下图:

若没有出现上面的结果 (多运行几次,总会有机会出现的)。

不过总是有些幸运的人一运行就会出现如下的结果:

程序运行到这里,不动了....,哈哈哈恭喜你,这就是发生了死锁。

我们也来分析一下:

怪物出场后,刺客线程一马当先的进入了杀怪过程,先进入了Monitor.Enter(monster),又发送了Monitor.Pulse(monster),不过此时的没有任何等待线程(白玩),刺客进行闪电攻击后,遇到了Wait 方法,交出了 排他锁控制权,然后去一边儿等待去了。

此时的 魔法师线程才刚刚开始运行,进入了Monitor.Enter(monster),获得排他锁控制权,还没有出招,就碰到了Wait 方法,结果是 也交出了排他锁,去一边儿等待去了,我们的程序就两个角色线程,都去等待去了,不发生死锁才怪!

如何解决上面的问题呢?我们可以采用Wait(object)的一个重载方法 Wait(Object, Int32) 方法。

bool Wait(Object, Int32):释放对象上的锁并阻止当前线程,直到它重新获取该锁。 如果指定的超时间隔已过,则线程进入就绪队列。

结合下面的代码给大家通俗的解释就是:Int32 是一个毫秒数;该方法释放排他锁,阻塞当前线程,如果在规定的毫秒数内获得是锁的控制权,就返回True, 该线程继续运行; 否则就返回False,该线程也继续运行。

来修改一下上面的代码,将魔法攻击和闪电攻击代码修改如下:

//魔法攻击
public void MigcAttack(Object monster)
{Monster m = monster as Monster;Monitor.Enter(monster);Console.WriteLine("当前玩家【{0}】进入战斗\r\n",this.Name);while (m.Blood > 0){Monitor.Wait(monster);Console.WriteLine("当前玩家【{0}】获得攻击权限", this.Name);if (m.Blood > 0){Console.WriteLine("当前玩家【{0}】,使用 魔法 攻击怪物!", this.Name, this.Weapon);m.Blood = (m.Blood >= this.Power) ? m.Blood - this.Power : 0;Console.WriteLine("怪物剩余血量:{0}\r\n", m.Blood);}else{Console.WriteLine("怪物倒下了! 【{0}】停止了魔法攻击 \r\n", this.Name);}Thread.Sleep(500);Monitor.Pulse(monster);}Monitor.Exit(monster);}//闪电攻击
public void LightAttack(Object monster)
{Monster m = monster as Monster;            Monitor.Enter(monster);Console.WriteLine("当前玩家【{0}】进入战斗\r\n", this.Name);while (m.Blood > 0){Monitor.Pulse(monster);if (Monitor.Wait(monster, 1000)) //主要是这里{Console.WriteLine("当前玩家【{0}】获得攻击权限", this.Name);if (m.Blood > 0){Console.WriteLine("当前玩家【{0}】,使用 闪电 攻击怪物!", this.Name);m.Blood = (m.Blood >= this.Power) ? m.Blood - this.Power : 0;Console.WriteLine("怪物剩余血量:{0}\r\n", m.Blood);}else{Console.WriteLine("怪物倒下了! 【{0}】停止了闪电攻击 \r\n", this.Name);}Thread.Sleep(500);}//Monitor.Wait(monster,1000);}Monitor.Exit(monster);
}

由于我们 使用 Monitor.Wait(monster, 1000)修改了 闪电攻击的方法。当刺客线程进入Wait 的时候,我们只让该线程等待1s ,如果 1s 内 获取到了魔法师线程的pusle脉冲信号并获取到了锁控制权,刺客线程就可以进行闪电攻击,如果1s 后,还没有获取到控制权,刺客线程继续运行。总有那么一个时刻魔法师线程进行了等待,刺客线程运行到Pulse 之后,就会通知魔法师线程就绪,再执行到Monitor.Wait(monster, 1000)的时候,魔法师线程进行魔法攻击。如果魔法师线程1s内攻击完成,并运行到Wait的时候。刺客线程可以进入if 进行闪电攻击,如果超时,刺客线程进行循环。

运行结果如下:

通过Monitor.Wait(monster, 1000),我们成功的避免了死锁的发生,我们再来看看 Monitor.TryEnter(Object) 的使用,该方法也能够避免死锁的发生,我们下面的例子用到的是该方法的重载,Monitor.TryEnter(Object,Int32)。

Bool Monitor.TryEnter(Object,Int32):在指定的毫秒数内尝试获取指定对象上的排他锁。如果在指定的毫秒数内获得排他锁,则返回True,否则返回False。

同样结合下面的代码,Int32 是一个毫秒数;该方法尝试去获得排他锁,阻塞当前线程,如果在规定的毫秒数内获得是锁的控制权,就返回True, 该线程继续运行; 否则就返回False,该线程也继续运行。

为了说明情况,我们新建一个简单的计算类,包含加法和减法两个操作:

public class Calculate
{public void Add(){while (true){               if (Monitor.TryEnter(this,1000)) //注意这里,如果1s内获得锁,则进入if,超时后进入else{Console.ForegroundColor = ConsoleColor.Red;Console.WriteLine(string.Format("线程{0}获得锁:进入了加法运算",Thread.CurrentThread.Name));Console.WriteLine("开始加法运算 1s 钟");Thread.Sleep(1000);Console.WriteLine(string.Format("线程{0}释放锁:离开了加法运算\r\n",Thread.CurrentThread.Name));Monitor.Exit(this);}else{Console.ForegroundColor = ConsoleColor.Red;Console.WriteLine("\r\n 由于减法运算未完成,未进入加法运算");                    }             }}public void Sub(){while (true){Monitor.Enter(this);Console.ForegroundColor = ConsoleColor.Blue;Console.WriteLine(string.Format("线程{0}获得锁:进入了减法运算", Thread.CurrentThread.Name));Console.ForegroundColor = ConsoleColor.Blue;Console.WriteLine("开始减法运算 2s 钟");Thread.Sleep(2000);   //让减法运算长一点,可以演示效果Console.ForegroundColor = ConsoleColor.Blue;Console.WriteLine(string.Format("线程{0}释放锁:离开了减法运算\r\n", Thread.CurrentThread.Name));Monitor.Exit(this);Thread.Sleep(2000);}}
}

在Main方法中进行调用:

static void Main(string[] args)
{ Calculate c = new Calculate();Thread t1 = new Thread(new ThreadStart(c.Sub));t1.Name = "减法线程";Thread t2 = new Thread(new ThreadStart(c.Add));t2.Name = "加法线程";t1.Start();t2.Start();t1.Join();t2.Join();Console.ReadKey();}

由于我们的代码中两个方法均用到的是 While(true), 所以两个线程会不停的运行下去,但不会发生死锁。结果如下图:

今天主要总结了 Monitor 类的一些使用方法,希望大家能看明白。另外 Monitor 是很容易产生死锁的类,我们平时可以通过 Wait(object,int32) 方法 和 TryEnter() 方法来解决。

转载于:https://www.cnblogs.com/jearay/p/3668763.html

转:C# 线程同步技术 Monitor 和Lock相关推荐

  1. .Net线程同步技术解读

    C#开发者(面试者)都会遇到lock(Monitor),Mutex,Semaphore,SemaphoreSlim这四个与锁相关的C#类型,本文期望以最简洁明了的方式阐述四种对象的区别. 什么是线程安 ...

  2. (删)Java线程同步实现二:Lock锁和Condition

    在上篇文章(3.Java多线程总结系列:Java的线程同步实现)中,我们介绍了用synchronized关键字实现线程同步.但在Java中还有一种方式可以实现线程同步,那就是Lock锁. 一.同步锁 ...

  3. 【总结】C# 线程同步技术(一)之 Join 方法

    最近工作闲暇之际,翻阅了以前保存的电子书<C#多线程编程手册>,发现此书同步技术这块写的甚好,于是参考此书并结合实例,对同步技术做一下总结和分析,也算是读书笔记与心得体会吧,并与大家分享. ...

  4. VC++中线程同步技术分析3

    管理事件内核对象 在前面讲述线程通信时曾使用过事件内核对象来进行线程间的通信,除此之外,事件内核对象也可以通过通知操作的方式来保持线程的同步.对于前面那段使用临界区保持线程同步的代码可用事件对象的线程 ...

  5. Visual C++线程同步技术

    线程同步的方式有: 临界区 管理事件内核对象 信号量内核对象 互斥内核对象 分别介绍如下: 使线程同步 在程序中使用多线程时,一般很少有多个线程能在其生命期内进行完全独立的操作.更多的情况是一些线程进 ...

  6. VC线程同步技术剖析

    在程序中使用多线程时,一般很少有多个线程能在其生命期内进行完全独立的操作,更多的情况是一些线程进行某些处理操作,而其他的线程必须对其处理结果进行了解.正常情况下对这种处理结果的了解应当在其处理任务完成 ...

  7. educoder 使用线程锁(lock)实现线程同步_性能:Lock的锁之优化

    Lock / synchronized Lock锁的基本操作是通过乐观锁实现的,由于Lock锁也会在阻塞时被挂起,依然属于悲观锁 synchronizedLock实现方式JVM层实现Java底层代码实 ...

  8. iOS开发线程同步技术-锁

    概览 1,什么是锁(临界区)? 2,常用的锁有哪些? 3,相关链接 什么是锁(临界区) 临界区:指的是一块对公共资源进行访问的代码,并非一种机制或是算法. 常用的锁有哪些? 互斥锁:是一种用于多线程编 ...

  9. 线程同步 – lock和Monitor

    在多线程代码中,多个线程可能会访问一些公共的资源(变量.方法逻辑等等),这些公共资源称为临界区(共享区):临界区的资源是不安全,所以需要通过线程同步对多个访问临界区的线程进行控制. 同样,有些时候我们 ...

最新文章

  1. SERU最佳需求分析方法
  2. supervisor
  3. pytorch 语义分割loss_Recall Loss:用于不平衡图像分类和语义分割的召回损失
  4. 如何独立实现一个基于知识图谱的问答系统
  5. 摩托罗拉v8对讲机驱动软件_摩托罗拉数字机如何设置“个性”提示音
  6. Hibernate 常见异常
  7. 转帖:django下操作数据库的字符问题
  8. css移动端页面单位,视窗单位在移动端上的使用技巧
  9. AllenNLP之入门解读代码
  10. 圆形区域的半透明填充
  11. java游戏少年张三丰的原代码_RPG大作《少年张三丰》完美游戏攻略
  12. 2012移动互联网之人在囧途
  13. VScode输入感叹号无法生成HTML模板
  14. 一、PHP基础-安装PHP集成环境
  15. 自媒体平台做网赚不要指望着,平台广告分成!
  16. (强烈推荐)基于SSM和BootStrap的共享云盘系统设计(项目开发与实现:注册/登录)
  17. 1660_MIT 6.828 JOS初始化boot_alloc的初步实现
  18. 想进BAT?这些测试面试题助你一臂之力(附答案)
  19. 蓄电池与超级电容混合储能并网matlab simulink仿真模型
  20. C# 关于AD域的操作 (首博)

热门文章

  1. linux26内核,Linux26内核对象机制研究.pdf
  2. python学习-装饰器(decorator)
  3. java类创建顺序,Java基础----你真的了解java类创建顺序吗?
  4. oracle 拼接_老品牌三明49寸液晶拼接屏多少钱支持定制
  5. 摊牌了,我靠它们成功实现了AI零基础入门到进阶!
  6. 计算机技能需求新排名:C语言仅排第 8,第 1 名你想不到!
  7. 在php100 防恶意注册这个需要怎么填,WordPress防止恶意注册代码
  8. c语言是自动四舍五入,请问c语言如何实现四舍五入?
  9. getclass方法 给属性赋值_反射给没有set方法的属性赋值
  10. java 文件通配符_Java中泛型通配符的使用方法示例