C# Winform项目中多线程环境下, 如何跨线程对Window窗体控件进行安全访问?

原创 2016年02月29日 15:35:15
[简介]
常用网名: 猪头三
出生日期: 1981.XX.XX
个人网站: http://www.x86asm.com
QQ交流: 643439947
编程生涯: 2001年~至今[共15年]
职业生涯: 13年
开发语言: C/C++、80x86ASM、PHP、Perl、Objective-C、Object Pascal、C#、Python
开发工具: Visual Studio、Delphi、XCode、Eclipse
技能种类: 逆向 驱动 磁盘 文件
研发领域: Windows应用软件安全/Windows系统内核安全/Windows系统磁盘数据安全
项目经历: 磁盘性能优化/文件系统数据恢复/文件信息采集/敏感文件监测跟踪/网络安全检测

[序言]
由于我的主力编程语言不是C#, 因此很多细节都没有进行充分的研究. 但由于C#越来越优秀, 也越来越面向跨平台的优势发展. 因此我的项目都在有计划的移植到.NET环境. 在利用C#进行软件开发时, 最头痛的是多线程环境下进行跨线程对Window窗体控件进行安全访问和内容更新. 但这方面的资料非常少, 至少我没有见到国内有人总结出来, 就算有人总结出来, 也基本还停留在 new Thread + InvokeRequired + Invoke + Delegate模式上, 真的非常过时了, 而且开发起来也非常繁琐. 现在已经进化到.NET 4.0以上了, 经过几次软件的开发, 我个人觉得我是十分推荐.NET 4.0 以上的多线程模式. 下面我们就说说这方面的技术细节.

[首先按照下面的截图, 创建一个C# Winform项目]
1> 按钮 类名为: Bn_Start
2> 按钮 单击事件为: private void Bn_Start_Click(object sender, EventArgs e){...};
3> TextBox文本框 类名为: Tb_Text


[注意一个细节]

如果你的异步方法没有出现await逻辑处理, 可以使用await Task.Yield()来强制你的方法转换为异步上下文环境, 详细说明: https://msdn.microsoft.com/zh-cn/library/system.threading.tasks.task.yield(v=vs.110).aspx

[下面我们开始写一个简单的多线程代码]

多线程代码功能: 循环 1000000 次 , 然后把计数显示在TextBox文本框.
private void Bn_Start_Click(object sender, EventArgs e)
{
    // 启动任务线程
    Task.Run(()=>
    {
        for (int int_Index = 0; int_Index < 1000000; int_Index++)
        {
            Tb_Text.Text = int_Index.ToString();
        }
    });

    // 异步显示对话框
    MessageBox.Show("异步执行...");

}// End Bn_Start_Click()

请尝试运行这段代码, 结果你会发现微软开发工具会提示, Tb_Text.Text = int_Index.ToString(); 涉及"对Windows窗体控件进行线程安全调用", 并给了如下的解决方案:https://msdn.microsoft.com/zh-cn/library/ms171728(v=vs.100).aspx 结果看到这篇文章, 我彻底蒙了. 还是.NET的过去式技术. 为什么不给出一个合理的Task模式下的跨线程访问Windows 窗体控件呢? 于是我只能阅读大量的MSDN文档. 终于找到了2个最合理的技术解决方案....

[方案1: Task + WindowsFormsSynchronizationContext + Send]
请看如下代码:
private SynchronizationContext mpr_sc_UIContext;
mpr_sc_UIContext = WindowsFormsSynchronizationContext.Current;

private void Bn_Start_Click(object sender, EventArgs e)
{
    // 启动任务线程
    Task.Run(()=>
    {
        for (int int_Index = 0; int_Index < 1000000; int_Index++)
        {
            mpr_sc_UIContext.Send( _ =>
            {
                Tb_Text.Text = int_Index.ToString();
            }, null);
        }
    });

    // 异步显示对话框
    MessageBox.Show("异步执行...");

}// End Bn_Start_Click()

经过上面的改进之后, 你会发现TextBox文本框里面内容能实时更新了, 但是由于 for 1000000次 这个运算消耗了大量的CPU时间片, 因此MessageBox.Show("异步执行...")这个代码没有能立即异步执行. 要过几秒钟才能弹出. 这样不是很完美, 那有没有更好的方案呢? 有的, 下面请看方案2.

[方案2: Task + TaskScheduler.FromCurrentSynchronizationContext + Task.Factory.StartNew]

请看如下代码:
private TaskScheduler mpr_ts_UIContext;
mpr_ts_UIContext = TaskScheduler.FromCurrentSynchronizationContext();

private void Bn_Start_Click(object sender, EventArgs e)
{
    // 启动任务线程
    Task.Run(()=>
    {
        for (int int_Index = 0; int_Index < 1000000; int_Index++)
        {
            var ts_Run = Task.Factory.StartNew(() =>
            {
                Tb_Text.Text = int_Index.ToString();
            }, CancellationToken.None, TaskCreationOptions.None, mpr_ts_UIContext);
            // 注意这里要同步等待Task.Factory.StartNew的任务结束
            ts_Run.Wait();
        }
    });

    // 异步显示对话框
    MessageBox.Show("异步执行...");

}// End Bn_Start_Click()

经过方案2的改进之后, 你会发现MessageBox.Show("异步执行...")这个代码能立即异步执行了. 是不是很爽.

[方案3: Task + TaskScheduler.FromCurrentSynchronizationContext + Task.Factory.StartNew + async + Unwrap]

请看如下代码:
private TaskScheduler mpr_ts_UIContext;
mpr_ts_UIContext = TaskScheduler.FromCurrentSynchronizationContext();

private void Bn_Start_Click(object sender, EventArgs e)
{
    // 启动任务线程
    Task.Run(()=>
    {
        for (int int_Index = 0; int_Index < 1000000; int_Index++)
        {
            var ts_Run = Task.Factory.StartNew(async () =>
            {
                Tb_Text.Text = int_Index.ToString();
                
                // 模拟使用await xxxx ;
                await Task.Delay(100);

            }, CancellationToken.None, TaskCreationOptions.None, mpr_ts_UIContext).Unwrap();

            // 注意这里要同步等待Task.Factory.StartNew().Unwrap()返回的任务(PS: 这个任务是async模式)
            ts_Run.Wait();
        }
    });

    // 异步显示对话框
    MessageBox.Show("异步执行...");

}// End Bn_Start_Click()

上面的方案3, 大家看到什么玄机了吗? 其实就是Task.Factory.StartNew任务支持async模式, 这样我们就可以在任务线程里面使用await进行同步了. 但是要注意, 如果在Task.Factory.StartNew任务下使用async模式, 那么必须使用Unwrap()来获取async模式的任务对象, 再进行Wait(), 就可以做到Task.Factory.StartNew任务的同步等待. 如果你不使用Unwrap(), 那么是无法等待的.

[总结]
这3个方案, 都是非常实用且国内还真没有人分享这个技术. 那就让我来填补这个空白吧. 技术是否有价值, 大家可以慢慢体会了...




C# 使用委托跨线程通讯

当我们需要处理大量数据时,为了使UI界面不致出现假死状态,我们就必须使用多线程进行处理。所以问题就出现了,我们都知道线程作为一个独立运行的单元,线程间不可以随意访问和修改,那么该怎么办呢?其实C#提供...
  • czw2010
  • czw2010
  • 2012年08月25日 21:19
  • 13848

Winform 让跨线程访问变得更简单

前言 由于多线程可能导致对控件访问的不一致,导致出现问题。C#中默认是要线程安全的,即在访问控件时需要首先判断是否跨线程,如果是跨线程的直接访问,在运行时会抛出异常。近期在项目中碰到这个问题,首先...

Delphi7高级应用开发随书源码

  • 2003年04月30日 00:00
  • 676KB
  • 下载

C# Winform 跨线程更新UI控件常用方法总结(转)

出处:http://www.tuicool.com/articles/FNzURb 概述 C#Winform编程中,跨线程直接更新UI控件的做法是不正确的,会时常出现“线程间操作无效: 从不是创建...
  • dragoo1
  • dragoo1
  • 2017年02月10日 12:35
  • 1856

如何跨线程调用Windows窗体控件

 在开发具有线程的应用程序时,有时会通过子线程实现Windows窗体,以及控件的操作,比如:在对文件进行复制时,为了使用户可以更好的观察到文件的复制情况,可以在指定的Windows窗体上显示一个进度条...

C#跨线程调用窗体控件

 前段时间遇到跨线程调用窗体控件的问题,其实一句话System.Windows.Forms.Control.CheckForIllegalCrossThreadCalls = false;就可以解决,...

Winform 跨线程访问控件的两个方法总结

博主刚接触线程没多久,需要实现将线程中计算出的数据传递给winform的label显示出来,但是C#中禁止跨线程直接访问控件,首先想到方法一(用委托实现) : 具体拿一个案例(内部计算已省略)说明:...

初试C#多线程_跨线程访问控件

C#里创建线程的方式是   Thread t = new Thread(new ThreadStart(this.DoSomething));  t.Start();           里面的DoS...

WinForm(C#)中跨线程访问控件的解决方法

由于多线程可能导致对控件访问的不一致,导致出现问题。C#中默认是要线程安全的,即在访问控件时需要首先判断是否跨线程,如果是跨线程的直接访问,在运行时会抛出异常。 解决办法有两个: 1、不进...

c#如何跨线程调用窗体控件

public partial class Form1 : Form     {         public Form1()         {             InitializeC...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:C# Winform项目中多线程环境下, 如何跨线程对Window窗体控件进行安全访问?
举报原因:
原因补充:

(最多只允许输入30个字)