第9章 多线程 王德俊 上海交通大学继续教育学院
第9章 多线程 9.1 线程及其实现方法 9.2 线程的同步控制 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();
其原因:恰好在user1等待接收从键盘输入的转帐金额时,user2从帐户account1上提走了2000元。 结论:需要同步控制 9.2 线程的同步控制 9.2.1 为什么要同步控制 程序运行结果如图: 分析:user1查询帐户account1时,显示了还剩2500元的信息,但在执行从account1向account2转2000元时,却出现了操作失败的提示。 其原因:恰好在user1等待接收从键盘输入的转帐金额时,user2从帐户account1上提走了2000元。 结论:需要同步控制
ManualResetEvent类的作用是:通知一个或多个正在等待的线程已发生事件。 9.2 线程的同步控制 9.2.2 使用ManualResetEvent类 ManualResetEvent类的作用是:通知一个或多个正在等待的线程已发生事件。 ManualResetEvent类对象有两种状态:有信号状态和无信号状态。状态常通过两种方法设置: 一种是使用构造函数, 例如: ManualResetEvent mre = new ManualResetEvent( false); //初始化mre为无信号状态 ManualResetEvent mre = new ManualResetEvent(true); //初始化mre为有信号状态 另一种是使用对象方法, 例如: mre.Reset(); //使mre处于无信号状态 mre.Set(); //使mre处于有信号状态 调用 Set 方法将使等待句柄一直保持终止状态,允许一个或多个等待线程继续,直到 Reset 方法被调用。
当ManualResetEvent类对象处于无信号状态时,调用该对象WaitOne()方法的线程将被阻止运行(暂停); 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; Console.WriteLine("转帐 后 帐户account1还剩余的金额:" +account1.ToString()); mre.Set(); //设置对象mre处于有信号状态 } public void fetching() //取款 { //阻止当前线程(线程user2)的运行,直到收到对象mre发的信息 mre.WaitOne(); Thread.Sleep(100); account1 = account1 - 2000;
2. 多线程的加锁 问题: 一个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 { 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(new ThreadStart(a.transfering)); //转帐用户1 Thread user2 = new Thread(new ThreadStart(a.transfering2)); //转帐用户2 Thread user3 = new Thread(new ThreadStart(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; 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.3 使用AutoResetEvent类 9.2 线程的同步控制 9.2.3 使用AutoResetEvent类 分析: ManualResetEvent的缺点:当进行多线程的同步控制时,创建的ManualResetEvent类对象的数量要与线程的个数相同,这使程序代码显得比较累赘。 AutoResetEvent的特点:只需要创建一个AutoResetEvent对象,就可以完成对多个线程的同步控制。 AutoResetEvent的Set()方法发出“一条”信号,就 “消掉”一个WaitOne()方法; 如果还有其他WaitOne()方法在等待信号,那么AutoResetEvent对象会自动变为无信号状态(如果没有就不改变其状态),直到再次执行一个Set()方法才能“消掉”下一个WaitOne()方法。
【例9.3】 修改程序BankTransfering2(见例9.2),使用AutoResetEvent类实现对其所涉及线程的同步控制。 9.2 线程的同步控制 9.2.3 使用AutoResetEvent类 【例9.3】 修改程序BankTransfering2(见例9.2),使用AutoResetEvent类实现对其所涉及线程的同步控制。 class Bank { private double account1 = 2500; private double account2 = 1000; AutoResetEvent are = new AutoResetEvent(false); public void transfering() //将100元从帐户account1转到帐户account2 are.Reset(); account1 = account1 - 100; Thread.Sleep(100); account2 = account2 + 100; are.Set(); }
public void transfering2() //将300元从帐户account2转到帐户account1 { 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(); Console.WriteLine("帐户account1上的余额为:{0} 元", account1); Console.WriteLine("帐户account2上的余额为:{0} 元", account2); 注意:有多少个Set()方法就应该多少个WaitOne()方法与之对应,否则会出现无限等待或其他问题。
本讲小结 为什么要同步控制 使用ManualResetEvent类 使用AutoResetEvent类