之前在博客园看到有位仁兄发表一篇关于AutoResetEvent介绍,看了下他写的代码,看上去没什么问题,但仔细看还是能发现问题。下图是这位仁兄代码截图。
仁兄博客地址:http://www.cnblogs.com/lzjsky/archive/2011/07/11/2102794.html
按照这种写法自己试了下,运行起来并不是他这种结果(运行结果很随机)。
原因有以下两点:
1、支付线程与取书线程都属于同级线程,运行先后顺序是随机的
2、在循环内部调用AutoResetEvent.Set(),不能确定子线程是否按顺序执行,有可能主线程已经循环多次,而子线程可能才循环一次
修正
首先,要明白实验的场景。还是引用这位仁兄的例子:“我去书店买书,当我选中一本书后我会去收费处付钱,付好钱后再去仓库取书。这个顺序不能颠倒,我作为主线程,收费处和仓库做两个辅助线程” 。
要实现上图这种效果,得先确定好执行先后顺序(上面已经说过):挑书-->收费-->取书-->完成。
代码编写如下:
1 class Program 2 { 3 static int _num = 0; 4 //本例重点对象 5 static AutoResetEvent _autoReset = new AutoResetEvent(false); 6 7 static AutoResetEvent _autoReset0 = new AutoResetEvent(false); 8 static AutoResetEvent _autoReset1 = new AutoResetEvent(false); 9 10 //static AutoResetEvent autoReset2 = new AutoResetEvent(false); 11 //static AutoResetEvent autoReset3 = new AutoResetEvent(false); 12 13 //static object _payMoneyObj = new object(); 14 //static object _getBookObj = new object(); 15 16 private static void ThreadPayMoneyProc() 17 { 18 while (true) 19 { 20 //_autoReset.WaitOne(); 21 _autoReset0.WaitOne(); 22 //lock (_payMoneyObj) 23 { 24 Console.WriteLine(Thread.CurrentThread.Name + ",编号: " + _num); 25 //通知主线程,钱已付完 26 //_autoReset2.Set(); 27 } 28 } 29 } 30 31 private static void TreadGetBookProc() 32 { 33 while (true) 34 { 35 //_autoReset.WaitOne(); 36 _autoReset1.WaitOne(); 37 //lock (_getBookObj) 38 { 39 Console.WriteLine(Thread.CurrentThread.Name + ",编号: " + _num); 40 //通知主线程,书已取走 41 //_autoReset3.Set(); 42 } 43 } 44 } 45 46 47 static void Main(string[] args) 48 { 49 //本案例是通过AutoResetEvent来实现多线程同步 50 //购买书数量 51 const int num = 50; 52 53 //付钱线程 54 Thread threadPayMoney = new Thread(new ThreadStart(ThreadPayMoneyProc)); 55 threadPayMoney.Name = "付钱线程"; 56 //取书线程 57 Thread threadGetBook = new Thread(new ThreadStart(TreadGetBookProc)); 58 threadGetBook.Name = "取书线程"; 59 60 //开始执行线程 61 threadPayMoney.Start(); 62 threadGetBook.Start(); 63 64 //主线程开始选书 65 Console.WriteLine("----------------主线程开始选书!------------------"); 66 for (int i = 1; i <= num; i++) 67 { 68 Console.WriteLine("主线程选书编号:" + i); 69 _num = i; 70 //_autoReset.Set(); 71 72 //通知付钱线程 73 _autoReset0.Set(); 74 //主线延时1ms执行(但不知道付钱线程这个过程需要多少时间) 75 Thread.Sleep(1); 76 //_autoReset2.WaitOne(); 77 78 //付完钱后,通知取书线程 79 _autoReset1.Set(); 80 //主线延时1ms执行(但不知道取书线程这个过程需要多少时间) 81 Thread.Sleep(1); 82 //_autoReset3.WaitOne(); 83 Console.WriteLine("-----------------------------------"); 84 } 85 86 Console.ReadKey(); 87 88 89 } 90 }
运行结果如下图:
这样做,效果是出来了,但主线程不知道付费线程、取书线程执行需要多长时间。上例中给定的是1ms,但如果其中某个子线程超过了给定的休眠时间,主线会继续往下执行,不会等待子线程处理完成。这样就导致了买书编号与付钱和取书的编号不同步。也就混乱了。
这时可以使用AutoResetEvent这个对象。上例中已经使用这个对象。没错,还可以在继续使用。
代码如下图:
1 class Program 2 { 3 static int _num = 0; 4 //本例重点对象 5 static AutoResetEvent _autoReset = new AutoResetEvent(false); 6 7 static AutoResetEvent _autoReset0 = new AutoResetEvent(false); 8 static AutoResetEvent _autoReset1 = new AutoResetEvent(false); 9 10 static AutoResetEvent _autoReset2 = new AutoResetEvent(false); 11 static AutoResetEvent _autoReset3 = new AutoResetEvent(false); 12 13 //static object _payMoneyObj = new object(); 14 //static object _getBookObj = new object(); 15 16 private static void ThreadPayMoneyProc() 17 { 18 while (true) 19 { 20 //_autoReset.WaitOne(); 21 _autoReset0.WaitOne(); 22 //lock (_payMoneyObj) 23 { 24 Console.WriteLine(Thread.CurrentThread.Name + ",编号: " + _num); 25 //通知主线程,钱已付完成 26 _autoReset2.Set(); 27 } 28 } 29 } 30 31 private static void TreadGetBookProc() 32 { 33 while (true) 34 { 35 //_autoReset.WaitOne(); 36 _autoReset1.WaitOne(); 37 //lock (_getBookObj) 38 { 39 Console.WriteLine(Thread.CurrentThread.Name + ",编号: " + _num); 40 //通知主线程,书已取走 41 _autoReset3.Set(); 42 } 43 } 44 } 45 46 47 static void Main(string[] args) 48 { 49 //本案例是通过AutoResetEvent来实现多线程同步 50 //购买书数量 51 const int num = 5; 52 53 //付钱线程 54 Thread threadPayMoney = new Thread(new ThreadStart(ThreadPayMoneyProc)); 55 threadPayMoney.Name = "付钱线程"; 56 //取书线程 57 Thread threadGetBook = new Thread(new ThreadStart(TreadGetBookProc)); 58 threadGetBook.Name = "取书线程"; 59 60 //开始执行线程 61 threadPayMoney.Start(); 62 threadGetBook.Start(); 63 64 //主线程开始选书 65 Console.WriteLine("----------------主线程开始选书!------------------"); 66 for (int i = 1; i <= num; i++) 67 { 68 Console.WriteLine("主线程选书编号:" + i); 69 _num = i; 70 //_autoReset.Set(); 71 72 //通知付钱线程 73 _autoReset0.Set(); 74 //主线延时1ms执行(但不知道付钱线程这个过程需要多少时间) 75 //Thread.Sleep(1); 76 //等待付钱线程 77 _autoReset2.WaitOne(); 78 79 //付完钱后,通知取书线程 80 _autoReset1.Set(); 81 //主线延时1ms执行(但不知道取书线程这个过程需要多少时间) 82 //Thread.Sleep(1); 83 //等待取书线程 84 _autoReset3.WaitOne(); 85 Console.WriteLine("-----------------------------------"); 86 //完成后,继续下一个任务处理 87 } 88 89 Console.ReadKey(); 90 91 92 } 93 }
运行结果如下图:
运行结果和上面使用指定主线程休眠所运行结果是一样的。但是,可以不用指定主线程休眠时间,也不需要指定。因为你没法估计子线程所运行的时间,而且每次运行时间都不一样。
后话
本例中, 买书场景其实有两种编程结构(或者编程思想)。一种是本例中的,买书是主线程,而收银台(付钱线程)、仓库(取书线程)。这两个线程是一直存在的,一直跑着的。只要有书过来,这两个线程就会执行。这可以联系到现实中的收银台和仓库。
第二种编程思想,买书是一个发起线程,然后开启一个付款线程和取书线程。这时,买书线程(主线程)可以确定这两个子线程什么时候执行完成。使用 线程对象.Join(),执行完后,主线程接着下步任务处理。