图片 8

C#.NET使用Task,await,async,异步执行控件耗时事件(event),不阻塞UI线程和不跨线程执行UI更新,以及其他方式比较

使用Task,await,async,异步执行事件(event),不阻塞UI线程和不跨线程执行UI更新

为什么选择多线程?

  使用Task,await,async 的异步模式 去执行事件(event)
解决不阻塞UI线程和不夸跨线程执行UI更新报错的最佳实践,附加几种其他方式比较

多线程处理可以使您能够通过确保程序“永不睡眠”从而保持 UI 的快速响应。

由于是Winform代码和其他原因,本文章只做代码截图演示,不做界面UI展示,当然所有代码都会在截图展示。

在多线程下,耗时较长的任务就可以在其自己的线程中运行,这些线程通常称为辅助线程。因为只有辅助线程受到阻止,所以阻塞操作不再导致用户界面冻结。

 

其基本原则是,负责响应用户输入和保持用户界面为最新的线程(通常称为 UI 线程)不应该用于执行任何耗时较长的操作。惯常做法是,任何耗时超过 30ms 的操作都要考虑从 UI 线程中移除。

1:封装异步按钮(为了比较放了3个按钮)和进度条的控件,包含基本文件演示截图

如果想让用户界面保持响应迅速,则任何阻塞操作都应该在辅助线程中执行—不管是机械等待某事发生(例如,等待 CD-ROM 启动或者硬盘定位数据),还是等待来自网络的响应。

1.1 演示工程截图图片 1 1.2按钮和进度条控件演示 图片 2

 

 

异步委托调用

2:定义异步委托和事件和几种演示封装

在辅助线程中运行代码的最简单方式是使用异步委托调用(所有委托都提供该功能)。委托通常是以同步方式进行调用,即,在调用委托时,只有包装方法返回后该调用才会返回。要以异步方式调用委托,请调用 BeginInvoke 方法,这样会对该方法排队以在系统线程池的线程中运行。调用线程会立即返回,而不用等待该方法完成。这比较适合于 UI 程序,因为可以用它来启动耗时较长的作业,而不会使用户界面反应变慢。

2.1
定义相关事件图片 3
解析:最前面的是普通的事件定义,后面2行是异步定义。

在以下代码中,System.Windows.Forms.MethodInvoker 类型是一个系统定义的委托,用于调用不带参数的方法。

 

private void StartSomeWorkFromUIThread () {

    // The work we want to do is too slow for the UI

    // thread, so let's farm it out to a worker thread.

 

    MethodInvoker mi = new MethodInvoker(

        RunsOnWorkerThread);

    mi.BeginInvoke(null, null); // This will not block.

}

 

// The slow work is done here, on a thread

// from the system thread pool.

private void RunsOnWorkerThread() {

    DoSomethingSlow();

}

如果想要传递参数,可以选择合适的系统定义的委托类型,或者自己来定义委托。

2.2 按钮名称[Task]执行普通异步Task

调用 BeginInvoke 会使该方法在系统线程池的线程中运行,而不会阻塞 UI
线程以便其可执行其他操作。
如果您需要该方法返回的结果,则 BeginInvoke
的返回值很重要,并且您可能不传递空参数。
然而,对于大多数 UI 应用程序而言,这种“启动后就不管”的风格是最有效的。
应该注意到,BeginInvoke 将返回一个 IAsyncResult。这可以和委托的
EndInvoke 方法一起使用,

图片 4

以在该方法调用完毕后检索调用结果。

解析调用过程:当用户点击按钮时会加载所有用户注册的事件进行多线程分发,单独每一个委托进行执行,最后单独使用线程进行等待,这样不阻塞UI线程。

 

但是用户注册的事件方法如果有更新UI会报错,需要额外的Invoke进行处理。

线程和控件

 

 Windows 窗体中最重要的一条线程规则:除了极少数的例外情况,否则都不要在它的创建线程以外的线程中使用控件的任何成员。规则的结果是一个被包含的控件(如,包含在一个表单中的按钮)必须与包含它控件位处于同一个线程中。也就是说,一个窗口中的所有控件属于同一个 UI 线程。大部分
Windows 窗体应用程序最终都只有一个线程,所有 UI 活动都发生在这个线程上。这个线程通常称为 UI 线程。这意味着您不能调用用户界面中任意控件上的任何方法,除非在该方法的文档说明中指出可以调用。

 

注意,以下代码是非法的:

2.3 按钮名称[BeginInvoke]执行普通异步

// Created on UI thread

private Label lblStatus;

...

// Doesn't run on UI thread

private void RunsOnWorkerThread() {

    DoSomethingSlow();

    lblStatus.Text = "Finished!";    // BAD!!

}

这就是多线程错误中的主要问题,即它们并不会立即显现出来。甚至当出现了一些错误时,在第一次演示程序之前一切看起来也都很正常。

图片 5

 

解析调用过程:这个调用过程和Task一样,但是简单,这个也可以写成多事件注册,多多领会异步编程模型的好处(原理:异步执行,内部等待信号通知结束)。

在正确的线程中调用控件

 

 

 

理论上讲,可以使用低级的同步原理和池化技术来生成自己的机制,但幸运的是,因为有一个以 Control 类的
Invoke 方法形式存在的解决方案,所以不需要借助于如此低级的工作方式。

2.4 (推荐)按钮名称[Task await]执行方便的异步耗时操作和简单的UI

Invoke 方法是 Control
类中少数几个有文档记录的线程规则例外之一:它始终可以对来自任何线程的
Control 进行 Invoke 调用。Invoke
方法本身只是简单地携带委托以及可选的参数列表,并在 UI
线程中为您调用委托,而不考虑 Invoke
调用是由哪个线程发出的。实际上,为控件获取任何方法以在正确的线程上运行非常简单。但应该注意,只有在
UI 线程当前未受到阻塞时
,这种机制才有效 — 调用只有在 UI
线程准备处理用户输入时才能通过。Invoke
方法会进行测试以了解调用线程是否就是 UI
线程。如果是,它就直接调用委托。否则,它将安排线程切换,并在 UI
线程上调用委托。无论是哪种情况,委托所包装的方法都会在 UI
线程中运行,并且只有当该方法完成时,Invoke 才会返回。

图片 6

Control 类也支持异步版本的
Invoke,它会立即返回并安排该方法以便在将来某一时间在 UI
线程上运行。这称为
BeginInvoke,它与异步委托调用很相似,与委托的明显区别在于:委托调用以异步方式在线程池的某个线程上运行,BeginInvoke以异步方式在
UI 线程上运行。
Control 的 Invoke、BeginInvoke 和 EndInvoke 方法,以及 InvokeRequired
属性都是 ISynchronizeInvoke
接口的成员。该接口可由任何需要控制其事件传递方式的类实现。由于
BeginInvoke 不容易造成死锁,所以尽可能多用该方法;而少用 Invoke
方法。
因为 Invoke 是同步的,所以它会阻塞辅助线程,直到 UI
线程可用。

解析调用过程:推荐的方式附加调用流程图片 7

回顾一下前面的代码。首先,必须将一个委托传递给
Control 的 BeginInvoke 方法,以便可以在 UI
线程中运行对线程敏感的代码。这意味着应该将该代码放在它自己的方法中。(前面所展示的代码片段的合法版本)

 这个全是优点啊:代码精简,异步执行方法可以像同步的方式来调用,用户注册的事件方法可以随意更新UI,无需invoke,稍微改造一下就能多事件注册。

// Created on UI thread

private Label lblStatus;

•••

// Doesn't run on UI thread

private void RunsOnWorkerThread() {

    DoSomethingSlow();

    // Do UI update on UI thread

    object[] pList = { this, System.EventArgs.Empty };

    lblStatus.BeginInvoke(

      new System.EventHandler(UpdateUI), pList);

}

•••

// Code to be run back on the UI thread

// (using System.EventHandler signature

// so we don't need to define a new

// delegate type here)

private void UpdateUI(object o, System.EventArgs e) {

    // Now OK - this method will be called via

    // Control.Invoke, so we are allowed to do

    // things to the UI.

    lblStatus.Text = "Finished!";

}

 

 

3:其他用户调用封装好的异步按钮执行耗时操作

一旦辅助线程完成缓慢的工作后,它就会调用
Label 中的 BeginInvoke,以便在其 UI
线程上运行某段代码。通过这样,它可以更新用户界面。

 图片 8

包装 Control.Invoke

发表评论

电子邮件地址不会被公开。 必填项已用*标注