- 使用Monitor类实现线程同步
Lock关键字是Monitor的一种替换用法,lock在IL代码中会被翻译成Monitor.
lock(obj)
{
//代码段
}
就等同于
Monitor.Enter(obj);
//代码段
Monitor.Exit(obj);
Monitor的常用属性和方法:
Enter(Object) 在指定对象上获取排他锁。
Exit(Object) 释放指定对象上的排他锁。
Pulse 通知等待队列中的线程锁定对象状态的更改。
PulseAll 通知所有的等待线程对象状态的更改。
TryEnter(Object) 试图获取指定对象的排他锁。
TryEnter(Object, Boolean) 尝试获取指定对象上的排他锁,并自动设置一个值,指示是否得到了该锁。
Wait(Object) 释放对象上的锁并阻止当前线程,直到它重新获取该锁。
常用的方法有两个,Monitor.Enter(object)方法是获取锁,Monitor.Exit(object)方法是释放锁,这就是Monitor最常用的两个方法,在使用过程中为了避免获取锁之后因为异常,致锁无法释放,所以需要在try{}
catch(){}之后的finally{}结构体中释放锁(Monitor.Exit())。
Enter(Object)的用法很简单,看代码
static void Main(string[] args)
{
Thread threadA = new Thread(ThreadMethod); //执行的必须是无返回值的方法
threadA.Name = "A";
Thread threadB = new Thread(ThreadMethod); //执行的必须是无返回值的方法
threadB.Name = "B";
threadA.Start();
threadB.Start();
Thread.CurrentThread.Name = "C";
ThreadMethod();
Console.ReadKey();
}
static object obj = new object();
public static void ThreadMethod()
{
Monitor.Enter(obj); //Monitor.Enter(obj) 锁定对象
try
{
for (int i = 0; i < 500; i++)
{
Console.Write(Thread.CurrentThread.Name);
}
}
catch(Exception ex){ }
finally
{
Monitor.Exit(obj); //释放对象
}
}
TryEnter(Object)和TryEnter() 方法在尝试获取一个对象上的显式锁方面和 Enter()
方法类似。然而,它不像Enter()方法那样会阻塞执行。如果线程成功进入关键区域那么TryEnter()方法会返回true.
和试图获取指定对象的排他锁。看下面代码演示:
我们可以通过Monitor.TryEnter(monster, 1000),该方法也能够避免死锁的发生,我们下面的例子用到的是该方法的重载,Monitor.TryEnter(Object,Int32),。
static void Main(string[] args)
{
Thread threadA = new Thread(ThreadMethod); //执行的必须是无返回值的方法
threadA.Name = "A";
Thread threadB = new Thread(ThreadMethod); //执行的必须是无返回值的方法
threadB.Name = "B";
threadA.Start();
threadB.Start();
Thread.CurrentThread.Name = "C";
ThreadMethod();
Console.ReadKey();
}
static object obj = new object();
public static void ThreadMethod()
{
bool flag = Monitor.TryEnter(obj, 1000); //设置1S的超时时间,如果在1S之内没有获得同步锁,则返回false //上面的代码设置了锁定超时时间为1秒,也就是说,在1秒中后, //lockObj还未被解锁,TryEntry方法就会返回false,如果在1秒之内,lockObj被解锁,TryEntry返回true。我们可以使用这种方法来避免死锁
try
{
if (flag)
{
for (int i = 0; i < 500; i++)
{
Console.Write(Thread.CurrentThread.Name);
}
}
}
catch(Exception ex)
{
}
finally
{
if (flag)
Monitor.Exit(obj);
}
}
Monitor.Wait和Monitor()Pause()
Wait(object)方法:释放对象上的锁并阻止当前线程,直到它重新获取该锁,该线程进入等待队列。
Pulse方法:只有锁的当前所有者可以使用 Pulse
向等待对象发出信号,当前拥有指定对象上的锁的线程调用此方法以便向队列中的下一个线程发出锁的信号。接收到脉冲后,等待线程就被移动到就绪队列中。在调用
Pulse 的线程释放锁后,就绪队列中的下一个线程(不一定是接收到脉冲的线程)将获得该锁。 另外:
Wait 和 Pulse 方法必须写在 Monitor.Enter 和Moniter.Exit 之间。
上面是MSDN的解释。不明白看代码:
首先我们定义一个攻击类,
/// <summary>
/// 怪物类
/// </summary>
internal class Monster
{
public int Blood { get; set; }
public Monster(int blood)
{
this.Blood = blood;
Console.WriteLine("我是怪物,我有{0}滴血",blood);
}
}
然后在定义一个攻击类
/// <summary>
/// 攻击类
/// </summary>
internal class Play
{
/// <summary>
/// 攻击者名字
/// </summary>
public string Name { get; set; }
/// <summary>
/// 攻击力
/// </summary>
public int Power{ get; set; }
/// <summary>
/// 法术攻击
/// </summary>
public void magicExecute(object monster)
{
Monster m = monster as Monster;
Monitor.Enter(monster);
while (m.Blood>0)
{
Monitor.Wait(monster);
Console.WriteLine("当前英雄:{0},正在使用法术攻击打击怪物", this.Name);
if(m.Blood>= Power)
{
m.Blood -= Power;
}
else
{
m.Blood = 0;
}
Thread.Sleep(300);
Console.WriteLine("怪物的血量还剩下{0}", m.Blood);
Monitor.PulseAll(monster);
}
Monitor.Exit(monster);
}
/// <summary>
/// 物理攻击
/// </summary>
/// <param name="monster"></param>
public void physicsExecute(object monster)
{
Monster m = monster as Monster;
Monitor.Enter(monster);
while (m.Blood > 0)
{
Monitor.PulseAll(monster);
if (Monitor.Wait(monster, 1000)) //非常关键的一句代码
{
Console.WriteLine("当前英雄:{0},正在使用物理攻击打击怪物", this.Name);
if (m.Blood >= Power)
{
m.Blood -= Power;
}
else
{
m.Blood = 0;
}
Thread.Sleep(300);
Console.WriteLine("怪物的血量还剩下{0}", m.Blood);
}
}
Monitor.Exit(monster);
}
}
执行代码:
static void Main(string[] args)
{
//怪物类
Monster monster = new Monster(1000);
//物理攻击类
Play play1 = new Play() { Name = "无敌剑圣", Power = 100 };
//魔法攻击类
Play play2 = new Play() { Name = "流浪法师", Power = 120 };
Thread thread_first = new Thread(play1.physicsExecute); //物理攻击线程
Thread thread_second = new Thread(play2.magicExecute); //魔法攻击线程
thread_first.Start(monster);
thread_second.Start(monster);
Console.ReadKey();
}
输出结果:
总结:
第一种情况:
thread_first首先获得同步对象的锁,当执行到
Monitor.Wait(monster);时,thread_first线程释放自己对同步对象的锁,流放自己到等待队列,直到自己再次获得锁,否则一直阻塞。
而thread_second线程一开始就竞争同步锁所以处于就绪队列中,这时候thread_second直接从就绪队列出来获得了monster对象锁,开始执行到Monitor.PulseAll(monster)时,发送了个Pulse信号。
这时候thread_first接收到信号进入到就绪状态。然后thread_second继续往下执行到
Monitor.Wait(monster,
1000)时,这是一句非常关键的代码,thread_second将自己流放到等待队列并释放自身对同步锁的独占,该等待设置了1S的超时值,当B线程在1S之内没有再次获取到锁自动添加到就绪队列。 这时thread_first从Monitor.Wait(monster)的阻塞结束,返回true。开始执行、打印。执行下一行的Monitor.Pulse(monster),这时候thread_second假如1S的时间还没过,thread_second接收到信号,于是将自己添加到就绪队列。
thread_first的同步代码块结束以后,thread_second再次获得执行权, Monitor.Wait(m_smplQueue,
1000)返回true,于是继续从该代码处往下执行、打印。当再次执行到Monitor.Wait(monster,
1000),又开始了步骤3。 依次循环。。。。
第二种情况:thread_second首先获得同步锁对象,首先执行到Monitor.PulseAll(monster),因为程序中没有需要等待信号进入就绪状态的线程,所以这一句代码没有意义,当执行到
Monitor.Wait(monster, 1000),自动将自己流放到等待队列并在这里阻塞,1S
时间过后thread_second自动添加到就绪队列,线程thread_first获得monster对象锁,执行到Monitor.Wait(monster);时发生阻塞释放同步对象锁,线程thread_second执行,执行Monitor.PulseAll(monster)时通知thread_first。于是又开始第一种情况…
Monitor.Wait是让当前进程睡眠在临界资源上并释放独占锁,它只是等待,并不退出,当等待结束,就要继续执行剩下的代码。
3.0 使用Mutex类实现线程同步
Mutex的突出特点是可以跨应用程序域边界对资源进行独占访问,即可以用于同步不同进程中的线程,这种功能当然这是以牺牲更多的系统资源为代价的。
主要常用的两个方法:
public virtual bool WaitOne() 阻止当前线程,直到当前
System.Threading.WaitHandle 收到信号获取互斥锁。
public void ReleaseMutex() 释放 System.Threading.Mutex 一次。
使用实例:
static void Main(string[] args)
{
Thread[] thread = new Thread[3];
for (int i = 0; i < 3; i++)
{
thread[i] = new Thread(ThreadMethod1);
thread[i].Name = i.ToString();
}
for (int i = 0; i < 3; i++)
{
thread[i].Start();
}
Console.ReadKey();
}
public static void ThreadMethod1(object val)
{
mutet.WaitOne(); //获取锁
for (int i = 0; i < 500; i++)
{
Console.Write(Thread.CurrentThread.Name);
}
mutet.ReleaseMutex(); //释放锁
}
2、线程池
上面介绍了介绍了平时用到的大多数的多线程的例子,但在实际开发中使用的线程往往是大量的和更为复杂的,这时,每次都创建线程、启动线程。从性能上来讲,这样做并不理想(因为每使用一个线程就要创建一个,需要占用系统开销);从操作上来讲,每次都要启动,比较麻烦。为此引入的线程池的概念。
好处:
1.减少在创建和销毁线程上所花的时间以及系统资源的开销
2.如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存以及”过度切换”。
在什么情况下使用线程池?
1.单个任务处理的时间比较短
2.需要处理的任务的数量大
线程池最多管理线程数量=“处理器数 * 250”。也就是说,如果您的机器为2个2核CPU,那么CLR线程池的容量默认上限便是1000
通过线程池创建的线程默认为后台线程,优先级默认为Normal。
代码示例:
static void Main(string[] args)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadMethod1), new object()); //参数可选
Console.ReadKey();
}
public static void ThreadMethod1(object val)
{
for (int i = 0; i <= 500000000; i++)
{
if (i % 1000000 == 0)
{
Console.Write(Thread.CurrentThread.Name);
}
}
}