再次探讨 WinForms 多线程开发

WinForms 已经开源,您现在可以在 GitHub 上查看 WinForm 源代码

正好有人又讨论到在 WinFroms 环境下的多线程开发,这里就再整理一下涉及到的技术点。

从官方文档可以知道,Windows Forms 是 Windows 界面库,例如 User32 和 GDI+ 的 .NET 封装,WinForms 中的控件背后实际上是 Windows 的 GDI 控件实现。

考虑在窗体上执行一个长时间执行的任务

LongTimeWork 代表一个需要长时间操作才能完成的任务。这里通过 Sleep() 来模拟长事件的操作。

主要特性:

  • 通过事件 ValueChanged 反馈任务进度
  • 通过事件 Finished 报告任务已经完成
  • 通过参数 CancellationTokenSource 提供对中途取消任务的支持

代码如下:

using System;
using System.Collections.Generic;
using System.Text; namespace LongTime.Business
{
// 定义事件的参数类
public class ValueEventArgs: EventArgs
{
public int Value { set; get; }
} // 定义事件使用的委托
public delegate void ValueChangedEventHandler(object sender, ValueEventArgs e); public class LongTimeWork
{
// 定义一个事件来提示界面工作的进度
public event ValueChangedEventHandler ValueChanged;
// 报告任务被取消
public event EventHandler Cancelled;
public event EventHandler Finished; // 触发事件的方法
protected void OnValueChanged(ValueEventArgs e)
{
this.ValueChanged?.Invoke(this, e);
} public void LongTimeMethod(System.Threading.CancellationTokenSource cancellationTokenSource)
{
for (int i = 0; i < 100; i++)
{
if(cancellationTokenSource.IsCancellationRequested)
{
this.Cancelled?.Invoke(this, EventArgs.Empty);
return;
} // 进行工作
System.Threading.Thread.Sleep(1000); // 触发事件
ValueEventArgs e = new ValueEventArgs() { Value = i + 1 };
this.OnValueChanged(e);
} this.Finished?.Invoke(this, EventArgs.Empty);
}
}
}

IsHandleCreated 属性告诉我们控件真的创建了吗

Control 基类 是 WinForms 中控件的基类,它定义了控件显示给用户的基础功能,需要注意的是 Control 是一个 .NET 中的类,我们创建出来的也是 .NET 对象实例。但是当控件真的需要在 Windows 上工作的时候,它必须要创建为一个实际的 GDI 控件,当它实际创建之后,可以通过 Control 的 Handle 属性提供 Windows 的窗口句柄。

new 一个 .NET 对象实例并不意味着实际的 GDI 对象被创建,例如,当执行到窗体的构造函数的时候,这时候仅仅正在创建 .NET 对象,而窗体所依赖的 GDI 对象还没有被处理,也就意味着真正的控件实际上还没有被创建出来,我们也就不能开始使用它,这就是 IsHandleCreated 属性的作用。

需要说明的是,通常我们并不需要管理底层的 GDI 处理,WinForms 已经做了良好的封装,我们需要知道的是关键的时间点。

窗体的构造函数和 Load 事件

构造函数是面向对象中的概念,执行构造函数的时候,说明正在内存中构建对象实例。而窗体的 Load 事件发生在窗体创建之后,与窗体第一次显示在 Windows 上之前的时间点上。

它们的关键区别在于窗体背后所对应的 GDI 对象创建问题。在构造函数执行的时候,背后对应的 GDI 对象还没有被创建,所以,我们并不能访问窗体以及控件。在 Load 事件执行的时候,GDI 对象已经创建,所以可以访问窗体以及控件。

在使用多线程模式开发 WinForms 窗体应用程序的时候,需要保证后台线程对窗体和控件的访问在 Load 事件之后进行。

控件访问的线程安全问题

Windows 窗体中的控件是绑定到特定线程的,不是线程安全的。 因此,在多线程情况下,如果从其他线程调用控件的方法,则必须使用控件的一个调用方法将调用封送到正确的线程。

当你在窗体的按钮上,通过双击生成一个对应的 Click 事件处理方法的时候,这个事件处理方法实际上是执行在这个特定的 UI 线程之上的。

不过 UI 线程背后的机制与 Windows 的消息循环直接相关,在 UI 线程上执行长时间的代码会导致 UI 线程的阻塞,直接表现就是界面卡顿。解决这个问题的关键是在 UI 线程之外的工作线程上执行需要花费长时间执行的任务。

这个时候,就会涉及到 UI 线程安全问题,在 工作线程上是不能直接访问 UI 线程上的控件,否则,会导致异常。

那么工作线程如何更新 UI 界面上的控件以达到更新显示的效果呢?

UI 控件提供了一个可以安全访问的属性:

  • InvokeRequired

和 4 个可以跨线程安全访问的方法:

  1. Invoke
  2. BeginInvode
  3. EndInvoke
  4. GreateGraphics

不要被这些名字所迷惑,我们从线程的角度来看它们的作用。

InvokeRequired 用来检查当前的线程是否就是创建控件的线程,现在 WinForms 已经开源,你可以在 GitHub 上查看 InvokeRequired 源码,最关键的就是最后的代码行。

public bool InvokeRequired
{
get
{
using var scope = MultithreadSafeCallScope.Create(); Control control;
if (IsHandleCreated)
{
control = this;
}
else
{
Control marshalingControl = FindMarshalingControl(); if (!marshalingControl.IsHandleCreated)
{
return false;
} control = marshalingControl;
} return User32.GetWindowThreadProcessId(control, out _) != Kernel32.GetCurrentThreadId();
}
}

所以,我们可以通过这个 InvokeRequired 属性来检查当前的线程是否是 UI 的线程,如果是的话,才可以安全访问控件的方法。示例代码如下:

if (!this.progressBar1.InvokeRequired) {
this.progressBar1.Value = e.Value;
}

但是,如果当前线程不是 UI 线程呢?

安全访问控件的方法 Invoke

当在工作线程上需要访问控件的时候,关键点在于我们不能直接调用控件的 4 个安全方法之外的方法。这时候,必须将需要执行的操作封装为一个委托,然后,将这个委托通过 Invoke() 方法投递到 UI 线程之上,通过回调方式来实现安全访问。

这个 Invoke() 方法的定义如下:

public object Invoke (Delegate method);
public object Invoke (Delegate method, params object[] args);

这个 Delegate 实际上是所有委托的基类,我们使用 delegate 定义出来的委托都是它的派生类。这就意味所有的委托其实都是可以使用的。

不过,有两个特殊的委托被推荐使用,根据微软的文档,它们比使用其它类型的委托速度会更快。见:https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.control.invoke?view=net-5.0

  • EventHandler
  • MethodInvoder

当注册的委托被系统回调的时候,如果委托类型是 EventHandler,那么参数 sender 将被设置为控件本身的引用,而 e 的值是 EventArgs.Empty。

MethodInvoder 委托的定义如下,可以看到它与 Action 委托定义实际上是一样的,没有参数,返回类型为 void。

public delegate void MethodInvoker();

辅助处理线程问题的 SafeInvoke()

由于需要确保对控件的访问在 UI 线程上执行,创建辅助方法进行处理。

这里的 this 就是 Form 窗体本身。

private void SafeInvoke(System.Windows.Forms.MethodInvoker method)
{
if (this.InvokeRequired)
{
this.Invoke(method);
}
else
{
method();
}
}

这样在需要访问 UI 控件的时候,就可以通过这个 SafeInvode() 来安全操作了。

private void workder_ValueChanged(object sender, ValueEventArgs e)
{
this.SafeInvoke(
() => this.progressBar1.Value = e.Value
);
}

使用 BeginInvoke() 和 EndInvoke()

如果你查看 BeginInvoke() 的源码,可以发现它与 Invoke() 方法的代码几乎相同。

public object Invoke(Delegate method, params object[] args)
{
using var scope = MultithreadSafeCallScope.Create();
Control marshaler = FindMarshalingControl();
return marshaler.MarshaledInvoke(this, method, args, true);
}

BeginInvoke() 方法源码

public IAsyncResult BeginInvoke(Delegate method, params object[] args)
{
using var scope = MultithreadSafeCallScope.Create();
Control marshaler = FindMarshalingControl();
return (IAsyncResult)marshaler.MarshaledInvoke(this, method, args, false);
}

它们都会保证注册的委托运行在 UI 安全的线程之上,区别在于使用 BeginInvoke() 方法的场景。

如果你的委托内部使用了异步操作,并且返回一个处理异步的 IAsyncResult,那么就使用 BeginInvoke()。以后,使用 EndInvode() 来得到这个异步的返回值。

使用线程池

在 .NET 中,使用线程并不意味着一定要创建 Thread 对象实例,我们可以通过系统提供的线程池来使用线程。

线程池提供了将一个委托注册到线程池队列中的方法,该方法要求的委托类型是 WaitCallback。

public static bool QueueUserWorkItem (System.Threading.WaitCallback callBack);
public static bool QueueUserWorkItem<TState> (Action<TState> callBack, TState state, bool preferLocal);

WaitCallback 委托的定义,它接收一个参数对象,返回类型是 void。

public delegate void WaitCallback(object state);

可以将启动工作线程的方法修改为如下方式,这里使用了弃元操作,见 弃元 - C# 指南

System.Threading.WaitCallback callback
= _ => worker.LongTimeMethod(); System.Threading.ThreadPool.QueueUserWorkItem(callback);

完整的代码:

using LongTime.Business;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms; namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
} private void Form1_Load(object sender, EventArgs e)
{ } private void Button1_Click(object sender, EventArgs e)
{
// 禁用按钮
this.button1.Enabled = false; // 实例化业务对象
LongTime.Business.LongTimeWork worker
= new LongTime.Business.LongTimeWork(); worker.ValueChanged
+= new LongTime.Business.ValueChangedEventHandler(workder_ValueChanged); /*
// 创建工作线程实例
System.Threading.Thread workerThread
= new System.Threading.Thread(worker.LongTimeMethod); // 启动线程
workerThread.Start();
*/ System.Threading.WaitCallback callback
= _ => worker.LongTimeMethod(); System.Threading.ThreadPool.QueueUserWorkItem(callback);
} private void SafeInvoke(System.Windows.Forms.MethodInvoker method)
{
if (this.InvokeRequired)
{
this.Invoke(method);
}
else
{
method();
}
} private void workder_ValueChanged(object sender, ValueEventArgs e)
{
this.SafeInvoke(
() => this.progressBar1.Value = e.Value
);
}
}
}

使用 BackgroundWorker

BackgroundWorker 封装了 WinForms 应用程序中,在 UI 线程之外的工作线程vs执行任务的处理。

主要特性:

  • 进度
  • 完成
  • 支持取消

该控件实际上希望你将业务逻辑直接写在它的 DoWork 事件处理中。但是,实际开发中,我们可能更希望将业务写在单独的类中实现。

报告进度

我们直接使用 BackgroundWorker 的特性来完成。

首先,报告进度要进行两个基本设置:

  • 首先需要设置支持报告进度更新
  • 然后,注册任务更新的事件回调
// 设置报告进度
this.backgroundWorker1.WorkerReportsProgress = true;
// 注册进度更新的事件回调
backgroundWorker1.ProgressChanged +=
new ProgressChangedEventHandler( backgroundWorker1_ProgressChanged);

当后台任务发生更新之后,通过调用 BackgroundWorker 的 ReportProgress() 方法来报告进度,这个一个线程安全的方法。

然后,BackgroundWorker 的 ProgressChanged 事件会被触发,它会运行在 UI 线程之上,可以安全的操作控件的方法。

private void workder_ValueChanged(object sender, ValueEventArgs e)
{
// 通过 BackgroundWorker 来更新进度
this.backgroundWorker1.ReportProgress( e.Value);
}
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
// BackgroundWorker 可以安全访问控件
this.progressBar1.Value = e.ProgressPercentage;
}

报告完成

由于我们并不在 DoWork 事件中实现业务,所以也不使用 BackgroundWorker 的报告完成操作。

在业务代码中,提供任务完成的事件。

this.Finished?.Invoke(this, EventArgs.Empty);

在窗体中,注册事件回调处理,由于回调处理不能保证执行在 UI 线程之上, 通过委托将待处理的 UI 操作封装为委托对象传递给 SaveInfoke() 方法。

private void worker_Finished(object sender, EventArgs e)
{
SafeInvoke(() =>
{
this.Reset();
this.resultLabel.Text = "Task Finished!";
});
}

取消任务

BackgroundWorker 的取消是建立在整个业务处理写在 DoWork 事件回调中, 我们的业务写在独立的类中。所以,我们自己完成对于取消的支持。

让我们的处理方法接收一个 对象来支持取消。每次都重新创建一个新的取消对象。

// 每次重新构建新的取消通知对象
this.cancellationTokenSource = new System.Threading.CancellationTokenSource();
worker.LongTimeMethod( this.cancellationTokenSource);

点击取消按钮的时候,发出取消信号。

private void BtnCancel_Click(object sender, EventArgs e)
{
// 发出取消信号
this.cancellationTokenSource.Cancel();
}

业务代码中会检查是否收到取消信号,收到取消信号会发出取消事件,并退出操作。

if(cancellationTokenSource.IsCancellationRequested)
{
this.Cancelled?.Invoke(this, EventArgs.Empty);
return;
}

在窗体注册的取消事件处理中,处理取消响应,还是需要注意线程安全问题

private void worker_Cancelled(object sender, EventArgs e)
{
SafeInvoke(() =>
{
this.Reset();
this.resultLabel.Text = "Task Cancelled!";
});
}

代码实现

using LongTime.Business;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms; namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
private System.ComponentModel.BackgroundWorker backgroundWorker1;
private System.Threading.CancellationTokenSource cancellationTokenSource; public Form1()
{
InitializeComponent(); // 创建后台工作者对象实例
this.backgroundWorker1
= new System.ComponentModel.BackgroundWorker(); // 设置报告进度
this.backgroundWorker1.WorkerReportsProgress = true; // 支持取消操作
this.backgroundWorker1.WorkerSupportsCancellation = true; // 注册开始工作的事件回调
backgroundWorker1.DoWork +=
new DoWorkEventHandler(backgroundWorker1_DoWork); // 注册进度更新的事件回调
backgroundWorker1.ProgressChanged +=
new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged);
} private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
// 可以接收来自 RunWorkerAsync() 的参数,供实际的方法使用
object argument = e.Argument; // 后台进程,不能访问控件 // 实例化业务对象
LongTime.Business.LongTimeWork worker
= new LongTime.Business.LongTimeWork(); worker.ValueChanged
+= new LongTime.Business.ValueChangedEventHandler(workder_ValueChanged);
worker.Finished
+= new EventHandler(worker_Finished);
worker.Cancelled
+= new EventHandler(worker_Cancelled); // 每次重新构建新的取消通知对象
this.cancellationTokenSource = new System.Threading.CancellationTokenSource();
worker.LongTimeMethod(this.cancellationTokenSource);
} private void worker_Cancelled(object sender, EventArgs e)
{
SafeInvoke(() =>
{
this.Reset();
this.resultLabel.Text = "Task Cancelled!";
});
} private void worker_Finished(object sender, EventArgs e)
{
SafeInvoke(() =>
{
this.Reset();
this.resultLabel.Text = "Task Finished!";
});
} private void SafeInvoke(System.Windows.Forms.MethodInvoker method)
{
if (this.InvokeRequired)
{
this.Invoke(method);
}
else
{
method();
}
} private void Form1_Load(object sender, EventArgs e)
{ } private void Button1_Click(object sender, EventArgs e)
{
// 控件操作,禁用按钮
this.button1.Enabled = false;
this.button2.Enabled = true; // 启动后台线程工作
// 实际的工作注册在
this.backgroundWorker1.RunWorkerAsync();
} private void workder_ValueChanged(object sender, ValueEventArgs e)
{
// 通过 BackgroundWorker 来更新进度
this.backgroundWorker1.ReportProgress(e.Value);
}
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
// BackgroundWorker 可以安全访问控件
this.progressBar1.Value = e.ProgressPercentage;
} private void BtnCancel_Click(object sender, EventArgs e)
{
// 发出取消信号
this.cancellationTokenSource.Cancel();
} private void Reset()
{
this.resultLabel.Text = string.Empty; // Enable the Start button.
this.button1.Enabled = true; // Disable the Cancel button.
this.button2.Enabled = false; this.progressBar1.Value = 0;
} }
}

详细的示例可以参看微软 Docs 文档中的 BackgroundWorker 类

再次探讨 WinForms 多线程开发的更多相关文章

  1. .NET基础拾遗(5)多线程开发基础

    Index : (1)类型语法.内存管理和垃圾回收基础 (2)面向对象的实现和异常的处理基础 (3)字符串.集合与流 (4)委托.事件.反射与特性 (5)多线程开发基础 (6)ADO.NET与数据库开 ...

  2. Java多线程开发系列之番外篇:事件派发线程---EventDispatchThread

    事件派发线程是java Swing开发中重要的知识点,在安卓app开发中,也是非常重要的一点.今天我们在多线程开发中,穿插进来这个线程.分别从线程的来由.原理和使用方法三个方面来学习事件派发线程. 一 ...

  3. Java多线程开发系列之四:玩转多线程(线程的控制2)

    在上节的线程控制(详情点击这里)中,我们讲解了线程的等待join().守护线程.本节我们将会把剩下的线程控制内容一并讲完,主要内容有线程的睡眠.让步.优先级.挂起和恢复.停止等. 废话不多说,我们直接 ...

  4. Java进阶(三)多线程开发关键技术

    原创文章,同步发自作者个人博客,转载请务必以超链接形式在文章开头处注明出处http://www.jasongj.com/java/multi_thread/. sleep和wait到底什么区别 其实这 ...

  5. iOS多线程开发

    概览 大家都知道,在开发过程中应该尽可能减少用户等待时间,让程序尽可能快的完成运算.可是无论是哪种语言开发的程序最终往往转换成汇编语言进而解释成机器码来执行.但是机器码是按顺序执行的,一个复杂的多步操 ...

  6. 多线程开发之一 NSThread

    每个 iOS 应用程序都有个专门用来更新显示 UI 界面.处理用户的触摸事件的主线程,因此不能将其他太耗时的操作放在主线程中执行,不然会造成主线程堵塞(出现卡机现象),带来不好的用户体验. 一般的解决 ...

  7. ​结合异步模型,再次总结Netty多线程编码最佳实践

    更多技术分享可关注我 前言 本文重点总结Netty多线程的一些编码最佳实践和注意事项,并且顺便对Netty的线程调度模型,和异步模型做了一个汇总.原文:​​结合异步模型,再次总结Netty多线程编码最 ...

  8. Java多线程开发系列之一:走进多线程

    对编程语言的基础知识:分支.选择.循环.面向对象等基本概念理解后,我们需要对java高级编程有一定的学习,这里不可避免的要接触到多线程开发. 由于多线程开发整体的系统比较大,我会写一个系列的文章总结介 ...

  9. Java之多线程开发时多条件Condition接口的使用

    转:http://blog.csdn.net/a352193394/article/details/39454157 我们在多线程开发中,可能会出现这种情况.就是一个线程需要另外一个线程满足某某条件才 ...

  10. Java多线程开发技巧

    很多开发者谈到Java多线程开发,仅仅停留在new Thread(...).start()或直接使用Executor框架这个层面,对于线程的管理和控制却不够深入,通过读<Java并发编程实践&g ...

随机推荐

  1. 第16天:信息打点-CDN绕过&业务部署&漏洞回链&接口探针&全网扫描&反向邮件

    #CDN配置: 配置1:加速域名-需要启用加速的域名 配置2:加速区域-需要启用加速的地区 配置3:加速类型-需要启用加速的资源 #参考知识: 超级Ping:http://www.17ce.com/ ...

  2. Response状态码

    1.数据是否正常 2.文件是否存在 3.地址自动跳转 4.服务提供错误 注:容错处理识别 •-1xx:指示信息-表示请求已接收,继续处理. •-2xx:成功-表示请求已经被成功接收.理解.接受. •- ...

  3. PasteForm最佳CRUD实践,实际案例PasteTemplate详解之3000问(四)

    无论100个表还是30个表,在使用PasteForm模式的时候,管理端的页面是一样的,大概4个页面, 利用不同操作模式下的不同dto数据模型,通过后端修改对应的dto可以做到控制前端的UI,在没有特别 ...

  4. 如何理解 .Net 中的 委托

    // 委托 // 一种方法的声明和定义,也就是方法的占位符 // 一般使用在 参数 和 属性中 int Add(int a,int b) { return a + b; } // 定义委托的三种方法 ...

  5. C#的函数使用 和参数修饰符 out ref params

    // 函数和方法 // 函数好比对象的动作行为 在定义函数的时候,职责(作用/功能)越单一越好 满足高内聚 低耦合的开发思路 // 变量的命名规则 小驼峰 // 函数的命名规则 大驼峰 动词开头 // ...

  6. JDBC 和 Mybatis

    使用JDBC连接操作数据库 Mybatis是JDBC的二次封装 使用更加简单了

  7. 我们如何在 vue 应用我们的权限

    权限可以分为用户权限和按钮权限: 用户权限,让不同的用户拥有不同的路由映射 ,具体实现方法: 1. 初始化路由实例的时候,只把静态路由规则注入 ,不要注入动态路由规则 : 2. 用户登录的时候,根据返 ...

  8. 2.flask 源码解析:应用启动流程

    目录 一.flask 源码解析:应用启动流程 1.1 WSGI 1.2 启动流程 Flask 源码分析完整教程目录:https://www.cnblogs.com/nickchen121/p/1476 ...

  9. 【技术分析】恶意 SPL 代币识别指南

    背景 在 EVM 生态上,存在各式各样的 ERC20 代币,因其实现方式有着极高的自由度,也催生了花样繁多的恶意代币.这些恶意代币通常会在代码中实现一些恶意的逻辑(禁止用户卖出,特权铸造或销毁等),其 ...

  10. 现代化 React UI 库:Material-UI 详解!

    随着 React 在前端开发中的流行,越来越多的 UI 框架和库开始涌现,以帮助开发者更高效地构建现代化.响应式的用户界面.其中,Material-UI 是基于 Google Material Des ...