不阻塞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

 

如果辅助线程希望在结束时提供更多的反馈信息,而不是简单地给出“Finished!”消息,则
BeginInvoke
过于复杂的使用方法会令人生畏。为了传达其他消息,例如“正在处理”、“一切顺利”等等,需要设法向
UpdateUI 函数传递一个参数。可能还需要添加一个进度栏以提高反馈能力。这么多次调用
BeginInvoke
可能导致辅助线程受该代码支配。这样不仅会造成不便,而且考虑到辅助线程与
UI
的协调性,这样设计也不好。 怎么办呢?使用包装函数!基于上述要求,上面的代码改进如下:

总结

public class MyForm : System.Windows.Forms.Form {

    ...

    public void ShowProgress(string msg, int percentDone) {

        // Wrap the parameters in some EventArgs-derived custom class:

        System.EventArgs e = new MyProgressEvents(msg, percentDone);

        object[] pList = { this, e };

        // Invoke the method. This class is derived

        // from Form, so we can just call BeginInvoke

        // to get to the UI thread.

        BeginInvoke(new MyProgressEventsHandler(UpdateUI), pList);

    }

    private delegate void MyProgressEventsHandler(

        object sender, MyProgressEvents e);

    private void UpdateUI(object sender, MyProgressEvents e) {

        lblStatus.Text = e.Msg;

        myProgressControl.Value = e.PercentDone;

    }

}

 

这里定义了自己的方法,该方法违背了“必须在
UI
线程上进行调用”这一规则,因为它进而只调用不受该规则约束的其他方法。这种技术会引出一个较为常见的话题:为什么不在控件上编写公共方法呢(这些方法记录为
UI 线程规则的例外)?

大家有时间的可以自己根据截图去敲打代码试试,总结如下:

刚好 Control
类为这样的方法提供了一个有用的工具。如果我提供一个设计为可从任何线程调用的公共方法,则完全有可能某人会从
UI 线程调用这个方法。在这种情况下,没必要调用
BeginInvoke,因为我已经处于正确的线程中。调用 Invoke
完全是浪费时间和资源,不如直接调用适当的方法。为了避免这种情况,Control
类将公开一个称为 InvokeRequired 的属性。这是“只限 UI
线程”规则的另一个例外。它可从任何线程读取,如果调用线程是 UI
线程,则返回假,其他线程则返回真。

1.按钮名称[Task] 
 : 
可以实现多个事件注册,但是代码比较多,需要额外的线程等待来结束进度条,而且用户注册的事件的方法更新UI时会报错,提示跨线程操作UI,需要invoke方法调用到UI线程执行。

public void ShowProgress(string msg, int percentDone) {

    if (InvokeRequired) {

        // As before

        ...

    } else {

        // We're already on the UI thread just

        // call straight through.

        UpdateUI(this, new MyProgressEvents(msg,

            PercentDone));

    }

}

2.按钮名称[BeginInvoke] : 
简单方便的异步编程模型,不需要额外的线程等待结束来结束进度条,缺点和按钮名称[Task]一样,用户注册的事件的方法更新UI时会报错,提示跨线程操作UI,需要invoke方法调用到UI线程执行.

ShowProgress
现在可以记录为可从任何线程调用的公共方法。这并没有消除复杂性 — 执行
BeginInvoke
的代码依然存在,它还占有一席之地
。不幸的是,没有简单的方法可以完全摆脱它(郁闷)。

3.按钮名称[Task await] :
稍微有一点点绕,但是简单呀,不需要额外的线程等待UI更新进度条,像同步方法放在await后面即可,而且用户注册的事件方法
更新UI时不需要invoke方法回到UI线程执行。

锁定

 

如果两个线程在同一时间、在同一个位置执行写入操作,则在同步写入操作发生之后,所有从该位置读取数据的线程就有可能看到一堆垃圾数据。为了避免这种问题,必须采取措施来确保一次只有一个线程可以读取或写入某个对象的状态。     
防止这些问题出现所采用的方式是,使用运行时的锁定功能。C#
可以让您利用这些功能、通过锁定关键字来保护代码(Visual Basic
也有类似构造,称为
SyncLock)。规则是,任何想要在多个线程中调用其方法的对象在每次访问其字段时(不管是读取还是写入)都应该使用锁定构造

还是看个例子:

// This field could be modified and read on any thread, so all access 

// must be protected by locking the object.

 

private double myPosition;

•••

public double Position {

    get {

        // Position could be modified from any thread, so we need to lock

        // this object to make sure we get a consistent value.

        lock (this) {

            return myPosition;

        }

    }

    set {

        lock (this) {

            myPosition = value;

        }

    }

}

 

public void MoveBy(double offset) {//这里也要锁

    // Here we are reading, checking and then modifying the value. It is

    // vitally important that this entire sequence is protected by a

    // single lock block.

    lock (this) {

        double newPos = Position + offset;

        // Check within range - MINPOS and MAXPOS

        // will be const values defined somewhere in

        // this class

        if (newPos > MAXPOS) newPos = MAXPOS;

        else if (newPos < MINPOS) newPos = MINPOS;

        Position = newPos;

    }

}

 

当所做的修改比简单的读取或写入更复杂时,整个过程必须由单独的锁语句保护。这也适用于对多个字段进行更新

在对象处于一致状态之前,一定不能释放该锁。如果该锁在更新状态的过程中释放,则其他线程也许能够获得它并看到不一致状态。如果您已经拥有一个锁,并调用一个试图获取该锁的方法,则不会导致问题出现,因为单独线程允许多次获得同一个锁。对于需要锁定以保护对字段的低级访问和对字段执行的高级操作的代码,这非常重要。

死锁

 

       先看例子:

public class Foo {

    public void CallBar() {

        lock (this) {

            Bar myBar = new Bar ();

            myBar.BarWork(this);

        }

    }

 

    // This will be called back on a worker thread

    public void FooWork() {

        lock (this) {

            // do some work

            •••

        }

    }

}

 

public class Bar {

    public void BarWork(Foo myFoo) {

        // Call Foo on different thread via delegate.

        MethodInvoker mi = new MethodInvoker(

            myFoo.FooWork);

        IAsyncResult ar = mi.BeginInvoke(null, null);

        // do some work

        •••

        // Now wait for delegate call to complete (DEADLOCK!)

        mi.EndInvoke(ar);

    }

}

 

         有两个或更多线程都被阻塞以等待对方进行。这里的情形和标准死锁情况还是有些不同,后者通常包括两个锁。这表明如果有某个因果性(过程调用链)超出线程界限,就会发生死锁,即使只包括一个锁!Control.Invoke 是一种跨线程调用过程的方法,这是个不争的重要事实。BeginInvoke 不会遇到这样的问题,因为它并不会使因果性跨线程。实际上,它会在某个线程池线程中启动一个全新的因果性,以允许原有的那个独立进行。然而,如果保留 BeginInvoke 返回的
IAsyncResult,并用它调用 EndInvoke,则又会出现问题,因为 EndInvoke 实际上已将两个因果性合二为一。避免这种情况的最简单方法是,当持有一个对象锁时,不要等待跨线程调用完成。要确保这一点,应该避免在锁语句中调用** Invoke 或
EndInvoke**。其结果是,当持有一个对象锁时,将无需等待其他线程完成某操作。要坚持这个规则,说起来容易做起来难。

 

最佳规则是,根本不调用 Control.Invoke 和
EndInvoke。这就是为什么“启动后就不管”的编程风格更可取的原因,也是为什么 Control.BeginInvoke 解决方案通常比 Control.Invoke 解决方案好的原因。
只要可能,在持有锁时就应该避免阻塞,因为如果不这样,死锁就难以消除。

 

使其简单

 

       到这里,我还是晕晕的,有个问题:如何既从多线程获益最大,又不会遇到困扰并发代码的棘手错误呢?

UI 代码的性质是:它从外部资源接收事件,如用户输入。它会在事件发生时对其进行处理,但却将大部分时间花在了等待事件的发生。如果可以构造辅助线程和 UI 线程之间的通信,使其适合该模型,则未必会遇到这么多问题,因为不会再有新的东西引入。

这样使事情简单化的:将辅助线程视为另一个异步事件源。如同 Button 控件传递诸如
Click 和 MouseEnter 这样的事件,可以将辅助线程视为传递事件(如 ProgressUpdate 和
WorkComplete)的某物。只是简单地将这看作一种类比,还是真正将辅助对象封装在一个类中,并按这种方式公开适当的事件,这完全取决于您。后一种选择可能需要更多的代码,但会使用户界面代码看起来更加统一。不管哪种情况,都需要 Control.BeginInvoke 在正确的线程上传递这些事件。

对于辅助线程,最简单的方式是将代码编写为正常顺序的代码块。但如果想要使用刚才介绍的“将辅助线程作为事件源”模型,那又该如何呢?这个模型非常适用,但它对该代码与用户界面的交互提出了限制:这个线程只能向 UI 发送消息,并不能向它提出请求。

例如,让辅助线程中途发起对话以请求完成结果需要的信息将非常困难。如果确实需要这样做,也最好是在辅助线程中发起这样的对话,而不要在主 UI 线程中发起。该约束是有利的,因为它将确保有一个非常简单且适用于两线程间通信的模型—在这里简单是成功的关键。这种开发风格的优势在于,在等待另一个线程时,不会出现线程阻塞。这是避免死锁的有效策略

相关文章

发表评论

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

*
*
Website