介绍
对于windows forms用户界面编程来说,如果不使用多线程的话,程序都是直接了当的。
但是在实际应用中,为了确保UI的响应性,就必须使用多线程。这就导致了界面开发变得复杂起来。
遇到的问题
如大家所知,windows forms并不是线程安全的。例如,除非你对消息队列进行了控制,那么对
Windows.Forms上的一个控件的属性值进行读写并不是安全的。这里的重点是,你只能通过消息
队列线程对你的Windows Forms上的控件进行修改。
标准解决方案
当然,我们有一个机制来解决这个问题。对于每一个Windows Forms的控件,都有一个 InvokeRequired
的属性。如果这个属性的值是False,那么当前的线程就是消息队列线程,你就可以对控件的属性进行
安全的读写。另外,还有一个方法 Invokde ,这个方法把一个delegate和他的参数一起放到某个控件
消息队列里面等待调用。
因为对delegate的调用是直接从消息队列里面发起的,所以没有什么threading相关的编程内容。但是这种
类型的编程是及其乏味的。仅仅为了设置一下一个Text的文本,或者enabling/disabling一个控件,你就
需要定义一个分离的方法和对应的delegate.
例子:随机字符串
为了说明这个情况,我写了一个小的Windows Forms程序来生成一个随机的字符串。下面的代码片段演示了如何
在消息循环队列和工作线程之间进行同步。
我使用了 Thread.Abort ,因为这只是一个简单的示例。如果你正在执行一个在任何情况下都不能打断的操作,
那么使用一个flag来通知你的线程。
上面的代码里面有许多简单而重复的代码。比如在调用Invoke之前,你必要总要检查一下InvokeRequired.因为
如果消息队列还没有创建你就调用了Invoke,就会产生一个错误。
生成的线程安全wrappers
在之前的文章里面,我介绍了如何自动生成一个类的wrappers来"隐含"的实现一个接口。同样的代码进行扩展,
可以实现创建wrppers,然后自动的让线程来调用正确的方法。
稍后我将解释整个机制的工作原理,首先,我们看看怎么使用。
首先你公布出来相关的属性,不用关心线程编程相关的事情。这个事情即便你不使用多线程,也是打算要做的。
然后,你定义一个接口,包含所有的你打算从另外一个线程里面调用的属性或者方法。
现在,在工作线程里面,你所要做的就是创建一个线程安全的wrapper然后使用,那些重复的代码你再也不需要输入了。
工作机制
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的。如果有
任何疑问,做一个深拷贝传递好了,别使用引用。
对于windows forms用户界面编程来说,如果不使用多线程的话,程序都是直接了当的。
但是在实际应用中,为了确保UI的响应性,就必须使用多线程。这就导致了界面开发变得复杂起来。
遇到的问题
如大家所知,windows forms并不是线程安全的。例如,除非你对消息队列进行了控制,那么对
Windows.Forms上的一个控件的属性值进行读写并不是安全的。这里的重点是,你只能通过消息
队列线程对你的Windows Forms上的控件进行修改。
标准解决方案
当然,我们有一个机制来解决这个问题。对于每一个Windows Forms的控件,都有一个 InvokeRequired
的属性。如果这个属性的值是False,那么当前的线程就是消息队列线程,你就可以对控件的属性进行
安全的读写。另外,还有一个方法 Invokde ,这个方法把一个delegate和他的参数一起放到某个控件
消息队列里面等待调用。
因为对delegate的调用是直接从消息队列里面发起的,所以没有什么threading相关的编程内容。但是这种
类型的编程是及其乏味的。仅仅为了设置一下一个Text的文本,或者enabling/disabling一个控件,你就
需要定义一个分离的方法和对应的delegate.
例子:随机字符串
为了说明这个情况,我写了一个小的Windows Forms程序来生成一个随机的字符串。下面的代码片段演示了如何
在消息循环队列和工作线程之间进行同步。
- char PickRandomChar(string digits)
- {
- Thread.Sleep(100);
- return digits[random.Next(digits.Length)];
- }
- delegate void SetBoolDelegate(bool parameter);
- void SetInputEnabled(bool enabled)
- {
- if(!InvokeRequired)
- {
- button1.Enabled=enabled;
- comboBoxDigits.Enabled=enabled;
- numericUpDownDigits.Enabled=enabled;
- }
- else
- Invoke(new SetBoolDelegate(SetInputEnabled),new object[] {enabled});
- }
- delegate void SetStringDelegate(string parameter);
- void SetStatus(string status) {
- if(!InvokeRequired)
- labelStatus.Text=status;
- else
- Invoke(new SetStringDelegate(SetStatus),new object[] {status});
- }
- void SetResult(string result) {
- if(!InvokeRequired)
- textBoxResult.Text=result;
- else
- Invoke(new SetStringDelegate(SetResult),new object[] {result});
- }
- delegate int GetIntDelegate();
- int GetNumberOfDigits()
- {
- if(!InvokeRequired)
- return (int)numericUpDownDigits.Value;
- else
- return (int)Invoke(new GetIntDelegate(GetNumberOfDigits),null);
- }
- delegate string GetStringDelegate();
- string GetDigits()
- {
- if(!InvokeRequired)
- return comboBoxDigits.Text;
- else
- return (string)Invoke(new GetStringDelegate(GetDigits),null);
- }
- void Work()
- {
- try
- {
- SetInputEnabled(false);
- SetStatus("Working");
- int n=GetNumberOfDigits();
- string digits=GetDigits();
- StringBuilder text=new StringBuilder();
- for(int i=0;i!=n;i++)
- {
- text.Append(PickRandomChar(digits));
- SetResult(text.ToString());
- }
- SetStatus("Ready");
- }
- catch(ThreadAbortException)
- {
- SetResult("");
- SetStatus("Error");
- }
- finally
- {
- SetInputEnabled(true);
- }
- }
- void Start()
- {
- Stop();
- thread=new Thread(new ThreadStart(Work));
- thread.Start();
- }
- void Stop()
- {
- if(thread!=null)
- {
- thread.Abort();
- thread=null;
- }
- }
我使用了 Thread.Abort ,因为这只是一个简单的示例。如果你正在执行一个在任何情况下都不能打断的操作,
那么使用一个flag来通知你的线程。
上面的代码里面有许多简单而重复的代码。比如在调用Invoke之前,你必要总要检查一下InvokeRequired.因为
如果消息队列还没有创建你就调用了Invoke,就会产生一个错误。
生成的线程安全wrappers
在之前的文章里面,我介绍了如何自动生成一个类的wrappers来"隐含"的实现一个接口。同样的代码进行扩展,
可以实现创建wrppers,然后自动的让线程来调用正确的方法。
稍后我将解释整个机制的工作原理,首先,我们看看怎么使用。
首先你公布出来相关的属性,不用关心线程编程相关的事情。这个事情即便你不使用多线程,也是打算要做的。
- public bool InputEnabled
- {
- set
- {
- button1.Enabled=value;
- comboBoxDigits.Enabled=value;
- numericUpDownDigits.Enabled=value;
- }
- }
- public string Status
- {
- set { labelStatus.Text=value;}
- }
- public int NumberOfDigits
- {
- get { return numericUpDownDigits.Value; }
- }
- public string Digits
- {
- get { return comboBoxDigits.Text; }
- }
- public string Result
- {
- set { textBoxResult.Text=value; }
- }
然后,你定义一个接口,包含所有的你打算从另外一个线程里面调用的属性或者方法。
- interface IFormState
- {
- int NumberOfDigits { get; }
- string Digits { get; }
- string Status { set; }
- string Result { set; }
- bool InputEnabled { set; }
- }
现在,在工作线程里面,你所要做的就是创建一个线程安全的wrapper然后使用,那些重复的代码你再也不需要输入了。
- void Work()
- {
- IFormState state=Wrapper.Create(typeof(IFormState),this);
- try
- {
- state.InputEnabled=false;
- state.Status="Working";
- int n=state.NumberOfDigits;
- string digits=state.Digits;
- StringBuilder text=new StringBuilder();
- for(int i=0;i<n;i++)
- {
- text.Append(PickRandomChar(digits));
- state.Result=text.ToString();
- }
- state.Status="Ready";
- }
- catch(ThreadAbortException)
- {
- state.Status="Error";
- state.Result="";
- }
- finally
- {
- state.InputEnabled=true;
- }
- }
工作机制
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的。如果有
任何疑问,做一个深拷贝传递好了,别使用引用。