(C#)安全、简单的Windows Forms多线程编程  (一)

Chris Sells

June 28, 2002

翻译:袁晓辉 www.farproc.com farproc#AT#gmail#dot#com
2006年1月18日

原文链接

下载例子:AsynchCalcPi.exe.

说实在的,我最初打算做的事情和本文主要讨论的内容毫不相关。那时,我第一次发现我需要在.NET中计算一个圆的面积,当然,首先需要一个pi(π)的精确值。System.Math.PI用起来倒是很方便,但它只提供了20位的精度,我不禁为计算的精度而担心(其实21位的就可以绝对令我感到舒服)。所以和其他任何称职的程序员一样,我忘记了真正需要解决的问题,而埋头写出了一个自己喜欢的可以算出任意位小数的π值的程序。最终的结果如图1。

图1. 计算Pi值的程序

耗时操作(Long-Running Operations)的进度

 

虽然大多数的程序不需要计算pi的值,但是很多的程序都需要进行一些耗时的操作,比如打印、调用一个Web service或者计算一位太平洋西北岸亿万富翁的利息收入。对用户来说,只要可以看到当前完成的进度,这样的等待通常还是可以接受的,甚至是一个抽身忙些其他事情的机会。所以我给我的每一个小程序都添加了一个进度条(progress bar)。我的这个计算pi值的程序所用的算法每次计算9位数字。一旦新的一组数字被计算出来,我的程序就更新TextBox控件并移动ProgressBar来显示我们的进度。比如图2就是正在计算1000位pi值的情形(如果21位没有问题,1000位也必定更好)。

图2. 正在计算1000位Pi值

下面的代码展示了pi值的数字被计算出来之后,用户界面(UI)是如何更新的:

void ShowProgress(string pi, int totalDigits, int digitsSoFar) {

  _pi.Text = pi;

  _piProgress.Maximum = totalDigits;

  _piProgress.Value = digitsSoFar;

}

 

void CalcPi(int digits) {

StringBuilder pi = new StringBuilder("3", digits + 2);

  // 显示进度

  ShowProgress(pi.ToString(), digits, 0);

if( digits > 0 ) {

pi.Append(".");

for( int i = 0; i < digits; i += 9 ) {

int nineDigits = NineDigitsOfPi.StartingAt(i+1);

int digitCount = Math.Min(digits - i, 9);

string ds = string.Format("{0:D9}", nineDigits);

pi.Append(ds.Substring(0, digitCount));

      // 显示进度

      ShowProgress(pi.ToString(), digits, i + digitCount);

}

}

}

一切进行的都很顺利,直到我在计算pi的1000位的过程中切换到了其他程序做了些什么然后再切换回来时……。我看到了图3显示的画面:

图3. 没有paint事件了

问题的根源当然在于我们的程序是单线程的。当这个线程忙于计算pi值时,就没有机会去绘制UI了。之前我之所以没有遇到这个问题是因为当我设置TextBox.Text属性和ProgressBar.Value属性时,这些控件,作为设置属性操作的一部分,可以强制他们的绘画操作立即进行(尽管我注意到了progress bar的情况要比text box好一些)。然而,当我把这个程序放到后台然后再带回前台时,我需要重绘整个客户区,对窗口来说就是一个Paint事件。因为当前正在处理的事件(Calc按钮的Click事件)返回之前其他事件是不会得到处理的,于是我们就没有机会看到更多的进度显示了。我现在需要做的就是把UI线程释放出来专做UI工作,把耗时的操作放到后台处理。为了实现这个目标,我们需要另外一个线程。

异步操作

 

现在我的同步Click handler看起来是这样的:

void _calcButton_Click(object sender, EventArgs e) {

CalcPi((int)_digits.Value);

}

回忆一下我们的问题是“直到CalcPi返回,线程才可以从Click handler返回,窗口才有机会处理Paint(或其他)事件”。处理这种情况的一个方法是启动另外的线程,比如:

using System.Threading;

int _digitsToCalc = 0;

void CalcPiThreadStart() {

CalcPi(_digitsToCalc);

}

void _calcButton_Click(object sender, EventArgs e) {

_digitsToCalc = (int)_digits.Value;

  Thread piThread = new Thread(new ThreadStart(CalcPiThreadStart));           

  piThread.Start();

}

现在,在button Click事件返回之前无需再等待CalcPi的完成了,我创建并启动了一个新的线程,Thread.Start方法将调度并启动新的线程,然后立即返回,让UI线程回到它自己的工作上来。现在如果用户和程序交互(比如放到后台,置到前台,改变窗口大小,关闭它),UI线程可以自由地处理这些事件,同时worker线程也在进行它的计算pi的工作。图4显示了两个线程工作的情形:

图4. 幼稚的多线程

你也许注意到了,我没有为worker线程的入口点——CalcPiThreadStart传递任何参数,而是把需要计算的位数放入一个字段(field)_digitsToCalc中,调用线程的入口点,CalcPi被调用。这是一种痛苦,也是我喜欢用Delegate来作异步工作的一个原因。Delegate支持参数,避免了我对增加一个额外的临时field和一个额外的函数而做激烈的思想斗争。

如果你对delegate(委托)不熟悉,就认为他们是调用static或instance函数的对象。在C#中它们的声明语法和函数是一样的。比如CalcPi的一个委托看起来是这样的:

delegate void CalcPiDelegate(int digits);

一旦有了一个委托,我就可以实例化一个对象来同步调用CalcPi函数:

void _calcButton_Click(object sender, EventArgs e) {

  CalcPiDelegate  calcPi = new CalcPiDelegate(CalcPi);

  calcPi((int)_digits.Value);

}

当然,我并不想同步调用CalcPi;我想要异步调用它。然而,在我做这个之前我们需要了解一点委托的工作原理。上面的委托声明实际上声明了一个类,该类继承自一个自带有三个函数(InvokeBeginInvokeEndInvoke)的MultiCastDelegate类,像这样:

class CalcPiDelegate : MulticastDelegate {

  public void Invoke(int digits);

  public void BeginInvoke(int digits, AsyncCallback callback,

                          object asyncState);

  public void EndInvoke(IAsyncResult result);

}

当我先实例化一个CalcPiDelegate对象然后像调用函数一样调用它时,我实际上调用了他的同步Invoke函数。它随后调用了我的CalcPi。然而BeginInvokeEndInvoke是一对可以让你异步调用并收获(harvest)返回值的函数。所以,为了让CalcPi在另外一个线程中运行,我需要像下面那样调用BeginInvoke

void _calcButton_Click(object sender, EventArgs e) {

CalcPiDelegate  calcPi = new CalcPiDelegate(CalcPi);

  calcPi.BeginInvoke((int)_digits.Value, null, null);

}

注意我们为BeginInvoke的最后两个参数传递了null。这两个参数在我们需要随后收获(harvest)函数的返回值时(EndInvoke就是用来做这个的)就有用了。由于CalcPi函数直接更新UI,我们仅需为这两个参数传递null。如果你对委托的细节(同步和异步)感兴趣,可以看看.NET委托:一个C#睡前故事中文翻译版)这篇文章。

到这时,我应该感到高兴。我已经在我的程序里显示耗时操作进度并保持了UI的良好交互性。

 

多线程安全

 

看起来我足够幸运(或者说不幸,看你怎么看待这些事情了)。Microsoft Windows(R)XP给我提供了一个非常健壮的Windows Forms赖以建立的windows系统底层实现。它是如此健壮,以至于优雅地帮我包揽了所有问题,即使我违反了主要的Windows编程方针——不要在创建一个窗口的线程之外的线程操作这个窗口。不幸的是,不能保证其他不太健壮的Windows实现不会同样优雅地给我脸色看。

问题当然是我自己造成的。回忆一下图4,我用两个线程同时访问一个窗口。然而,因为耗时操作在Windows程序中是如此的普遍,以至于Windows Forms里的每个UI类(每一个本质上从System.Windows.Forms.Control继承的类)都有一个可以在任何线程中安全访问的属性InvokeRequired。这个属性在一个线程调用控件的对象方法之前需要先封传该控件到创建这个控件的线程时返回true。我的ShowProgress函数中如果简单地增加一个Assert就会立即显现出我上述做法的错误之处。

using System.Diagnostics;

void ShowProgress(string pi, int totalDigits, int digitsSoFar) {

  // Make sure we're on the right thread

  Debug.Assert(_pi.InvokeRequired == false);

...

}

实事上,.NET文档在这一点上是很清晰的。它描述如下:“控件上有四种方法可以安全地从任何线程进行调用:InvokeBeginInvokeEndInvokeCreateGraphics。对于所有其他方法调用,当从另一个线程进行调用时,应使用这些 Invoke 方法之一。”所以,当我设置控件属性时,就明确地违反了这条规则。前三个函数的名字就明确地指出了我需要构建另外一个在UI线程中执行的委托。如果我不想像阻塞UI线程一样阻塞我的worker线程,我就需要使用异步的BeginInvokeEndInvoke。然而,由于我的worker线程生来就是为UI线程服务的,就让世界简单一点,使用同步的Invoke方法吧。像下面:

public object Invoke(Delegate method);

public object Invoke(Delegate method, object[] args);

第一个重载的Invoke接收一个包含我们将要在UI线程中调用的方法的委托的实例,这个委托(或方法)必须没有参数。然而我们想要用来更新UI的函数ShowProgress带有三个参数,所以我们需要第二个重载形式。我们同样需要为我们的ShowProgress方法定义一个单独的委托以便我们可以正确地传递参数。这里给出了如何使用Invoke来确保我们调用的ShowProgress也包括我们对窗口的使用是在正确的线程中执行的方法:(请确认替换CalcPi中两处对ShowProgress的调用)。

delegate

void ShowProgressDelegate(string pi, int totalDigits, int digitsSoFar);

 

void CalcPi(int digits) {

StringBuilder pi = new StringBuilder("3", digits + 2);

  // 准备异步显示进度

  ShowProgressDelegate showProgress =

    new ShowProgressDelegate(ShowProgress);

  // 显示进度

  this.Invoke(showProgress, new object[] { pi.ToString(), digits, 0});

if( digits > 0 ) {

pi.Append(".");

for( int i = 0; i < digits; i += 9 ) {

...

      // 显示进度

      this.Invoke(showProgress,

        new object[] { pi.ToString(), digits, i + digitCount});

}

}

}

Invoke的使用最终让我在Windows Forms程序中安全的使用多线程。UI线程孵化出一个worker线程来执行耗时操作,当UI需要更新时worker线程把控制传递回UI线程。图5显示了我们的安全多线程构架。

图5. 安全的多线程

简化了的多线程

 

对Invoke的调用多少有点麻烦,因为它在CalcPi中调用了两次,我可以简化一下,改进ShowProgress让它自己来作异步调用。如果ShowProgress是在正确的线程中调用的,他将更新控件,但是如果它在不正确的线程中被调用,它就会使用Invoke在正确的线程中回头调用它自身。这让我们回到了以前,简单些的CalcPi

void ShowProgress(string pi, int totalDigits, int digitsSoFar) {

  // 确保我们在正确的线程中

  if( _pi.InvokeRequired == false ) {

_pi.Text = pi;

_piProgress.Maximum = totalDigits;

_piProgress.Value = digitsSoFar;

  }

  else {

    // 异步显示进度

    ShowProgressDelegate showProgress =

      new ShowProgressDelegate(ShowProgress);

    this.Invoke(showProgress,

      new object[] { pi, totalDigits, digitsSoFar});

  }

}

void CalcPi(int digits) {

StringBuilder pi = new StringBuilder("3", digits + 2);

  // 显示进度

  ShowProgress(pi.ToString(), digits, 0);

if( digits > 0 ) {

pi.Append(".");

for( int i = 0; i < digits; i += 9 ) {

...

      //显示进度

      ShowProgress(pi.ToString(), digits, i + digitCount);

}

}

}

因为Invoke是一个异步调用并且我们并不真的需要它的返回值(事实上,ShowProgress本身就不返回任何值),这里最好是使用BeginInvoke以便worker线程不阻塞。就像下面:

BeginInvoke(showProgress, new object[] { pi, totalDigits, digitsSoFar});

如果不需要函数调用的返回值,BeginInvoke总是应该优先考虑,因为它可以让worker线程立即回到自己的工作上来,并且避免死锁的可能性。

 

我们做了什么?

 

我用了一个简短的例子,演示了如何在执行耗时操作的同时显示进度并保持UI的用户操作的响应。为了完成这个任务,我用了一个异步委托来“孵化”出一个worker线程、主窗口上的Invoke方法和另外一个要封传回UI线程执行的代理。

我非常小心地避免做一件事情,那就是在UI线程和worker线程之间共享数据。相反,对于那些必需的数据我传递它们的拷贝(要计算的位数、已经计算出来的数字和进度)。在最终的解决方案中,我从来都没有传递对象的引用,比如当前StringBuilder的引用(虽然传递引用可以节省每次回到UI线程时对字符串的拷贝)。如果我要在两个线程之间传递数据的引用,我就必须使用.NET原始的同步手段来确保任一时刻只有一个线程访问一个对象,这将增加许多工作量。这样已经足够了,无需引入同步进制。

当然,如果你需要处理的是数据库,你肯定不会打算把数据库到处复制。然而在Windows Forms程序中,如果可能我推荐你在worker线程和UI线程之间使用异步委托和消息来实现耗时操作。

感谢

I'd like to thank Simon Robinson for his post on the DevelopMentor .NET mailing list that inspired this article, Ian Griffiths for his initial work in this area, Chris Andersen for his message-passing ideas, and last but certainly not least, Mike Woodring for the fabulous multithreading pictures that I lifted shamelessly for this article.

参考

  • This article's source code
  • .NET Delegates: A C# Bedtime Story
  • Win32 Multithreaded Programming by Mike Woodring and Aaron Cohen

Chris Sells is an independent consultant, specializing in distributed applications in .NET and COM, as well as an instructor for DevelopMentor. He's written several books, including ATL Internals, which is in the process of being updated for ATL7. He's also working on Essential Windows Forms for Addison-Wesley and Mastering Visual Studio .NET for O'Reilly. In his free time, Chris hosts the Web Services DevCon and directs the Genghis source-available project. More information about Chris, and his various projects, is available at http://www.sellsbrothers.com.

<待续>

(C#)安全、简单的Windows Forms多线程编程 (一)相关推荐

  1. 通信软件基础B-重庆邮电大学-Java-编程实现一个简单的聊天程序-多线程编程实现

    实验任务六 编程实现一个简单的聊天程序-多线程编程实现 1. 系统设计要求 编程实现一个简单的聊天程序,实现两台计算机间的信息交互,使用多线程编程实现:可同时连接多个客户端,服务器收到客户端发送的消息 ...

  2. Windows下多线程编程

    前言 熟练掌握Windows下的多线程编程,能够让我们编写出更规范多线程代码,避免不要的异常.Windows下的多线程编程非常复杂,但是了解一些常用的特性,已经能够满足我们普通多线程对性能及其他要求. ...

  3. Windows下多线程编程技术及其实现

    本文首先讨论16位Windows下不具备的线程的概念,然后着重讲述在32位Windows 95环境下多线程的编程技术,最后给出利用该技术的一个实例,即基于Windows95下TCP/IP的可视电话的实 ...

  4. Windows C++ 多线程编程示例

    /* 题目:主线程创建两个辅助线程,辅助线程1使用选择排序算法对数组的前半部分排序,辅助线程2使用选择排序算法对数组的后半部分排序,主线程等待辅助线程运行結束后,使用归并排序算法归并子线程的计算结果 ...

  5. Windows下多线程编程 C/C++ —— 矩阵乘法的并行算法

    一.串行算法 设两个矩阵A和B,大小分别为M * N 和 N * P, 如果C = A * B, 则C的大小为M * P. 相应的代码表示如下: 这里可能大家直观想法可能是int A[M][N],但是 ...

  6. C++使用thread类多线程编程

    目录 pthread多线程 系统自带CreateThread std::thread c++ 多线程总结_jacke121的专栏-CSDN博客 std thread比较好用,但是系统带的socket不 ...

  7. Windows Forms 实现安全的多线程详解

    原文:http://www.cnblogs.com/smartsoft2005/archive/2005/09/11/234687.html 前言 在我们应用程序开发过程中,经常会遇到一些问题,需要使 ...

  8. Windows平台下的多线程编程

    线程是进程的一条执行路径,它包含独立的堆栈和CPU寄存器状态,每个线程共享所有的进程资源,包括打开的文件.信号标识及动态分配的内存等.一个进程内的所有线程使用同一个地址空间,而这些线程的执行由系统调度 ...

  9. C语言中pthread或Windows API在多线程编程中的基本应用

    文章目录 多线程概述 掌握多线程需要学习什么? 使用pthread.h实现多线程 使用Windows API实现多线程 使用threads.h实现多线程 参考资料 警告 由于我懒得写完,而且懂的也不是 ...

最新文章

  1. 使用git上传代码到github
  2. java迭代器应用 源码探究
  3. Log4J入门教程(一) 入门例程
  4. Expo大作战(十二)--expo中的自定义样式Custom font,以及expo中的路由RouteNavigation
  5. 彼聆智能语音机器人_电销行业的人工智能:智能语音电话机器人
  6. jquery和php上传文件进度条,jQuery实现文件上传进度条特效_jquery
  7. 利用tar 通过网络拷贝数据
  8. python修改excel内容怎么覆盖_Python修改Excel的内容,python,excel
  9. php数组用中文作为键
  10. Python TAB 补齐
  11. 新版本steam退回旧版本教程
  12. 华为交换机,登录密码忘记
  13. 强烈推荐:创业起步 八种赢利模式
  14. 如何传递NoteExpress的参考文献(包括题录和附件)给他人?
  15. [Ynoi2015]此时此刻的光辉
  16. 7-16 寻找大富翁
  17. C++ upper_bound()和lower_bound()(二分查找中使用)的定义,使用方法和区别
  18. 微信支付失败提示签名错误,请检查后重试
  19. Vscode babel 将es6转es5
  20. IPGuard客户端与应用程序冲突处理方法

热门文章

  1. heic格式转化jpg,heic无损转jpg
  2. str and system
  3. 微信社群裂变小程序开发
  4. android开发艺术探讨_深入探讨:为Android开发应用程序
  5. 《游戏脚本的设计与开发》-(RPG部分)3.5 游戏背包和任务系统
  6. 自行搭建家庭版服务器x96 max-硬件篇(一)
  7. java根据模板导出word文档
  8. 金蝶k3c 对接InvokeHelper类的改写
  9. 给大家介绍一款开源的熟人/陌生人社交平台
  10. [今日干货]一个吸粉效果也不错的APP