多线程(异步委托)调用和控件响应问题

本文探讨了在.NET环境下如何利用多线程技术刷新UI控件,并详细解释了BeginInvoke方法的使用,以及如何避免在后台线程直接操作UI控件以确保程序稳定性和响应性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

当系统要进行工作量较大的任务时,倾向于将该任务安排在一个优先级别相对较低的后台线程处理,这样UI线程或主线程还将可以响应用户的其他操作,使系统具有良好的交互性。下面以一个控件刷新的程序为例,讨论多线程和控件的关系。程序代码如下:
  1. usingSystem;
  2. usingSystem.Collections.Generic;
  3. usingSystem.ComponentModel;
  4. usingSystem.Data;
  5. usingSystem.Drawing;
  6. usingSystem.Text;
  7. usingSystem.Windows.Forms;
  8. usingSystem.Threading;
  9. namespaceWindowsApplication3
  10. {
  11. publicpartialclassFormTest:Form
  12. {
  13. privatedelegatevoidInvokeDelegate();
  14. privatedelegatevoidSetTextBoxHandler(TextBoxtb,stringstr);
  15. privateInvokeDelegateinvokeDelegate;
  16. privateSetTextBoxHandlerdeleChangeText;
  17. publicFormTest()
  18. {
  19. InitializeComponent();
  20. }
  21. privatevoidFormTest_Load(objectsender,EventArgse)
  22. {
  23. //初始化两个代理实例
  24. this.invokeDelegate=newInvokeDelegate(this.FunTest);
  25. this.deleChangeText=newSetTextBoxHandler(this.SetTextBoxValue);
  26. }
  27. privatevoidFunTest()
  28. {
  29. for(inti=0;i<100000;i++)
  30. {
  31. //搜索控件句柄,判断控件是否生成,不生成则返回false,此时就不能调用begininvoke方法了
  32. //另外,如果对if内部语句的调用发生在同一线程(即UI线程或主线程),则InvokeRequired将返回false;
  33. if(this.textBox1.InvokeRequired)
  34. {
  35. this.BeginInvoke(this.deleChangeText,newobject[]{this.textBox1,i.ToString()});
  36. //一定注意,Sleep的动作一定不能放在SetTextBoxValue()方法中,因为控件的BeginInvoke()会将SetTextBoxValue()方法
  37. //封送至后台线程,这就变成了在后台线程中让主线程Sleep300ms的时间,而不是让后台线程Sleep300ms了。
  38. Thread.Sleep(300);
  39. }
  40. else
  41. {
  42. this.SetTextBoxValue(this.textBox1,i.ToString());
  43. Thread.Sleep(300);
  44. }
  45. }
  46. }
  47. privatevoidSetTextBoxValue(TextBoxtb,stringstr)
  48. {
  49. tb.Text=str;
  50. tb.Refresh();
  51. //下面的句子表明,即使该方法被封送到后台线程后,该方法内的线程是主线程,而非后台线程
  52. //MessageBox.Show(Thread.CurrentThread.IsBackground.ToString());
  53. }
  54. //在后台线程中调用FunTest()
  55. privatevoidbutton1_Click(objectsender,EventArgse)
  56. {
  57. //使用异步调用,开辟后台线程
  58. this.invokeDelegate.BeginInvoke(null,null);
  59. }
  60. //在UI线程中调用FunTest()
  61. privatevoidbutton2_Click(objectsender,EventArgse)
  62. {
  63. this.FunTest();
  64. }
  65. //使用代理调用函数,结果同button2_Click()
  66. privatevoidbutton3_Click(objectsender,EventArgse)
  67. {
  68. this.invokeDelegate();
  69. }
  70. }
  71. }

代码的主要目的是,点击button,使得控件textbox控件的值从1刷新到100000,主要有三种方式,第一,异步委托调用;第二,直接调用;第三,代理同步调用。

经过试验发现一下特点:

第一,三者都可以刷新textbox,但是后面二者不能响应窗体其他事件,但使用异步委托的话,窗体可以相应拖放、点击button等事件,拖放窗体时,textbox仍持续刷新。

第二, 使用异步委托或者多线程调用FunTest()方法,涉及到对UI控件的操作,本例中表现为对textbox设置text属性及刷新,而在后台线程中操作控件很容易引起异常,使得线程使用不安全。一般情况下,不鼓励在后台线程中操作控件。.Net提供了Control.BeginInvoke()等方法来解决这一问题。倘若后台线程需要操作控件,则必须将这些操作函数绑定到一个代理D上,然后通过控件的BeginInvoke()方法调用,D作为该方法的一个参数。在例程中,操作控件的方法为SetTextBoxValue(),使用代理deleChangeText绑定,然后,通过BeginInvoke()调用。

第三, 控件的InvokeRequired属性的作用。如果FunTest()方法在非UI线程中执行,则InvokeRequired返回true,若在UI线程中执行,则返回false。在例子中使用if else流程控制语句,若FunTest()在后台线程中执行,则必须使用控件的BeginInvoke()方法,若FunTest()在UI线程中执行,则可直接调用控件操作方法,无需使用BeginInvoke()方法。注意FunTest()既可能在后台线程中调用,也可能在UI线程中调用,而在不同的线程内操作控件的方法有所不同,因此,必须使用InvokeRequired属性和if else来分别给出正确的调用方法。本例子中点击button2button3所调用的方法,就是else分支的方法,因为这两种调用方法不属于多线程调用。

第四, 关于FunTest()方法中的Thread.Sleep(300)。在不加这一sleep操作时,在对四台计算机的试验中,运行时,三台计算机的FormTest窗体可以拖动,一台不可以拖动。添加sleep操作后,所有计算机的FormTest窗口都可以拖动。我猜想,虽然实现的是多线程操作,但计算机的性能不同,是否能够响应窗体事件也不一样。当我不停的点击button1,不停的开辟新线程的时候,sleep参数设置为300时,最多开辟62个线程后,窗体不再响应事件,button1也无法再点击;当sleep设置为200时,不停点击button1,最多开辟61个线程,此后窗体不再响应事件。可见,虽然多线程可以提高UI线程的响应,但这并不是绝对的,随着计算机性能的下降,UI线程的响应能力就越差,直到无法响应。

第五, Thread.Sleep(300)的位置问题。是否可以将Thread.Sleep(300)放到SetTextBoxValue()方法中?实验证明,这样做是不行的,我们希望sleep的线程是后台线程而非UI线程,一旦Sleep()方法放到SetTextBoxValue()方法中,SetTextBoxValue()将被封送至后台线程中运行,然后这里的Sleep,实际上是UI线程的Sleep,而不是后台线程的Sleep了。

*当子线程是前台线程,则主线程结束并不影响其他线程的执行,只有所有前台线程都结束,程序结束

*当子线程是后台线程,则主线程的结束,会导致子线程的强迫结束

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值