上一篇文章:多线程和异步的区别

同步:执行一个耗时的操作时,主线程就会一直等待,直到该操作执行完成,我们大部分程序都是同步操作。例如,你给朋友发了一个消息:一起玩王者农药,但是此时你朋友正在游戏中,你为了他能带你躺赢,你就一直等待他把这局打完,然后一起开黑。

异步:执行一个耗时的操作时,主线程不会等待,会继续执行下面的操作,当耗时的操作完成时,它会通过回调的方式告诉主线程我执行完了,然后主程序在回调函数里继续后续的操作。例如,你给朋友发了一个消息:一起玩王者农药,但是此时你朋友正在游戏中,虽然他是大神,你为了证明自己不是坑,不用他带依然可以上分,果断单独开了一局,等你朋友虐菜完成,通知你说,我打完了,要不要我带你飞。

IO操作

IO流操作使我们程序开发中很常见的与硬盘的交互方式,如: 上面代码中Data.dxf文件很大,fs.Read读取过程中,我们只能一直等待Read方法执行完,才能打印“End”。程序主界面一定会处于卡死状态,用户体验较差,此时我们就需要用到异步方式去读取文件流。如何去执行异步操作呢?

BeginXXX / EndXXX

.Net早期,采用BeginXXX/EndXXX方式实现异步,你在使用这些方法的时候,你就在使用异步编程模型来编写程序。异步编写模型是一种模式,该模式允许用更少的线程去做更多的操作,.NET Framework很多类也实现了该模式,例如上面的FileStream(实现返回类型为IAsyncResult接口的BeginXXX和EndXXX方法),委托也定义了BeginInvoke和EndInvoke的方法。经常看到和使用类似BeginXXX和EndXXX方法,即是采用APM即异步编程模型(Asynchronous Programming Model

我们先抛开IO的例子,先看一个委托的同步方法实例。

调用Invoke方法,主线程就会等待DoSomething执行完,再继续往下执行。

现在换成BeginInvoke再打印一下结果

调用BeginInvoke后,主线程根本不会等待DoSomething执行完成,就打印了“go on!”。还有一点,DoSomething方法里面的线程是线程池提供的线程,也就是BeginInvoke的异步操作是基于线程池实现的。

AsyncCallback、IAsyncResult

在上面代码中我们可以看到BeginInvoke方法里面有两个参数为null,这两个参数难道没作用吗?非也。看一下BeginInvoke的方法参数构造。

第一个参数:DoSomething方法中的参数,如果DoSomething方法没有参数,则BeginInvoke就只有后面两个参数

第二个参数:AsyncCallback callback,看一下AsyncCallback这是个什么东东?

哦,原来是一个委托,没有返回值,接收一个IAsyncResult类型的参数。

上面用Lambda表达式实例化一个AsyncCallback,如果对lambda表达式不是很懂,请看这篇文章:Lambda表达式和Linq

从结果中我们看到:最后一句是在异步完成之后打印的,其实这就是我们异步回调后的操作。你可能会问这个ac代表了什么,从委托AsyncCallback的参数中我们可以推断出ac就是IAsyncResult类型的实例。但是这个IAsyncResult的实例从哪里来的呢?也没见传进来就执行通过了。

BeginInvoke方法的返回值是iAsyncResult,上面代码输出结果为”true”,所以ac就是BeginInvoke的返回值iAsyncResult,AsyncCallback这个委托就是执行异步后续的操作。

再来看一下IAsyncResult接口构造

IAsyncResult存储了异步方法的相关状态信息,其中AsyncState属性存储了BeginInvoke方面里面第三个参数。下面我们自己写一个简单的两个数字相加异步调用的例子

自定义IAsyncResult

调用类WydAsyncTask

Main方法调用

同步调用可以直接返回Int结果,异步调用就需要调用EndAsyncTask方法才能取得Int结果。回调函数里面ac.AsyncState的值就是传入的callbacking。

对于访问异步操作的结果,APM提供了四种方式供开发人员选择:

  1. 在调用BeginXxx方法的线程上调用EndXxx方法来得到异步操作的结果,但是这种方式会阻塞调用线程,知道操作完成之后调用线程才继续运行
  2. 查询IAsyncResult的AsyncWaitHandle属性,从而得到WaitHandle,然后再调用它的WaitOne方法来使一个线程阻塞并等待操作完成再调用EndXxx方法来获得操作的结果。
  3. 循环查询IAsyncResult的IsComplete属性,操作完成后再调用EndXxx方法来获得操作返回的结果。
  4. 使用 AsyncCallback委托来指定操作完成时要调用的方法,在操作完成后调用的方法中调用EndXxx操作来获得异步操作的结果。

第一种方式:上面Main方法里面的写法,在BeginXXX后面写EndXXX。

第二种方式:WaitOne()

第三种方式:轮询

第四种方式:回调函数中执行(推荐)

看到这,文章最开始的FileStream类中的BeginRead应该知道怎么实现的了吧。

实例

做过Winform的猿们肯定遇到过一个问题:多线程访问Winform的控件报错的问题,下面还原一下报错场景。

窗体创建一个Label控件,在窗体Load方法里面通过委托的异步调用把当前时间赋值给Label控件,同时把当前线程唯一标识Id打印一下。调试运行一下,错误信息如下图:

错误信息:线程间操作无效,从不是创建控件”lblShow”的线程访问它。什么意思呢?

也就是说控件”lblShow”不允许不是创建它的线程访问,“lblShow”是主线程创建Action委托通过线程池新开另一个线程去操作”lblShow”控件是不允许的。从上面的打印结果也能看出两者不是同一个线程。

如何去解决这个问题呢?

Winform程序遇到耗时任务时,让用户看着卡死的界面等待耗时任务不可取,我们可以通过另开一个线程去执行这个耗时任务,把主线程从耗时任务中解脱出来。

但是为了保证线程安全,框架不允许你新开的线程直接去操作控件,既然控件只能有主线程访问,所以只有一条路,”lblShow”控件的赋值重任还得落在主线程身上。

那还得从”lblShow”控件入手,看看微软给我们提供了什么?查看Control基类发现,每一个控件都有一个Invoke方法。方法构造如下:

此方法参数需要传入一个委托,那我们现在写写看:

此时再去调试发现,上面的错误没有了,为什么呢?看一下打印结果,在UpdateControl方法里面的线程Id不就是主线程的Id吗,主线程(创建控件线程)去操作”lblShow”控件当然不会报上面的跨线程访问控件的错误信息了。所以可以看出调用控件的Invoke方法,作为参数的委托内部方法执行是由主线程来完成的。

上面的例子只是简单的打印下当前时间,并没有体会到耗时。下面我们加一个while(true)循环,让”lblShow”控件的时间动起来,这个循环代码写到哪里呢?写到UpdateControl里面?稍微想一下就可以否决了,这个方法里面是主线程在操作,里面加个死循环,那界面永远处于卡死状态。所以我们把while(true)语句加在新开辟的线程里面。代码如下:

再贴出一个Thread写法(Thread后续文章会讲)

此时我们的时间就可以愉快的跑起来了,即不卡界面,也不存在跨线程操作控件的问题。

希望对大家有一点帮助,文中如有错误,烦劳您不吝指出,谢谢!

 

如果转载,请给出原文链接,谢谢!


微信支付宝

如果本文帮助到您,请随意打赏作者