谈.Net委托与线程——解决窗体假死

本文深入探讨了.NET Framework中Control.Invoke和Control.BeginInvoke的使用方法,以及如何利用Application.DoEvents()函数解决窗体加载大量数据时出现的假死问题。通过实例演示,展示了如何在不阻塞UI线程的情况下高效加载数据,同时避免了界面响应延迟和闪烁现象。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引言

  在之前的 《创建无阻塞的异步调用》中,已经介绍过异步调用的编写步骤和实施原理。异步调用是CLR为开发者提供的一种重要的编程手段,它也是构建高性能、可伸缩应用程序的关键。在多核CPU越来越普及的今天,异步编程允许使用非常少的线程执行很多操作。我们通常使用异步完成许多计算型、IO型的复杂、耗时操作,去取得我们的应用程序运行所需要的一部分数据。在取得这些数据后,我们需要将它们绑定在UI中呈现。当数据量偏大时,我们会发现窗体变成了空白面板。此时如果用鼠标点击,窗体标题将会出现”失去响应”的字样,而实际上UI线程仍在工作着,这对用户来说是一种极度糟糕的体验。如果你希望了解其中的原因(并不复杂:)),并彻底解决该问题,那么花时间读完此文也许是个不错的选择。
  一般来说,窗体阻塞分为两种情况。一种是在UI线程上调用耗时较长的操作,例如访问数据库,这种阻塞是UI线程被占用所导致,可以通过delegate.BeginInvoke的异步编程解决;另一种是窗体加载大批量数据,例如向ListView、DataGridView等控件中添加大量的数据。本文主要探讨后一种阻塞。

基础理论

  这部分简单介绍CLR对跨线程UI访问的处理。作为基础内容,相信大部分.NET开发者对它并不陌生,读者可根据实际情况略过此处。

控件的线程安全检测

  在传统的窗体编程中,UI中的控件元素与其他工作线程互相隔离,每次我们访问一个UI控件,实际上都是在UI线程中进行。如果尝试在其他线程中访问控件,CLR针对不同的.NET Framework版本,会有不同的处理。在Framework1.x中,CLR允许应用程序以跨线程的方式运行,而在Framework2.0及以后版本中,System.Windows.Form.Control新增了CheckForIllegalCrossThreadCalls属性,它是一个可读写的bool常量,标记我们是否需要对非UI线程对控件的调用做出检测。如果指定true,当以其他线程访问UI,CLR会跑出一个”InvalidOperationException:线程间操作无效,从不是创建控件***的线程访问它”;如果为false,则不对该错误线程的调用进行捕获,应用程序依然运行。
  在Framework1.x版本中,这个值默认是false。问什么之后的版本会加入这个属性来约束我们的UI呢?实际上官方对此的解释是当有多个并发线程尝试对UI进行读写时,容易造成线程争用资源带来的死锁。所以,CLR默认不允许以非UI线程访问控件。
  然而,我们常常需要在窗体中使用异步线程来处理一些操作,例如IO和Socket通讯等。这时跨线程的UI访问又是必须的,对此,.NET给我们的补充方案就是Control的Invoke和BeginInvoke。

Control的Invoke和BeginInvoke

对于这两个方法,首先我们要有以下的认识:
  1. Control.Invoke,Control.BeginInvoke和delegate.Invoke,delegate.BeginInvoke是不同的。
  2. Control.Invoke中的委托方法,执行在主线程,也就是我们的UI线程。而Control.BeginInvoke从命名上来看虽然具有异步调用的特征(Begin),但也仍然执行在UI线程。
  3. 如果在UI线程中直接调用Invoke和BeginInvoke,数据量偏大时,依然会造成UI的假死。
  有很多开发者在初次接触这两个函数时,很容易就将它们同异步联系起来、有些人会认为他们是独立于UI线程之外的工作线程,实际上,他们都被这两个函数的命名所蒙蔽了。如果以传统调用异步的方式,直接调用Control.BeginInvoke,与同步函数的执行无异,UI线程还是会处理所有辛苦的操作,造成我们的应用程序阻塞。
  Control.Invoke的调用模型很明确:在UI线程中以代码顺序同步执行,因此,抛开工作线程调用UI元素的干扰,我们可以将Control.Invoke视为同步,本文不做过多介绍。
  很多开发者在接触异步后,再来处理窗体假死的问题,很容易想当然的将Control.BeginInvoke视为WinForm封装的异步。所以我们重点关注这个方法。

体验BeginInvoke

  前面说过,BeginInvoke除了命名上来看像异步,其实很多时候我们调用起来根本没有异步的”非阻塞”特性,我用下面这个例子简单的尝试一次对BeginInvoke的调用。
  如你所见,我现在创建了一个简陋的Form,其中放置了一个Lable控件lable1,一个Button控件btn_Start,下面,开始code:
复制代码
private void btn_Start_Click( object sender, EventArgs e)
{
// 储存UI线程的标识符
int curThreadID = Thread.CurrentThread.ManagedThreadId;

new Thread((ThreadStart) delegate ()
{
PrintThreadLog(curThreadID);
})
.Start();
}

private void PrintThreadLog( int mainThreadID)
{
// 当前线程的标识符
// A代码块
int asyncThreadID = Thread.CurrentThread.ManagedThreadId;

// 输出当前线程的扼要信息,及与UI线程的引用比对结果
// B代码块
label1.BeginInvoke((MethodInvoker) delegate ()
{
// 执行BeginInvoke内的方法的线程标识符
int curThreadID = Thread.CurrentThread.ManagedThreadId;

label1.Text
= string .Format( " Async Thread ID:{0},Current Thread ID:{1},Is UI Thread:{2} " ,
asyncThreadID, curThreadID, curThreadID.Equals(mainThreadID));
});

// 挂起当前线程3秒,模拟耗时操作
// C代码块
Thread.Sleep( 3000 );
}
复制代码
  这段代码在新的线程中访问了UI,所以我们使用了label1.BeginInvoke函数。新的线程中,我们取得了当前工作线程的线程标识符,也取得了BeginInvoke函数内的线程。然后,将它与UI线程的标志符作比对,将结果输出于Label1控件上。最后,我们挂起当前工作线程3秒,用于模拟一些常见的耗时操作。
  为了便于区分,我们将这段代码分为A、B、C三个代码块。
运行结果:
我们能得到以下结论:
●  PrintThreadLog函数主体(A、C代码块)执行在新的线程,它执行了不被BeginInvoke所包含的其他代码。
●  当我们调用了Control.BeginInvoke之后,线程调度权回归到了UI线程。也就是说,BeginInvoke内部的代码(B代码块)均执行在UI线程。
●  在UI线程执行BeginInvok中封装的代码时,工作线程内的剩余代码(C代码块)同时进行。它与BeginInvoke中的UI线程并行执行,互不干扰。
●  由于Thread.Sleep(3000)是隔离在UI线程外的工作线程,因此这行代码带来的线程阻塞实际上阻塞了工作线程,不会给UI带来任何影响。

Control.BeginInvoke的真正含义

  既然Control.BeginInvoke其中的委托函数仍执行在UI线程内,那这个”异步”到底指的是什么?话题回到本文最初:我们在上文已经提到了”控件的线程安全检测”概念,相信大家对这种工作线程内调用Control.BeginInvoke的做法已经太熟悉了。我们也提到了”CLR不喜欢工作线程调用UI元素”。微软的决心如此之大,以至于CLR团队在.NET Framework2.0中添加了CheckForIllegalCrossThreadCalls和Control.Invoke、Control.BeginInvoke方法。这是一次相当重大的改革,CLR团队希望达到这样的效果:
  如果不申明CheckForIllegalCrossThreadCalls = false;这样的”不安全”代码,你就只能使用Control.Invoke和Control.BeginInvoke;而只要使用后两者,不论它们的上下文运行环境是其它工作线程还是UI线程,它们封装的代码都会执行在UI线程内。
所以,msdn对Control.BeginInvoke给出了这样的解释: 在创建控件的基础句柄所在线程上异步执行指定委托。
它的真正含义是: BeginInvoke所谓的异步,是相对于调用线程的异步,而不是相对于UI线程的异步。
  CLR把Control.BeginInvoke(delegate method)中的异步函数执行在UI内,如果你像我上文那样用新线程调用BeginInvoke,那么method相对于这个新线程内的其他函数是异步的。毕竟method执行在了UI线程,新线程立即回调,不必等待Control.BeginInvoke的完成。所以,这个后台线程充分享受了”异步”的好处,不再阻塞,只是我们看不到而已;当然,如果你在BeginInvoke内执行一段耗时的代码,无论是从远程服务器获取数据库资料、IO读取,还是在控件内加载一大批数据,UI线程还是阻塞的。
  正如传统的Delegate.BeginInvoke的异步工作线程取自于.NET线程池,Control.BeginInvoke的异步工作线程就是UI线程。
  现在您明白两种BeginInvoke的区别了吗?

Control.Invoke、BeginInvoke与Windows消息

  实际上,Invoke和BeginInvoke的原理是将调用的方法Marshal成消息,然后调用Win32Api的RegisterWindowMessage()向UI发送消息。我们使用Reflector,可以看到以下代码:
Control.Invoke:
复制代码
public object Invoke(Delegate method, params object [] args)
{
using ( new MultithreadSafeCallScope())
{
return this .FindMarshalingControl().MarshaledInvoke( this , method, args, true );
}
}
复制代码
Control.BeginInvoke:
复制代码
[EditorBrowsable(EditorBrowsableState.Advanced)]
public IAsyncResult BeginInvoke(Delegate method, params object [] args)
{
using ( new MultithreadSafeCallScope())
{
return (IAsyncResult) this .FindMarshalingControl().MarshaledInvoke( this , method, args, false );
}
}
复制代码
  在以上代码中我们看到Control.Invoke和BeginInvoke的不同之处,在于调用MarshaledInvoke时,Invoke向最后一个参数传递了true,而BeginInvoke则是false。
MarshaledInvoke的结构是这样的:
private object MarshaledInvoke(Control caller, Delegate method, object [] args, bool synchronous)
  很明显,最后一个参数synchronous表示是否按照同步处理。MarshaledInvoke内部这样处理这个参数:
复制代码
if ( ! synchronous)
{
return entry;
}
if ( ! entry.IsCompleted)
{
this .WaitForWaitHandle(entry.AsyncWaitHandle);
}
复制代码
  所以,BeginInvoke的处理就是直接回调,Invoke却在等待异步函数执行完后,才继续执行。
  到此为止,Invoke和BeginInvoke的工作就结束了,其余的工作就是UI对消息的处理,它由Control的WndProc(ref Message m)来执行。消息处理到底会给我们的UI带来什么样的影响?接着来看Application.DoEvents()函数。

Application.DoEvents

  Application.DoEvents()函数是WinForm编程中极为重要的函数,但实际编程中,大多数开发者极少调用它。如果您对这个函数缺乏了解,那很可能会在以后长期的编程中对“窗体假死”这样的现象陷入迷惑。
  当运行 Windows 窗体时,它将创建新窗体,然后该窗体等待处理事件。该窗体在每次处理事件时,均将处理与该事件关联的所有代码。所有其他事件在队列中等待。 当代码处理事件时,应用程序不会响应。例如,如果将甲窗口拖到乙窗口之上,则乙窗口不会重新绘制。
  如果在代码中调用 DoEvents,则您的应用程序可以处理其他事件。 例如,如果您有向ListBox添加数据的窗体,并将 DoEvents 添加到代码中,那么当将另一窗口拖到您的窗体上时,该窗体将重新绘制。如果从代码中移除 DoEvents,那么在按钮的单击事件处理程序执行结束以前,您的窗体不会重新绘制。
  因此,如果我们在窗体执行事件时,不处理消息队列中的windows消息,窗体必然会失去响应。而上文已经介绍过,Control.Invoke和BeginInvoke都会向UI发送消息,造成UI对消息的处理,因此,这为我们解决窗体加载大量数据时的假死提供了思路。

解决方案

尝试”无假死”

  这次我们使用开发中出现频率极高的ListView控件,体验一次理想的”异步刷新”,窗体中有一个ListView控件命名为listView1,并将View设置为Detail,添加两个ColumnHeader;一个Button命名为btn_Start,设计视图如下:
开始code:  
复制代码
private readonly int Max_Item_Count = 10000 ;

private void button1_Click( object sender, EventArgs e)
{
new Thread((ThreadStart)( delegate ()
{
for ( int i = 0 ; i < Max_Item_Count; i ++ )
{
// 此处警惕值类型装箱造成的"性能陷阱"
listView1.Invoke((MethodInvoker) delegate ()
{
listView1.Items.Add(
new ListViewItem( new string []
{ i.ToString(),
string .Format( " This is No.{0} item " , i.ToString()) }));
});
};
}))
.Start();
}
复制代码
  代码运行后,你将会看到一个飞速滚动的ListView列表,在加载的过程中,列表以令人眼花缭乱的速度添加数据,此时你尝试拉动滚动条,或者移动窗体,都会发现这次的效果与以往的”白板”、”假死”截然不同!这是一个令人欣喜的变化。
运行过程:
  从我的截图中可以看出,窗体在加载数据的过程中,依然绘制界面,并没有出现”假死”。
  如果上述代码调用的是Control.BeginInvoke,程序会发生些奇怪的现象,想想是为什么?
 
好吧,到了现在,我们终于可以松了一口气了,界面响应的问题已经被解决,一切美好。但是,这样的窗体还是暴漏出两个大问题:
1.  比起传统加载,”无假死窗体”加载速度明显减慢。
2.  加载数据过程中,窗体发生剧烈闪烁现象。

问题分析

  我们在调用Control.Invoke时,强迫窗体处理消息,从而使界面得到了响应,同时也产生了一些副作用。其中之一就是消息处理使得窗体发生了在循环中发生了重绘,”闪烁”现象就是窗体重绘引发的,有过GDI+开发经验的开发者应该比较熟悉。同时,每次调用Invoke都会使UI处理消息,也直接增加了控件对数据处理的时间成本,导致了性能问题。
  对于”性能问题”,我并没有什么解决方案(有自己见解的朋友欢迎提出)。有些控件(ListView、ListBox)具有BeginUpdate和EndUpdate函数,可以临时挂起刷新,加快性能。但毕竟我们这里创建了一个会滚动的界面,这种数据的”动态加载”方式是前者无法比拟的。
  对于”闪烁”,我先来解释问题的原因。通常,控件的绘制包括两个环节:擦出原对象与绘制新对象。首先windows发送一个消息,通知控件擦除原图像,然后进行绘制。如果要在控件面板上以SolidBrush绘制,控件就会在其面板上直接绘制内容。当用户改变了控件尺寸,Windows将会调用很多绘制回收操作,当每次回收和绘制发生时,由于”绘制”较”擦除”更为延后,才会给用户带来”闪烁”的感觉。以往我们为解决此类问题,往往需要在Control.WndProc中作出复杂的处理。而.NET Framework为我们提供了更为优雅的一种方案,那就是双缓冲,我们直接调用它即可。

最终方案

  1. 新建Windows组件DBListView.cs,让它继承自ListView。
  2. 在控件中添加如下代码:
public DBListView()
{
// 打开控件的双缓冲
SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true );
}
  将项目重新生成,然后从工具箱中拖出新增的组建DBListView到窗体上,命名为dbListView1,执行以下代码:
复制代码
private void button1_Click( object sender, EventArgs e)
{
new Thread((ThreadStart)( delegate ()
{
for ( int i = 0 ; i < Max_Item_Count; i ++ )
{
// 此处警惕值类型装箱造成的"性能陷阱"
dbListView1.Invoke((MethodInvoker) delegate ()
{
dbListView1.Items.Add(
new ListViewItem( new string []
{ i.ToString(),
string .Format( " This is No.{0} item " , i.ToString()) }));
});
};
}))
.Start();
}
复制代码
  现在”闪烁”的问题是不是已经得到了解决?
  在我们的实际应用中,这种加载数据引起的阻塞是很常见的,在用户对界面性能关注度不高的情况下,使用本文介绍的方式处理这种阻塞是一种不错的选择,如果以类似IE8、迅雷等软件的载入动画配合,效果会更理想。
 
分类: 线程处理
27
1
(请您对文章做出评价)
« 博主前一篇: 谈.Net委托与线程——创建无阻塞的异步调用(二)
posted @ 2011-04-08 09:23 横竖都溢 阅读(7315) 评论( 44) 编辑 收藏
  
#1楼 2011-04-08 09:41 DreamSea530  
学习了!
  
#2楼 2011-04-08 09:43 风枫峰  
学习了!
  
#3楼 2011-04-08 09:51 ㄟ荖樹炪厊ㄖ  
没见到application.doevent 如何使用的~~也没看明白你讲的原理
  
#4楼 2011-04-08 10:10 风云  
WinForm 开发人员必须掌握的技能之一,推荐!
  
#5楼 2011-04-08 10:25 乐章  
通俗易懂,好文,应该是循环体内调用一下application.doevent()吧,不知道dbListView1.Refresh() 性能会不会好点,或者每10条调用一下
  
#6楼 2011-04-08 10:39 server126  
  
#7楼 2011-04-08 10:54 小涡  
  
#8楼 2011-04-08 12:58 dege301  
这篇文章写得很不错,有粗有细,行文思路很清晰,内容也很充实,希望楼主继续努力
  
#9楼 [ 楼主] 2011-04-08 13:05 横竖都溢  
@ㄟ荖樹炪厊ㄖ
对Application.DoEvents()的大部分介绍来自msdn,我只是要通过它表达"如果在代码中调用 DoEvents,则您的应用程序可以处理其他事件。"这个观点,限于篇幅,它的使用不做过多的介绍。
  
#10楼 [ 楼主] 2011-04-08 13:13 横竖都溢  
引用 乐章:通俗易懂,好文,应该是循环体内调用一下application.doevent()吧,不知道dbListView1.Refresh() 性能会不会好点,或者每10条调用一下

在同步代码中的循环体调用Application.DoEvents().效果等同于新线程中调用Control.Invoke(),都是加载一条数据处理一次消息,效率完全相同,可以这么用。
对于每10条或者100条调用一次Control.BeginInvoke的做法,思路是正确的。这样可以减少消息处理的次数,与控件的重绘,显著提高效率。我将代码改成这样:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
new Thread((ThreadStart) delegate ()
{
     for ( int i = 0; i < count; i+=10)
     {
         listView1.Invoke((MethodInvoker) delegate ()
         {
             for ( int j = i; j < i + 10; j++)
             {
                 if (j >= count) continue ;
  
                 listView1.Items.Add( new ListViewItem( new string [] { j.ToString(), string .Format( "This is No.{0} item" , j.ToString()) }));
             }
         });
     };
})
.Start();

运行后,效果要比之前好的多。
  
#11楼 [ 楼主] 2011-04-08 13:14 横竖都溢  
@dege301
谢谢。其实我写文自认为还是比较罗嗦的,以后尽量改进。
  
#12楼 2011-04-08 13:23 浪涛风雨  
  
#13楼 2011-04-08 14:05 Jacklondon Chen  
只需要在循环中调用 Application.DoEvents() 就可以了,为什么弄那么复杂呢?根本不需要调用什么 Invoke!
  
#14楼 [ 楼主] 2011-04-08 14:26 横竖都溢  
引用 Jacklondon Chen:只需要在循环中调用 Application.DoEvents() 就可以了,为什么弄那么复杂呢?根本不需要调用什么 Invoke!

看到文章里大量篇幅都在讲Control.Invoke、Control.BeginInvoke和Windows消息处理么?因为很多人都对这两个函数存在误解,将它们同窗体线程的异步工作联系起来,所以做这么多论述。
由于侧重于多线程情况下的UI处理,因此UI线程的同步处理在这里就不再涉及了。
  
#15楼 2011-04-08 15:10 俊鹏  
我顶你个肺啊!
  
#16楼 2011-04-08 16:03 CPPWorking  
也可以用BackgroundWorker做。。DoWork里面做后台的操作,ProgressChanged里面做UI的更新。
  
#17楼 2011-04-08 16:28 huyong  
好家伙。谢谢。
  
#18楼 2011-04-08 17:33 山大勺士  
主动调用Application.DoEvents
会有线程安全问题
  
#19楼 2011-04-08 19:04 深蓝医生  
非常好,收藏啊!
  
#20楼 2011-04-08 19:54 chasefornone  
感谢楼主分享,有个小疑问:如果使用Control.BeginInvoke的话是不是可能会出现listviewitem的顺序不对,还是其他问题?
  
#21楼 2011-04-08 21:01 DYStudio.Net  
很实用~~~
  
#22楼 2011-04-09 08:34 吴伯伯  
最近在做CS编程、受教了。
  
#23楼 2011-04-10 09:06 Moen  
好文,不过有个问题。就是在加载数据的过程中关闭窗体,会抛出InvalidOperationException异常
  
#24楼 [ 楼主] 2011-04-10 12:48 横竖都溢  
@chasefornone
由于使用BeginInvoke,会使工作线程立即回调,因此对UI数据的加载无法得到顺序的保证。因为没办法确定每次加载的执行顺序。
  
#25楼 [ 楼主] 2011-04-10 12:51 横竖都溢  
@Moen
实例里为了简化代码,直接实例化了工作线程。实际上你应该设置每个工作线程的IsBackground属性,作为后台线程处理。否则主线程关闭了,你的工作线程仍不会随之停止。
  
#26楼 2011-04-13 11:38 yueyue184  
  
#27楼 2011-05-21 10:25 cong8023  
非常好!
  
#28楼 2011-05-23 16:15 cong8023  
楼主,我有个疑问?
你说Control.Invoke()跟Control.BeginInvoke()都是把线程封装到主线程(Ui线程)上运行,那为什么你的例子中用Control.Invoke()就能防假死,而Control.BeginInvoke()却不能呢,而且还执行顺序错乱。
能解释么
  
#29楼 [ 楼主] 2011-05-23 16:48 横竖都溢  
@cong8023
这个很容易理解。Invoke将指令Marshal成消息传送给UI,如果UI不处理完这条消息,工作线程仍然继续阻塞,直到UI处理完上一条消息。
对于BeginInvoke,虽然也将消息传送给UI,但它不理会上调消息UI是否处理完毕,而是直接发送下一个命令,循环中所有命令的执行顺序是无法得到保证的,所以BeginInvoke既不能保证UI处理完了消息,也不能保证其执行顺序,它适合于处理单条指令,而非例子中的情形。
  
#30楼 2011-05-23 17:54 cong8023  
呵呵,谢谢,我理解了,自己研究了一会,没看你的,想的也跟你一样
  
#31楼 2011-07-13 11:42 zhengjian211  
作者你好:请问这个实例中如何捕捉到加载过程已经完成 能否给出代码呢?
我的目的是:数据加载完给出弹窗提示
  
#32楼 2011-08-08 14:15 火星大能猫  
这边文章写得比较透彻,很给力,不像大部分文章介绍异步,就是介绍的Control.BeginInvoke
  
#33楼 2011-08-08 15:00 火星大能猫  
使用这个方法的话,datagridview的滚动条会变成黑色,为何??
  
#34楼 2011-09-08 09:47 diorlv  
赞一个,非常受用的文章,初学者必学啊,而且要弄明白才有意义,比如Invoke和BeginInvoke的区别,呵呵,这个当时学的时候也是困扰多时的问题,学会了,就从程序员入门进阶了
  
#35楼 2011-10-23 17:20 好好*学习  
Very Good !
楼主从哪里学到这些知识的,能不能给我一些相关资料?
我想多了解一些关于c#编程底层的东西.
  
#36楼 [ 楼主] 2012-02-06 11:54 横竖都溢  
@好好*学习
都是自己在各个地方慢慢积累的,没有什么系统的资料。
  
#37楼 2012-02-10 15:25 afutureBoss  
楼主好,拜读过文章了,请问,0x0318,0x00000004这些消息值你都是在那些地方找的啊?可否分享一下
  
#38楼 2012-04-15 18:33 kakoyi  
楼主,辛苦你了。
通过看这篇文章,我知道了control 的invoke和begininvoke的区别是

begininvoke是不阻塞调用线程。
我把 listView1.Invoke换成了listView1.BeginInvoke 界面就死掉了,按理说,是不会死的呀

还有 我直接换成了下面这种,居然也不卡界面了,不知道为什么,
new Thread(() =>
{
foreach (int s in new int[10000000])
{ this.listBox1.Items.Add(s);
}
}).Start();

还有 我直接换成
foreach (int s in new int[10000000])
{
ThreadPool.QueueUserWorkItem((o)=>{ .... });
}
这样也不报线程间操作无效的 异常,只是调试模式下会看到此异常。搞不懂为什么。
  
#39楼 [ 楼主] 2012-04-16 22:28 横竖都溢  
引用 kakoyi:
楼主,辛苦你了。
通过看这篇文章,我知道了control 的invoke和begininvoke的区别是

begininvoke是不阻塞调用线程。
我把 listView1.Invoke换成了listView1.BeginInvoke 界面就死掉了,按理说,是不会死的呀

还有 我直接换成了下面这种,居然也不卡界面了,不知道为什么,
new Thread(() =>
{
foreach (int s in new int[10000000])
{ th...

你对BeginInvoke理解错了,你应该详细注意的是这段话:正如传统的Delegate.BeginInvoke的异步工作线程取自于.NET线程池,Control.BeginInvoke的异步工作线程就是UI线程。因此:Control.BeginInvoke内的所有代码都执行在UI线程。
为什么使用BeginInvoke还会卡:因为你的理解存在误区,对于ListView或者ListBox的Add方法,界面卡不是因为他们工作在UI线程的原因,而是在Add过程中,没有持续的调用Application.DoEvents()函数,Invoke工作在UI线程内,每次add后都会由windows执行完所有的消息,再进行下一次add,所以界面不会卡。BeginInvoke也工作在UI线程内,但每次Add后立即回调,根本不理会windows是否执行完所有的消息,所以界面依然会卡——这点有点难理解,需要花时间消化。

至于为什么调试模式下会看到线程操作异常,是因为微软的控件检测只发生在调试环境下,你可以去msdn上查看CheckForIllegalCrossThreadCalls函数的说明。
  
#40楼 2012-05-12 17:59 Johnny Qian  
引用在以上代码中我们看到Control.Invoke和BeginInvoke的不同之处,在于调用MarshaledInvoke时,Invoke向最后一个参数传递了false,而BeginInvoke则是true。

楼主,这句话里面的true和false写反了吧?
  
#41楼 [ 楼主] 2012-05-13 01:04 横竖都溢  
@Johnny Qian
恩,确实是写反了,已经更改,谢谢!
  
#42楼 2012-05-16 11:20 ysh1988  
学习了,辛苦
  
#43楼 2012-05-21 14:56 Rookier  
有点眼晕。。先mk
  
#44楼 2457370 2012/8/27 17:52:50 2012-08-27 17:52 编程记录  
谢谢分享!
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值