rainbowzc

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: :: :: 管理 ::
介绍

对于windows forms用户界面编程来说,如果不使用多线程的话,程序都是直接了当的。
但是在实际应用中,为了确保UI的响应性,就必须使用多线程。这就导致了界面开发变得复杂起来。

遇到的问题

如大家所知,windows forms并不是线程安全的。例如,除非你对消息队列进行了控制,那么对
Windows.Forms上的一个控件的属性值进行读写并不是安全的。这里的重点是,你只能通过消息
队列线程对你的Windows Forms上的控件进行修改。

标准解决方案

当然,我们有一个机制来解决这个问题。对于每一个Windows Forms的控件,都有一个 InvokeRequired
的属性。如果这个属性的值是False,那么当前的线程就是消息队列线程,你就可以对控件的属性进行
安全的读写。另外,还有一个方法 Invokde ,这个方法把一个delegate和他的参数一起放到某个控件
消息队列里面等待调用。
因为对delegate的调用是直接从消息队列里面发起的,所以没有什么threading相关的编程内容。但是这种
类型的编程是及其乏味的。仅仅为了设置一下一个Text的文本,或者enabling/disabling一个控件,你就
需要定义一个分离的方法和对应的delegate.

例子:随机字符串

为了说明这个情况,我写了一个小的Windows Forms程序来生成一个随机的字符串。下面的代码片段演示了如何
在消息循环队列和工作线程之间进行同步。

  1. char PickRandomChar(string digits) 
  2.     Thread.Sleep(100); 
  3.     return digits[random.Next(digits.Length)]; 
  4. delegate void SetBoolDelegate(bool parameter); 
  5. void SetInputEnabled(bool enabled)
  6. {
  7.     if(!InvokeRequired)
  8.     {
  9.         button1.Enabled=enabled; 
  10.         comboBoxDigits.Enabled=enabled; 
  11.         numericUpDownDigits.Enabled=enabled;
  12.     } 
  13.     else 
  14.         Invoke(new SetBoolDelegate(SetInputEnabled),new object[] {enabled}); 
  15. delegate void SetStringDelegate(string parameter);
  16. void SetStatus(string status) { 
  17.     if(!InvokeRequired)
  18.         labelStatus.Text=status; 
  19.     else 
  20.         Invoke(new SetStringDelegate(SetStatus),new object[] {status});
  21. void SetResult(string result) {
  22.     if(!InvokeRequired)
  23.         textBoxResult.Text=result; 
  24.     else
  25.         Invoke(new SetStringDelegate(SetResult),new object[] {result});
  26. delegate int GetIntDelegate(); 
  27. int GetNumberOfDigits()
  28. {
  29.     if(!InvokeRequired) 
  30.         return (int)numericUpDownDigits.Value;
  31.     else 
  32.         return (int)Invoke(new GetIntDelegate(GetNumberOfDigits),null);
  33. delegate string GetStringDelegate(); 
  34. string GetDigits()
  35. {
  36.     if(!InvokeRequired) 
  37.         return comboBoxDigits.Text; 
  38.     else
  39.         return (string)Invoke(new GetStringDelegate(GetDigits),null);
  40. }
  41. void Work() 
  42. {
  43.     try 
  44.     {
  45.         SetInputEnabled(false);
  46.         SetStatus("Working");        
  47.         int n=GetNumberOfDigits();
  48.         string digits=GetDigits();
  49.         StringBuilder text=new StringBuilder();
  50.         for(int i=0;i!=n;i++)
  51.         {
  52.             text.Append(PickRandomChar(digits));
  53.             SetResult(text.ToString());
  54.         }
  55.         SetStatus("Ready");
  56.     }
  57.     catch(ThreadAbortException) 
  58.     {
  59.         SetResult("");
  60.         SetStatus("Error");
  61.     }
  62.     finally 
  63.     {
  64.         SetInputEnabled(true);
  65.     }
  66. }
  67. void Start() 
  68. {
  69.     Stop();
  70.     thread=new Thread(new ThreadStart(Work));
  71.     thread.Start();
  72. }
  73. void Stop() 
  74. {
  75.     if(thread!=null
  76.     {
  77.         thread.Abort();
  78.         thread=null;
  79.     }
  80. }

我使用了 Thread.Abort ,因为这只是一个简单的示例。如果你正在执行一个在任何情况下都不能打断的操作,
那么使用一个flag来通知你的线程。
上面的代码里面有许多简单而重复的代码。比如在调用Invoke之前,你必要总要检查一下InvokeRequired.因为
如果消息队列还没有创建你就调用了Invoke,就会产生一个错误。

生成的线程安全wrappers

在之前的文章里面,我介绍了如何自动生成一个类的wrappers来"隐含"的实现一个接口。同样的代码进行扩展,
可以实现创建wrppers,然后自动的让线程来调用正确的方法。

稍后我将解释整个机制的工作原理,首先,我们看看怎么使用。

首先你公布出来相关的属性,不用关心线程编程相关的事情。这个事情即便你不使用多线程,也是打算要做的。

  1. public bool InputEnabled
  2. {
  3.     set
  4.     { 
  5.         button1.Enabled=value; 
  6.         comboBoxDigits.Enabled=value; 
  7.         numericUpDownDigits.Enabled=value;
  8.     } 
  9. }
  10. public string Status 
  11. {
  12.     set { labelStatus.Text=value;} 
  13. }
  14. public int NumberOfDigits
  15. {
  16.     get { return numericUpDownDigits.Value; }
  17. }
  18. public string Digits 
  19. {
  20.     get { return comboBoxDigits.Text; }
  21. }
  22. public string Result 
  23. {
  24.     set { textBoxResult.Text=value; }
  25. }

然后,你定义一个接口,包含所有的你打算从另外一个线程里面调用的属性或者方法。

  1. interface IFormState
  2. {
  3.     int NumberOfDigits { get; } 
  4.     string Digits { get; }
  5.     string Status { set; }
  6.     string Result { set; }
  7.     bool InputEnabled { set; }
  8. }

现在,在工作线程里面,你所要做的就是创建一个线程安全的wrapper然后使用,那些重复的代码你再也不需要输入了。

  1. void Work() 
  2. {
  3.     IFormState state=Wrapper.Create(typeof(IFormState),this);
  4.     try 
  5.     {
  6.         state.InputEnabled=false;
  7.         state.Status="Working";
  8.         int n=state.NumberOfDigits;
  9.         string digits=state.Digits;
  10.         StringBuilder text=new StringBuilder();
  11.         for(int i=0;i<n;i++) 
  12.         {
  13.             text.Append(PickRandomChar(digits));
  14.             state.Result=text.ToString();
  15.         }   
  16.         state.Status="Ready";
  17.     }
  18.     catch(ThreadAbortException) 
  19.     {
  20.         state.Status="Error";
  21.         state.Result=""
  22.     }
  23.     finally
  24.     {
  25.         state.InputEnabled=true;
  26.     }
  27. }

工作机制

wrapper生成器使用System.Reflection.Emit来生成一个代理类,这个代理类包含接口所需的所有方法,同时他也包含访问属性
的方法,每个方法都有一个特定的签名(signature)。

在方法体里面,如果InvokeRequired的返回值是false,那么就直接调用原始的方法。这个检查是很重要的,为了如果form还没有
attached到一个消息线程的时候,调用也能够工作。

如果InvokeRequired返回true,那么一个指向原始的方法的delegate当作是调用这个form的Invode方法的参数传递进去。delegate
的类型被缓存,这样对于相同签名的方法,不会重复创建delegate类型。

因为wrapper生成器使用ISynchronizeInvoke这个接口来进行同步调用,所以你可以在非windows-forms的程序里面来使用。你要做的
只是实现接口和大概其自己实现一个类似消息队列的东西。

局限性和一些警告


需要理解的一个很重要的事情就是,使用线程安全的wrapper把线程同步这个事情给隐藏起来了,但是并不意味着没有做线程同步。
所以,如果使用线程安全的wrapper来访问一个属性,在InvokeRequired返回ture的情况下,比直接访问这个属性要慢的多。因此,
如果你从几个不同的线程里面对你的form做复杂的改动,最好是把他们放到一个方法里面一次完成,而不是分开几次来进行调用。

另外一个需要牢记在心的是,不是所有的类型在不进行同步的情况下进行线程间传递都是安全的。通常,只有类似int,DateTime,和
一些immutable reference类型,比如string,是安全的。如果你要从一个线程向另一个线程传递一个mutable reference类型,比如
StringBuilder,那么你一定要小心。如果这个object没有在不同线程里面改动,或者他本身是线程安全的,那么传递是ok的。如果有
任何疑问,做一个深拷贝传递好了,别使用引用。


posted on 2009-01-06 10:34  ct  阅读(391)  评论(0编辑  收藏  举报