再次探讨 WinForms 多线程开发

再次探讨 WinForms 多线程开发

WinForms 已经开源,您如今能够在 GitHub 上查看 WinForm 源代码git

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

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

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

LongTimeWork 表明一个须要长时间操做才能完成的任务。这里经过 Sleep() 来模拟长事件的操做。api

主要特性:安全

  • 经过事件 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 已经作了良好的封装,咱们须要知道的是关键的时间点。ui

窗体的构造函数和 Load 事件

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

它们的关键区别在于窗体背后所对应的 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 类