之前在博客園看到有位仁兄發表一篇關于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(),執行完后,主線程接著下步任務處理。