這幾天都有事,一直沒更新博客,有個內容我早就想好了,可是也沒空來寫。
在WPF中,我們經常要用到BeginInvoke、Invoke來更新前台界面,實際上都是Post一個Message給了UI線程,然后由UI線程來操作界面更新,只不過BeginInvoke是無阻塞異步式的Post,而Invoke是在Post后使用WaitHandle來阻塞了當前線程直到UI線程處理Message后才返回。
現在我遇到的問題是使用委托的BeginInvoke方法來執行多線程的操作時,其返回值是亂序的。一般而言,亂序是很正常的,因為它本身是個異步方法,調用、返回順序本身就是隨機的,可是在一些情況下,這會存在很大的問題,而我們很可能會忽略這個問題。
舉個例子吧,現在有三個同學,要通過一個函數來判斷它們的成績是否及格,然后根據函數的返回值進行輸出。我先把基礎的數據結構解釋一下:

1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Threading; 5 using System.Windows; 6 7 namespace TestInvoke 8 { 9 // 委托定義 10 public delegate bool RankGradeDlg(int score); 11 // 數據項 12 public class Student 13 { 14 public int Score { get; set; } 15 public string Name { get; set; } 16 public Student(int score, string name) 17 { 18 Score = score; 19 Name = name; 20 } 21 } 22 23 public partial class MainWindow : Window 24 { 25 // 成績數組 26 private Student[] _Grades = new Student[] { new Student(-10,"1"), new Student(60,"2"), new Student(110,"3") }; 27 private List<Student> _NotPassed = new List<Student>(); // 不及格名單 28 private RankGradeDlg _RankGradeDlg; // 委托 29 private DateTime _StartTime; // 開始時間 30 31 public MainWindow() 32 { 33 InitializeComponent(); 34 _RankGradeDlg = RankGrade; 35 } 36 37 // 判斷是否及格 38 private bool RankGrade(int score) 39 { 40 Thread.Sleep(500); // 等待0.5秒 41 if (score < 60) 42 { 43 ShowText("不及格(委托內):--- 線程" + Thread.CurrentThread.ManagedThreadId + " --- " + score + '\n'); 44 return false; 45 } 46 ShowText("及 格(委托內):--- 線程" + Thread.CurrentThread.ManagedThreadId + " --- " + score + '\n'); 47 return true; 48 } 49 50 // 顯示數據 51 private void ShowText(string text) 52 { 53 TBX_Result.Dispatcher.Invoke((Action)(() => 54 { 55 TBX_Result.Text += text; 56 TBX_Result.ScrollToEnd(); 57 })); 58 } 59 60 // 顯示不及格名單 61 private void ShowList() 62 { 63 foreach (Student grade in _NotPassed) 64 ShowText("不及格名單 ————> Name: " + grade.Name + " , Score: " + grade.Score + '\n'); 65 } 66 } 67 }
一、好了,步入正題,一般我們會這樣寫這個過程:
1 foreach (Student grade in _Grades) 2 { 3 if (!RankGrade(grade.Score)) 4 _NotPassed.Add(grade); 5 }
這是一個直接調用函數順序執行的過程,它的執行情況如下圖所示:
結果是正確的,花費的時間1.51s正好就是分別調用三次RankGrade函數的時間(一個函數為0.5s多一點),而且執行線程的ID都為“1”。另外,使用Invoke的方式來執行此函數所得的結果也是相同的。上面提到的這兩種方式都會導致調用線程被阻死,要想不阻死當前線程,可以另外開一個線程來執行函數:
1 new Thread(delegate() 2 { 3 foreach (Student grade in _Grades) 5 if (!_RankGradeDlg.Invoke(grade.Score)) 6 _NotPassed.Add(grade); 8 }).Start();
所得的結果仍然是一樣的,但是執行的線程將不會是當前的線程“1”了,而是在另外開辟的新線程上執行三次RankGrade函數。
二、接下來便是本文的關鍵了,使用BeginInvoke來執行線程,也就是說讓三次RankGrade過程異步執行,最后返回結果(及格/不及格):
1 new Thread(delegate() 2 { 3 List<WaitHandle> waitList = new List<WaitHandle>(); 4 foreach (Student grade in _Grades) 5 { 6 waitList.Add(( 7 _RankGradeDlg.BeginInvoke(grade.Score, ar => 8 { 9 if (!_RankGradeDlg.EndInvoke(ar)) // 不及格 10 _NotPassed.Add(grade); 11 }, null) 12 ).AsyncWaitHandle); 13 } 14 WaitHandle.WaitAll(waitList.ToArray()); 15 }).Start();
注意這里的WaitHandle List是用來記錄所有線程執行完的時間的,以完成數據的同步。在BeginInvoke的回調方法中,根據返回值把不及格的學生填入了不及格名單。好了,滿以為大功告成,可是運行一看結果:
這結果不對啊,雖然在函數執行內部的結果是沒有問題的,但是最終得到的名單卻不對,姓名為“3”的同學有“110”分,卻成為了不及格。這到底是怎么了?我們來分析一下,從線程數來看,這里開辟了兩個線程“4”、“5”來完成這三次計算,所以耗時減少到1.03s了,這是預計之內的。然后在函數內部執行時結果也是正解的,不及格的是“-10”分的同學,返回的也是false。那么問題就只可能出在EndInvoke()上,我在MSDN上找到了這樣一段話:
“如果按同一個 DispatcherPriority 調用多個 BeginInvoke,將按調用發生的順序執行它們。”
這只是說它們的進入是有順序的,但是回調呢?我試過其它許多形式,比如把RankGrade函數標識為STAThread,把回調函數單獨寫出來……所得結果都不正確,事實證明,它們的回調是亂序的,C#並不會記錄委托執行的哪一個過程返回給哪一個相應的EndInvoke,於是,“110”分的同學很悲劇地遇上了“-10”分同學的回調結果“false”,最終就被打入了不及格名單。
如果連續多次執行剛才的過程,我們還可以看到如下的結果:
發現區別了沒?時間變得更少了,接近於執行一次RankGrade的過程了。這是因為CLR看到你經常用這東西,就分了三個線程給你用“5”、“8”、“4”,這同時也說明了BeginInvoke是通過線程池來幫助用戶完成異步操作的。
三、怎么解決這個問題呢?我的方法便是使用Thread,傳遞參數過去,然后再讀取參數。這種方法,需要把學生的數據結構改變一下:
1 public class Student 2 { 3 public int Score { get; set; } 4 public string Name { get; set; } 5 public ManualResetEvent AsyncHandle = new ManualResetEvent(false); 6 public bool Result { get; set; } 7 public Student(int score, string name) 8 { 9 Score = score; 10 Name = name; 11 } 12 }
增加了一個同步用的Handle,一個返回用的Result字段。然后把RankGrade函數改為:
1 // Thread使用的處理函數 2 private void RankGrade(object parm) 3 { 4 Student stu = parm as Student; 5 stu.AsyncHandle.Reset(); 6 Thread.Sleep(500); // 等待0.5秒 7 if (stu.Score < 60) 8 { 9 ShowText("不及格(委托內):--- 線程" + Thread.CurrentThread.ManagedThreadId + " --- " + stu.Score + '\n'); 10 stu.Result = false; 11 } 12 else 13 { 14 ShowText("及 格(委托內):--- 線程" + Thread.CurrentThread.ManagedThreadId + " --- " + stu.Score + '\n'); 15 stu.Result = true; 16 } 17 stu.AsyncHandle.Set(); 18 }
通過這種方式,來完成BeginInvoke異步和AsyncWaitHandle功能,調用的時候:
1 new Thread(delegate() 2 { 3 _StartTime = DateTime.Now; 4 foreach (Student grade in _Grades) 5 { 6 new Thread(RankGrade).Start(grade); 7 } 8 WaitHandle.WaitAll(_Grades.Select(c => c.AsyncHandle).ToArray()); 9 _NotPassed.AddRange(_Grades.Where(c => c.Result == false)); 10 }).Start();
所得的結果為:
終於正確了,“-10”分的同學“1”被打入了不及格名單,執行效率也得到了改善。這里我需要指出的是,使用BeginInvoke也是可以的,但是BeginInvoke使用線程池的方式在處理事務非常多、處理時間比較長時,會有排隊機制,這會影響其它線程進駐線程池,所以還是用Thread比較好。
附上源代碼:InvokeTest.rar
轉載請注明原址:http://www.cnblogs.com/lekko/archive/2012/08/01/2618088.html