您的位置:首页 > 编程语言

多线程编程(2):线程的同步 .

2012-06-05 13:05 267 查看
多线程编程(2):线程的同步

分类:
C#基础 2010-01-10 20:18
5463人阅读
评论(34)
收藏
举报

在《多线程编程》系列第一篇讲述了如何启动线程,这篇讲述线程之间存在竞争时如何确保同步并且不发生死锁。

线程不同步引出的问题

下面做一个假设,假设有100张票,由两个线程来实现一个售票程序,每次线程运行时首先检查是否还有票未售出,如果有就按照票号从小到大的顺序售出票号最小的票,程序的代码如下:

using System;
using System.Collections.Generic;
using System.Text;
//需要添加对System.EnterpriseServices.dll这个类库的引用采用使用这个dll
using System.EnterpriseServices;

namespace StartThread
{
[Synchronization(SynchronizationOption.Required)]//确保创建的对象已经同步
public class SynchronizationAttributeClass
{
public void Run()
{
}
}
}


所有在同一个上下文域的对象共享同一个锁。这样创建的对象实例属性、方法和字段就具有线程安全性,需要注意的是类的静态字段、属性和方法是不具有线程安全性的。

同步代码区

同步代码区是另外一种策略,它是针对特定部分代码进行同步的一种方法。

lock同步

针对上面的代码,要实现不会出现混乱(两次卖出同一张票或者有些票根本就没有卖出),可以lock关键字来实现,出现问题的部分就是在于判断剩余票数是否大于0,如果大于0则从当前总票数中减去最大的一张票,因此可以对这部分进行处理,代码如下:

private void Run()
{
while (ticketList.Count > 0)//①
{

Monitor.Enter(objLock);
if (ticketList.Count > 0)
{
string ticketNo = ticketList[0];//②
Console.WriteLine("{0}:售出一张票,票号:{1}", Thread.CurrentThread.Name, ticketNo);
ticketList.RemoveAt(0);//③
Thread.Sleep(1);
}
Monitor.Exit(objLock);
}
}


当然这段代码最终运行的效果也和使用lock关键字来同步的效果一样。比较之下,大家会发现使用lock关键字来保持同步的差别不大:”lock (objLock){“被换成了”Monitor.Enter(objLock);”,”}”被换成了” Monitor.Exit(objLock);”。实际上如果你通过其它方式查看最终生成的IL代码,你会发现使用lock关键字的代码实际上是用Monitor来实现的。

如下代码:

try{
Monitor.Enter(objLock);
//同步代码
}
finally
{
Monitor.Exit(objLock);
}


我们知道在绝大多数情况下finally中的代码块一定会被执行,这样确保了即使同步代码出现了异常也仍能释放同步锁。

Monitor类出了Enter()和Exit()方法之外,还有Wait()和Pulse()方法。Wait()方法是临时释放当前活得的锁,并使当前对象处于阻塞状态,Pulse()方法是通知处于等待状态的对象可以准备就绪了,它一会就会释放锁。下面我们利用这两个方法来完成一个协同的线程,一个线程负责随机产生数据,一个线程负责将生成的数据显示出来。下面是代码:

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace StartThread
{
public class ThreadWaitAndPluse
{
private object lockObject;
private int number;
private Random random;
public ThreadWaitAndPluse()
{
lockObject = new object();
random = new Random();
}
//显示生成数据的线程要执行的方法
public void ThreadMethodOne()
{
Monitor.Enter(lockObject);//获取对象锁
Console.WriteLine("当前进入的线程:" + Thread.CurrentThread.GetHashCode());
for (int i = 0; i < 5; i++)
{
Monitor.Wait(lockObject);//释放对象锁,并阻止当前线程
Console.WriteLine("WaitAndPluse1:工作");
Console.WriteLine("WaitAndPluse1:得到了数据,number=" + number + ",Thread ID=" + Thread.CurrentThread.GetHashCode());
//通知其它等待锁的对象状态已经发生改变,当这个对象释放锁之后等待锁的对象将会活得锁
Monitor.Pulse(lockObject);
}
Console.WriteLine("退出当前线程:" + Thread.CurrentThread.GetHashCode());
Monitor.Exit(lockObject);//释放对象锁
}
//生成随机数据线程要执行的方法
public void ThreadMethodTwo()
{
Monitor.Enter(lockObject);//获取对象锁
Console.WriteLine("当前进入的线程:" + Thread.CurrentThread.GetHashCode());
for (int i = 0; i < 5; i++)
{
//通知其它等待锁的对象状态已经发生改变,当这个对象释放锁之后等待锁的对象将会活得锁
Monitor.Pulse(lockObject);
Console.WriteLine("WaitAndPluse2:工作");
number =random.Next(DateTime.Now.Millisecond);//生成随机数
Console.WriteLine("WaitAndPluse2:生成了数据,number=" + number + ",Thread ID=" + Thread.CurrentThread.GetHashCode());
Monitor.Wait(lockObject);//释放对象锁,并阻止当前线程
}
Console.WriteLine("退出当前线程:" + Thread.CurrentThread.GetHashCode());
Monitor.Exit(lockObject);//释放对象锁
}

public static void Main()
{
ThreadWaitAndPluse demo=new ThreadWaitAndPluse();
Thread t1 = new Thread(new ThreadStart(demo.ThreadMethodOne));
t1.Start();

Thread t2 = new Thread(new ThreadStart(demo.ThreadMethodTwo));
t2.Start();

Console.ReadLine();
}
}
}


程序的执行效果如下:

WaitHandle

WaitHandle类是一个抽线类,有多个类直接或者间接继承自WaitHandle类,类图如下:

在WaitHandle类中SignalAndWait、WaitAll、WaitAny及WaitOne这几个方法都有重载形式,其中除WaitOne之外都是静态的。WaitHandle方法常用作同步对象的基类。WaitHandle对象通知其他的线程它需要对资源排他性的访问,其他的线程必须等待,直到WaitHandle不再使用资源和等待句柄没有被使用。

WaitHandle方法有多个Wait的方法,这些方法的区别如下:

WaitAll:等待指定数组中的所有元素收到信号。

WaitAny:等待指定数组中的任一元素收到信号。

WaitOne:当在派生类中重写时,阻塞当前线程,直到当前的 WaitHandle 收到信号。

这些wait方法阻塞线程直到一个或者更多的同步对象收到信号。

下面的是一个MSDN中的例子,讲的是一个计算过程,最终的计算结果为第一项+第二项+第三项,在计算第一、二、三项时需要使用基数来进行计算。在代码中使用了线程池也就是ThreadPool来操作,这里面涉及到计算的顺序的先后问题,通过WaitHandle及其子类可以很好地解决这个问题。

代码如下:

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace StartThread
{
//下面的代码摘自MSDN,笔者做了中文代码注释
//周公
public class EventWaitHandleDemo
{
double baseNumber, firstTerm, secondTerm, thirdTerm;
AutoResetEvent[] autoEvents;
ManualResetEvent manualEvent;

//产生随机数的类.
Random random;

static void Main()
{
EventWaitHandleDemo ewhd = new EventWaitHandleDemo();
Console.WriteLine("Result = {0}.",
ewhd.Result(234).ToString());
Console.WriteLine("Result = {0}.",
ewhd.Result(55).ToString());
Console.ReadLine();
}
//构造函数
public EventWaitHandleDemo()
{
autoEvents = new AutoResetEvent[]
{
new AutoResetEvent(false),
new AutoResetEvent(false),
new AutoResetEvent(false)
};

manualEvent = new ManualResetEvent(false);
}
//计算基数
void CalculateBase(object stateInfo)
{
baseNumber = random.NextDouble();

//指示基数已经算好.
manualEvent.Set();
}

//计算第一项
void CalculateFirstTerm(object stateInfo)
{
//生成随机数
double preCalc = random.NextDouble();

//等待基数以便计算.
manualEvent.WaitOne();

//通过preCalc和baseNumber计算第一项.
firstTerm = preCalc * baseNumber *random.NextDouble();

//发出信号指示计算完成.
autoEvents[0].Set();
}
//计算第二项
void CalculateSecondTerm(object stateInfo)
{
double preCalc = random.NextDouble();
manualEvent.WaitOne();
secondTerm = preCalc * baseNumber *random.NextDouble();
autoEvents[1].Set();
}
//计算第三项
void CalculateThirdTerm(object stateInfo)
{
double preCalc = random.NextDouble();
manualEvent.WaitOne();
thirdTerm = preCalc * baseNumber *random.NextDouble();
autoEvents[2].Set();
}
//计算结果
public double Result(int seed)
{
random = new Random(seed);

//同时计算
ThreadPool.QueueUserWorkItem(new WaitCallback(CalculateFirstTerm));
ThreadPool.QueueUserWorkItem(new WaitCallback(CalculateSecondTerm));
ThreadPool.QueueUserWorkItem(new WaitCallback(CalculateThirdTerm));
ThreadPool.QueueUserWorkItem(new WaitCallback(CalculateBase));

//等待所有的信号.
WaitHandle.WaitAll(autoEvents);

//重置信号,以便等待下一次计算.
manualEvent.Reset();
//返回计算结果
return firstTerm + secondTerm + thirdTerm;
}
}
}

程序的运行结果如下:

Result = 0.355650523270459.

Result = 0.125205692112756.

当然因为引入了随机数,所以每次计算结果并不相同,这里要讲述的是它们之间的控制。首先在 Result(int seed)方法中讲计算基数、第一项、第二项及第三项的方法放到线程池中,要计算第一二三项时首先要确定基数,这些方法通过manualEvent.WaitOne()暂时停止执行,于是计算基数的方法首先执行,计算出基数之后通过manualEvent.Set()方法通知计算第一二三项的方法开始,在这些方法完成计算之后通过autoEvents数组中的AutoResetEvent元素的Set()方法发出信号,标识执行完毕。这样WaitHandle.WaitAll(autoEvents)这一步可以继续执行,从而得到执行结果。

在上面代码中的WaitHandle的其它子类限于篇幅不在这里一一举例讲解,它们在使用了多少有些相似之处(毕竟是一个爹、从一个抽象类继承下来的嘛)。

图片上传功能暂时关闭,敬请谅解。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: