zoukankan      html  css  js  c++  java
  • 使Windows Forms成为线程安全的(转)

    介绍

    对于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的。如果有
    任何疑问,做一个深拷贝传递好了,别使用引用。


  • 相关阅读:
    Vue.js——vue-router 60分钟快速入门
    史上最全最强SpringMVC详细示例实战教程
    介绍用C#和VS2015开发基于Unity架构的2D、3D游戏的技术
    iphone导入照片不显示,不同步怎么整
    Unity 3D入门简介
    Vue + Element UI 实现权限管理系统
    Spring Boot + Spring Cloud 构建微服务系统
    linux下文件的复制、移动与删除
    2017年全球AI芯片公司大盘点
    人工智能爆发 中美AI芯片大比拼
  • 原文地址:https://www.cnblogs.com/rainbowzc/p/1370073.html
Copyright © 2011-2022 走看看