转:C# 线程同步技术 Monitor 和Lock
原文地址: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,记录一 ...
- Java多线程之线程同步【synchronized、Lock、volatitle】
线程同步 线程同步:当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多. ...
- Linux/Unix 线程同步技术之互斥量(1)
众所周知,互斥量(mutex)是同步线程对共享资源访问的技术,用来防止下面这种情况:线程A试图访问某个共享资源时,线程B正在对其进行修改,从而造成资源状态不一致.与之相关的一个术语临界区(critic ...
随机推荐
- tomcat启动(Ⅶ)请求处理--Processor.process(SocketWrapper<S> socketWrapper)
tomcat启动(六)Catalina分析-StandardServer.start() 上一篇分析到:Http11NioProcessor.process(SocketWrapper<S> ...
- tomcat启动(五)Catalina分析-service.init
上篇写到StandardService.init() 这个方法做什么呢?一起来看看. 这个类也是实现了Lifecycle 如图.这个图中i表示Interface接口.如Lifecycle,Contai ...
- GRU
GRU模型(比LSTM减少了计算量) LSTM的模型,LSTM的重复网络模块的结构很复杂,它实现了三个门计算,即遗忘门.输入门和输出门. 而GRU模型如下,它只有两个门了,分别为更新门和重置门,即图中 ...
- j2ee高级开发技术课程第一周
一.课程目标 这学期开始了J2EE高级开发技术这门课,在此之前我学习了javaSE,为这门课的学习打下了一定的基础.到这学期的结束我希望我能熟悉javaee,能开发企业级应用,对开发轻量级企业应用的主 ...
- mongodb-导出数据到csv文件或json文件
在mongodb的bin目录下, 有一个mongoexport, 可用于数据的导出 [wenbronk@localhost bin]$ ./mongoexport --help Usage: mong ...
- SVN 基本的工作循环
基本的工作循环 Subversion有许多特性.选项和华而不实的高级功能,但日常的工作中你只使用其中的一小部分,在这一节里,我们会介绍许多你在日常工作中常用的命令. 典型的工作周期是这样的: 更新你的 ...
- Automapper问题记录
在Automapper使用中会碰到一些未能映射或者错误的问题,这些问题可能会经常忘记如何处理,想到一些就记录一些: 映射值有时为空又不报错的情况 这很可能是由于目标类中的部分属性有问题导致的,最简单的 ...
- Rails 增加一个模型(model)
之前我们已经看到用脚手架运行的model程序.现在是时候第二个model了. 第二个model用来处理post的评论. 7.1 新建一个模型 Rails模型使用一个单一的的名称,其相应的数据库表使 ...
- 15.Generator 函数的语法
Generator 函数的语法 Generator 函数的语法 简介 基本概念 Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同.本章详细介绍 Generat ...
- 【转】分布式环境下5种session处理策略(大型网站技术架构:核心原理与案例分析 里面的方案)
前言 在搭建完集群环境后,不得不考虑的一个问题就是用户访问产生的session如何处理.如果不做任何处理的话,用户将出现频繁登录的现象,比如集群中存在A.B两台服务器,用户在第一次访问网站时,Ngin ...