zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    NOIP知识点&&模板整理【更新中】
    qbxt DAY7 T4
    qbxt DAY7 T2
    qbxt DAY 6 T3 柯西不等式和拉格朗日不等式
    qbxt DAY4 T4
    qbxt DAY4 T3
    #98. 表达式计算 杂想
    扫描线入门学习笔记 (主要讲解代码实现)
    学OI要知道的基础知识(咕咕咕)
    主定理学习笔记(总结向)
  • 原文地址:https://www.cnblogs.com/lekko/p/2618088.html
Copyright © 2011-2022 走看看