C#委托BeginInvoke返回值亂序問題


  這幾天都有事,一直沒更新博客,有個內容我早就想好了,可是也沒空來寫。

  在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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM