当前位置:编程学习 > asp >>

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

引言

  在之前的《创建无阻塞的异步调用》中,已经介绍过异步调用的编写步骤和实施原理。异步调用是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线
补充:Web开发 , ASP.Net ,
CopyRight © 2012 站长网 编程知识问答 www.zzzyk.com All Rights Reserved
部份技术文章来自网络,