原文地址:http://www.cnblogs.com/lxblog/archive/2013/03/07/2947182.html
今天我们总结一下 C#线程同步 中的 Monitor 类 和 Lock 关键字进行一下总结。
首先来看看他们有什么异同(相信对此熟悉的朋友们都很清楚):
1、他们都是在指定对象上获取排他锁,用于同步代码区 lock(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() 方法来解决。
转:C# 线程同步技术 Monitor 和Lock的更多相关文章
-
Java线程同步的Monitor机制(Lock配合Condition)
Monitor模式是一种常见的并行开发机制, 一个Monitor实例可以被多个线程安全使用, 所有的monitor下面的方法在运行时是互斥的, 这种互斥机制机制可以用于一些特性, 例如让线程等待某种条 ...
-
C#线程同步技术(二) Interlocked 类
接昨天谈及的线程同步问题,今天介绍一个比较简单的类,Interlocked.它提供了以线程安全的方式递增.递减.交换和读取值的方法. 它的特点是: 1.相对于其他线程同步技术,速度会快很多. 2.只能 ...
-
多线程状态与优先级、线程同步与Monitor类、死锁
一.线程状态 二.线程优先级 三.初步尝试多线程 class Program { static void Main(string[] args) { while (true) { MessagePri ...
-
iOS开发系列-线程同步技术
概述 多线程的本质就是CPU轮流随机分配给每条线程时间片资源执行任务,看起来多条线程同时执行任务. 多条线程同时访问同一块资源,比如操作同一个对象.统一变量.同一个文件,就会引发数据错乱和数据安全的问 ...
-
【WIN32进阶之路】:线程同步技术纲要
前面博客讲了互斥量(MUTEX)和关键段(CRITICAL SECTION)的使用,想来总觉不妥,就如盲人摸象一般,窥其一脚而言象,难免以偏概全,追加一篇博客查遗补漏. win32下的线程同步技术分为 ...
-
(删)Java线程同步实现二:Lock锁和Condition
在上篇文章(3.Java多线程总结系列:Java的线程同步实现)中,我们介绍了用synchronized关键字实现线程同步.但在Java中还有一种方式可以实现线程同步,那就是Lock锁. 一.同步锁 ...
-
C#线程同步技术(一) lock 语句
开篇语: 上班以后,烦恼少了,至少是没有什么好烦的了,只要负责好自己的工作就可以了,因此也有更多的时间去探索自己喜欢的程序.买回来的书已经看了一半,DEMO也敲了不少,昨晚终于在这里开BLOG,记录一 ...
-
Linux/Unix 线程同步技术之互斥量(1)
众所周知,互斥量(mutex)是同步线程对共享资源访问的技术,用来防止下面这种情况:线程A试图访问某个共享资源时,线程B正在对其进行修改,从而造成资源状态不一致.与之相关的一个术语临界区(critic ...
-
Delphi 线程同步技术(转)
上次跟大家分享了线程的标准代码,其实在线程的使用中最重要的是线程的同步问题,如果你在使用线程后,发现你的界面经常被卡死,或者无法显示出来,显示混乱,你的使用的变量值老是不按预想的变化,结果往往出乎意料 ...
随机推荐
-
HBase分享会议笔记
今天参加了一个关于HBase的分享,有一些内容是之前的知识的补充. 之前关于Hadoop家族,包括HBase的内容,可以参考:http://www.cnblogs.com/charlesblc/p/6 ...
-
Codeforces 418d Big Problems for Organizers [树形dp][倍增lca]
题意: 给你一棵有n个节点的树,树的边权都是1. 有m次询问,每次询问输出树上所有节点离其较近结点距离的最大值. 思路: 1.首先是按照常规树形dp的思路维护一个子树节点中距离该点的最大值son_di ...
-
《java.util.concurrent 包源码阅读》07 LinkedBlockingQueue
这篇文章来说说稍微复杂一些的LinkedBlockingQueue.LinkedBlockingQueue使用一个链表来实现,会有一个head和tail分别指向队列的开始和队列的结尾.因此Linked ...
-
简单的C语言编译器--语法分析器
语法分析算是最难的一部分了.总而言之,语法分析就是先设计一系列语法,然后再用设计好的语法去归约词法分析中的结果.最后将归约过程打印出来,或者生成抽象语法树. 1. 设计文法 以下是我的文法(引入的 ...
-
SpringMVC之Ajax与Controller交互
前面学习了拦截器,通过拦截器我们可以拦截请求,做进一步处理之后再往下进行,这里我们使用Ajax的时候会有一个问题就是会把js.css这些静态资源文件也进行了拦截,这样在jsp中就无法引入的静态资源文件 ...
-
ubuntu 14.04 rabbitmq集群部署
1.准备机器,我这边准备的是三台ubuntu14.04 机器主机名不能相同,不然节点冲突 2.安装rabbitmq 3.修改hosts文件 root@abc-web-04:~# vim /etc/ho ...
-
彻底征服 Spring AOP 之 实战篇
Spring AOP 实战 看了上面这么多的理论知识, 不知道大家有没有觉得枯燥哈. 不过不要急, 俗话说理论是实践的基础, 对 Spring AOP 有了基本的理论认识后, 我们来看一下下面几个 ...
-
mysql理论结合实际篇(一)
最近两天做需求,是要将退款和退货报表里使用的临时表改用固定表, 自己建表时,如(只是举例): CREATE TABLE tasks ( task_id INT UNSIGNED NOT NULL AU ...
-
Ubuntu下安装virtualbox: RTR3InitEx failed with rc=-1912 (rc=-1912)
下载好合适的安装包: http://www.oracle.com/technetwork/server-storage/virtualbox/downloads/index.html 然后进行安装,配 ...
-
腾讯后台研发暑期实习offer经历
昨晚看到腾讯校招的微信状态,一颗心终于落下来了,终于可以去梦寐以求的鹅厂工作了.想想这一个多月以来,心情就像过山车一样,此起彼伏,一会充满希望,一会又跌入谷底. 三月份的时候,听说腾讯可以内推了,我内 ...