今天在编写一个windows应用程序的时候碰到了一个小问题,程序需求是这样的,创建多个线程调用执行某个方法,Windows Form中有个Progress Bar控件用于显示已经执行完毕的进程数,即当所有的线程都运行完毕后,Progress Bar的进度也到头了。先给出初步的实现方式:
2 private int n = 0 , count = 0 ; // 实际线程数、已结束的线程数
3
4 private void StartTest()
5 {
6 n = int .Parse(txtThreadCount.Text); // 线程数
7 progressBar1.Maximum = n * 10 ; // 设置ProgressBar的最大值
8
9 Thread thread = null ;
10 List < Thread > threads = new List < Thread > (MAXTHREAD);
11 ReadTableTest t = new ReadTableTest(tableName, fileds);
12 t.ThreadExitEvent += new ThreadExit(OnThreadExit); // 线程执行完毕后通知主界面
13
14 try
15 {
16 // 创建线程
17 for ( int i = 0 ; i < n; i ++ )
18 {
19 thread = new Thread( new ThreadStart(t.Run));
20 threads.Add(thread);
21 }
22
23 // 开始调用接口
24 foreach (Thread tt in threads)
25 {
26 tt.Start();
27 }
28 }
29 catch (Exception ex)
30 {
31 MessageBox.Show(ex.Message);
32 }
33 }
34
35 // 线程执行完毕后回调
36 public void OnThreadExit()
37 {
38 count ++ ;
39 this .progressBar1.Value = count * 10 ; // 设置ProgressBar的进度
40
41 // 判断是否全部进程已结束
42 if (count == n)
43 {
44 MessageBox.Show( " 所有线程已执行完毕! " );
45 ClearState();
46 }
47 }
当调试执行这段程序时在this.progressBar1.Value = count * 10;处,报出了InvalidOperationException Cross-thread operation not valid异常,在网上搜索一番后,发现产生该问题的原因如下。
问题原因
由于 Windows 窗体控件本质上不是线程安全的。因此如果有两个或多个线程适度操作某一控件的状态(set value),则可能会迫使该控件进入一种不一致的状态。还可能出现其他与线程相关的 bug,包括争用和死锁的情况。于是在调试器中运行应用程序时,如果创建某控件的线程之外的其他线程试图调用该控件,则调试器会引发一个 InvalidOperationException
解决方案
对于该异常的解决方案有两种,一种是关闭该异常检测的方式来避免异常的出现,经过测试发现此种方法虽然避免了异常的抛出,但是并不能保证程序运行结果的正确性,对于此例来说,经常是全部线程结束时,进度条的显示还未到头呢。下面再来看看更加优雅的解决方案,即通过保证线程的安全性来避免该异常,先来看看两种方案的代码。
方案1
2 {
3 InitializeComponent();
4 Control.CheckForIllegalCrossThreadCalls = false ;
5 }
说明
关闭CheckForIllegalCrossThreadCalls,这是Control class上的一个static property,默认值为flase,目的在于开关是否对Handle的可能存在的不一致存取的监测;且该项设置是具有Application scope的。
方案2
2 private delegate void SafeSetProgressBarValue( int v);
3
4 // 线程执行完毕后回调
5 public void OnThreadExit()
6 {
7 count ++ ;
8 OnSafeSetValue(count * 10 ); //使用线程安全的代码 设置ProgressBar的进度
9
10 // 判断是否全部进程已结束
11 if (count == n)
12 {
13 MessageBox.Show( " 所有线程已执行完毕! " );
14 ClearState();
15 }
16 }
17
18 /// <summary>
19 /// 线程安全的修改ProgressBarValue方式。
20 /// </summary>
21 /// <param name="va"></param>
22 private void OnSafeSetValue( int va)
23 {
24
25 if ( this .progressBar1.InvokeRequired)
26 {
27 SafeSetProgressBarValue call = delegate ( int v) { this .progressBar1.Value = v; };
28
29 this .progressBar1.Invoke(call,va);
30 }
31 else
32 this .progressBar1.Value = va;
33 }
说明
Windows 窗体中的控件被绑定到特定的线程,不具备线程安全性。因此,如果从另一个线程调用控件的方法,那么必须使用控件的一个 Invoke 方法来将调用封送到适当的线程。该属性可用于确定是否必须调用 Invoke 方法,当不知道什么线程拥有控件时这很有用。控件上有四种方法可以安全地从任何线程进行调用:Invoke、BeginInvoke、EndInvoke 和 CreateGraphics。对于所有其他方法调用,当从另一个线程进行调用时,应使用这些 Invoke 方法之一。
Control.InvokeRequired 属性
获取一个值,该值指示调用方在对控件进行方法调用时是否必须调用 Invoke 方法,因为调用方位于创建控件所在的线程以外的线程中。
属性值
如果控件的 Handle 是在与调用线程不同的线程上创建的(说明您必须通过 Invoke 方法对控件进行调用),则为 true;否则为 false。
更多资料:
http://msdn2.microsoft.com/zh-cn/library/ms171728(VS.80).aspx
http://msdn2.microsoft.com/zh-cn/library/system.windows.forms.control.invokerequired(VS.80).aspx