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

[简介]
常用网名: 猪头三
出生日期: 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个方案, 都是非常实用且国内还真没有人分享这个技术. 那就让我来填补这个空白吧. 技术是否有价值, 大家可以慢慢体会了...




阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页