C#委托BeginInvoke返回值乱序问题(转)
来源:互联网 发布:触摸查询软件 编辑:程序博客网 时间:2024/06/06 19:57
在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 Student13 {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 : Window24 {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 else13 {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比较好。
- C#委托BeginInvoke返回值乱序问题(转)
- [转]C#试写一个多线程问题(委托,Invoke(),beginInvoke())
- BeginInvoke与EndInvoke方法解决多线程接收委托返回值问题
- BeginInvoke与EndInvoke方法解决多线程接收委托返回值问题
- C#委托的BeginInvoke和EndInvoke方法
- C# 用委托BeginInvoke做异步线程
- C# 调用委托线程BeginInvoke与EndInvoke
- C#中跨线程访问控件: 委托, Invoke, BeginInvoke
- C# BeginInvoke
- 委托,begininvoke,endinvoke
- 黄聪:C#多线程教程(1):BeginInvoke和EndInvoke方法,解决主线程延时Thread.sleep柱塞问题(转)
- C# 多线程 用委托实现异步_调用委托的BeginInvoke和EndInvoke方法
- C# 多线程 用委托实现异步_调用委托的BeginInvoke和EndInvoke方法
- 委托的BeginInvoke和EndInvoke方法(多线程)
- 异步委托---使用BeginInvoke,EndInvoke
- 委托中的invoke和begininvoke
- Invoke and BeginInvoke BeginInvoke和EndInvoke方法 (转)
- Invoke and BeginInvoke BeginInvoke和EndInvoke方法 (转)
- SurfaceFlinger详解
- OpenRisc-57-ORPSoC仿真环境的构建
- c++ string 用法详解
- Sicily 1135 飞越原野
- db2start: error while loading shared libraries: libnuma.so.1: cannot open shared object file: No suc
- C#委托BeginInvoke返回值乱序问题(转)
- vs2010 调试程序加载符号慢
- linux内核编译错误
- 编程小技巧
- sprintf用法
- 八 redis学习笔记之主从复制
- ExtJs4 笔记(7) Ext.tip.ToolTip 提示
- python_打印pstree效果
- Exception之循环是否继续?