240 likes | 439 Views
第 9 章 多线程. 王德俊 上海交通大学继续教育学院. 第 9 章 多线程. 9.1 线程及其实现方法 9.2 线程的同步控制. 9.2 线程的同步控制. 9.2.1 为什么要同步控制 9.2.2 使用 ManualResetEvent 类 9.2.3 使用 AutoResetEvent 类. 9.2.1 为什么要同步控制. 线程之间由于共享资源而产生相互之间需要互相等待、互通消息 才能正确地完成任务,此时就 需要相应的同步控制机制 来支持线程的合作关系。. 9.2 线程的同步控制. 9.2.1 为什么要同步控制.
E N D
第9章 多线程 王德俊 上海交通大学继续教育学院
第9章多线程 9.1 线程及其实现方法 9.2 线程的同步控制
9.2 线程的同步控制 9.2.1 为什么要同步控制 9.2.2 使用ManualResetEvent类 9.2.3 使用AutoResetEvent类
9.2.1 为什么要同步控制 • 线程之间由于共享资源而产生相互之间需要互相等待、互通消息才能正确地完成任务,此时就需要相应的同步控制机制来支持线程的合作关系。
9.2 线程的同步控制 9.2.1 为什么要同步控制 【例9.1】存在同步访问问题的多线程程序。如下:创建控制台应用程序BankTransfering,简单地模拟银行用户进行转帐和取款的程序: class Bank { private double account1 = 2500; private double account2 = 1000; public void transfering() //转帐 { Console.WriteLine("转帐前帐户account1还剩余的金额:" + account1.ToString()); Console.Write("转帐金额(元):"); double sum = double.Parse(Console.ReadLine()); //输入转帐金额 if (sum > account1) { Console.WriteLine("转帐金额超出了帐户account1所剩的金额,"+"转帐失败!"); return; } account1 = account1 - sum; account2 = account2 + sum; Console.WriteLine("转帐后帐户account1还剩余的金额:" + account1.ToString()); }
9.2 线程的同步控制 9.2.1 为什么要同步控制 public void fetching() //取款 { Thread.Sleep(100); account1 = account1 - 2000; //取款2000元 } } static void Main(string[] args) { Bank a = new Bank(); Thread user1 = new Thread(new ThreadStart(a.transfering)); Thread user2 = new Thread(new ThreadStart(a.fetching)); user1.Start(); user2.Start(); Console.ReadKey(); }
9.2 线程的同步控制 9.2.1 为什么要同步控制 • 程序运行结果如图: • 分析:user1查询帐户account1时,显示了还剩2500元的信息,但在执行从account1向account2转2000元时,却出现了操作失败的提示。 • 其原因:恰好在user1等待接收从键盘输入的转帐金额时,user2从帐户account1上提走了2000元。 • 结论:需要同步控制
9.2 线程的同步控制 9.2.2 使用ManualResetEvent类 • ManualResetEvent类的作用是:通知一个或多个正在等待的线程已发生事件。 • ManualResetEvent类对象有两种状态:有信号状态和无信号状态。状态常通过两种方法设置: • 一种是使用构造函数, 例如: ManualResetEvent mre = newManualResetEvent( false); //初始化mre为无信号状态 ManualResetEvent mre = newManualResetEvent(true); //初始化mre为有信号状态 • 另一种是使用对象方法, 例如: mre.Reset(); //使mre处于无信号状态 mre.Set(); //使mre处于有信号状态 调用 Set 方法将使等待句柄一直保持终止状态,允许一个或多个等待线程继续,直到 Reset方法被调用。
9.2 线程的同步控制 9.2.2 使用ManualResetEvent类 • 当ManualResetEvent类对象处于无信号状态时,调用该对象WaitOne()方法的线程将被阻止运行(暂停); • 当该对象变为处于有信号状态时, WaitOne()方法收到信号, WaitOne()方法将解除该线程的暂停状态,使它继续运行。 • 实现多线程的同步控制方法是: • 将被视为一体的语句序列置于Reset()和Set()方法之间(称为“加锁”) • 需要与它们同步的线程,在读取共享变量前先调用WaitOne()方法;用于检测ManualResetEvent类对象有无信号: • 无信号,则该线程被暂停 • 有信号,该线程才能继续执行,从而实现线程的同步控制。
9.2 线程的同步控制 9.2.2 使用ManualResetEvent类 1. 单线程的加锁 2. 多线程的加锁
1. 单线程的加锁 • 对程序BankTransfering出现问题的解决办法: • 通过创建ManualResetEvent类的对象mre,并适当地添加mre.Reset()和mre.Set() 添加同步控制 • 代码修改如下(红色部分): • class Bank • { • private double account1 = 2500; • private double account2 = 1000; • //创建ManualResetEvent类的对象mre • public ManualResetEvent mre = new ManualResetEvent(false);
public void transfering() //转帐 { mre.Reset(); //设置对象mre处于无信号状态 Console.WriteLine("转帐 前 帐户account1还剩余的金额:" + account1.ToString()); Console.Write("转帐金额(元):"); double sum = double.Parse(Console.ReadLine()); if (sum > account1) { Console.WriteLine("转帐金额超出了帐户account1所剩的金额," +"转帐失败!"); return; }
account1 = account1 - sum; account2 = account2 + sum; Console.WriteLine("转帐 后 帐户account1还剩余的金额:" +account1.ToString()); mre.Set(); //设置对象mre处于有信号状态 } public void fetching() //取款 { //阻止当前线程(线程user2)的运行,直到收到对象mre发的信息 mre.WaitOne(); Thread.Sleep(100); account1 = account1 - 2000; } }
2. 多线程的加锁 • 问题: • 一个ManualResetEvent类对象只能对一个线程中的语句序列进行加锁; • 如果需要对多个线程中的语句序列进行加锁,就需要创建与线程数量一样多的ManualResetEvent类对象。
2. 多线程的加锁 【例9.2】下列是控制台应用程序BankTransfering2中文件Program.cs的代码,它仍然模拟银行转帐、查账的功能,但对代码进行了简化(该程序需要解决多线程的同步控制问题): class Bank { private double account1 = 2500; private double account2 = 1000; public void transfering() //将100元从帐户account1转到帐户account2 { account1 = account1 - 100; Thread.Sleep(100); account2 = account2 + 100; }
2. 多线程的加锁 public void transfering2() //将300元从帐户account2转到帐户account1 { account1 = account1 + 300; Thread.Sleep(200); account2 = account2 - 300; } public void querying() //查询帐户account1和account2上的余额 { Console.WriteLine("帐户account1上的余额为:{0} 元", account1); Console.WriteLine("帐户account2上的余额为:{0} 元", account2); } }
2. 多线程的加锁 static void Main(string[] args) { Bank a = new Bank(); Thread user1 = new Thread(newThreadStart(a.transfering)); //转帐用户1 Thread user2 = new Thread(newThreadStart(a.transfering2)); //转帐用户2 Thread user3 = new Thread(newThreadStart(a.querying)); //查账用户 user1.Start(); //执行转帐(account1到account2) user2.Start(); //执行转帐(account2到account1) user3.Start(); //查账用户 Console.ReadKey(); }
2. 多线程的加锁 • 实现对两个线程中的语句进行加锁: • (1)先创建一个包含两个ManualResetEvent类对象的ManualResetEvent数组mres:ManualResetEvent[] mres = { new ManualResetEvent(false), • new ManualResetEvent(false) }; • (2)用数组mres中的两个对象分别对方法transfering()和方法transfering2()中的代码进行加锁; • (3)在方法querying()中查询语句之前调用WaitHandle.WaitAll()方法,该方法的参数类型是ManualResetEvent数组,其作用是:当数组中所有的对象都接收到信号后才允许方法querying()继续执行。
2. 多线程的加锁 修改后的代码: class Bank { private double account1 = 2500; private double account2 = 1000; ManualResetEvent[] mres = { new ManualResetEvent(false), new ManualResetEvent(false) }; //创建包含两个ManualResetEvent类对象的数组 public void transfering() //将100元从帐户account1转到帐户account2 { mres[0].Reset(); account1 = account1 - 100; Thread.Sleep(100); account2 = account2 + 100; mres[0].Set(); }
2. 多线程的加锁 public void transfering2() //将300元从帐户account2转到帐户account1 { mres[1].Reset(); account1 = account1 + 300; Thread.Sleep(200); account2 = account2 - 300; mres[1].Set(); } public void querying() //查询帐户account1和account2上的余额 { WaitHandle.WaitAll(mres); Console.WriteLine("帐户account1上的余额为:{0} 元", account1); Console.WriteLine("帐户account2上的余额为:{0} 元", account2); } }
9.2 线程的同步控制 9.2.3 使用AutoResetEvent类 • 分析: • ManualResetEvent的缺点:当进行多线程的同步控制时,创建的ManualResetEvent类对象的数量要与线程的个数相同,这使程序代码显得比较累赘。 • AutoResetEvent的特点:只需要创建一个AutoResetEvent对象,就可以完成对多个线程的同步控制。 • AutoResetEvent的Set()方法发出“一条”信号,就 “消掉”一个WaitOne()方法; • 如果还有其他WaitOne()方法在等待信号,那么AutoResetEvent对象会自动变为无信号状态(如果没有就不改变其状态),直到再次执行一个Set()方法才能“消掉”下一个WaitOne()方法。
9.2 线程的同步控制 9.2.3 使用AutoResetEvent类 【例9.3】修改程序BankTransfering2(见例9.2),使用AutoResetEvent类实现对其所涉及线程的同步控制。 class Bank { private double account1 = 2500; private double account2 = 1000; AutoResetEvent are = newAutoResetEvent(false); public void transfering() //将100元从帐户account1转到帐户account2 { are.Reset(); account1 = account1 - 100; Thread.Sleep(100); account2 = account2 + 100; are.Set(); }
9.2 线程的同步控制 9.2.3 使用AutoResetEvent类 public void transfering2() //将300元从帐户account2转到帐户account1 { are.Reset(); account1 = account1 + 300; Thread.Sleep(200); account2 = account2 - 300; are.Set(); } public void querying() //查询帐户account1和account2上的余额 { are.WaitOne(); are.WaitOne(); Console.WriteLine("帐户account1上的余额为:{0} 元", account1); Console.WriteLine("帐户account2上的余额为:{0} 元", account2); } } 注意:有多少个Set()方法就应该多少个WaitOne()方法与之对应,否则会出现无限等待或其他问题。
本讲小结 为什么要同步控制 使用ManualResetEvent类 使用AutoResetEvent类