C#并行编程(5):需要知道的异步
异步与并行的联系
大家知道“并行”是利用CPU的多个核心或者多个CPU同时执行不同的任务,我们不关心这些任务之间的依赖关系。
但是在我们实际的业务中,很多任务之间是相互影响的,比如统计车间全年产量的运算要依赖于各月产量的统计结果。假如你想在计算月产量的时候做些其他事情,如导出生产异常报表,“异步”就可以登上舞台了。
说到异步,必须要先提一下同步。一图胜千言:
图中操作C的执行依赖B的结果,B的执行依赖A的结果。线程1连续执行操作A、B、C便是一个同步过程;相对地,线程1执行完A后把结果给线程2,线程2开始执行B,完成后把B的结果通知到线程1,线程1开始执行C,线程1在等待操作B结果的时候执行了D,这就是一个异步的过程;此外,异步过程中,B和D是并行执行的。
并行会提高业务的执行效率,但异步不会,异步甚至会拖慢业务的执行,比如上面A->B->C的执行过程。异步是让等待变得更有价值,这种价值则体现在多个业务的并行上。
C#中的异步
在需要长时间等待的地方都可以使用异步,比如读写文件、访问网络或者处理图片。特别是在UI线程中,我们要保持界面的响应性,耗时的操作最好都使用异步的方式执行。
.NET提供了三种异步模式:
- IAsyncResult模式(APM)
- 基于事件的异步模式(EAP)
- 基于任务的异步模式(TAP)
其中基于任务的异步模式是.NET推荐的异步编程方式。
IAsyncResult异步模式APM
下面是IAsyncResult基于委托的用法。
/// <summary> /// 做作业的委托 /// </summary> /// <param name="workNo">作业编号</param> private delegate void AsyncWorkCaller(int workNo); public static void Run() { Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} will do some work."); AsyncWorkCaller caller = DoWork; AsyncCallback callback = ar => {// 异步任务完成后的回调,在异步任务的执行线程中执行 Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} did the callback. [{ar.AsyncState}]"); }; IAsyncResult result = caller.BeginInvoke(1, callback, "callback msg"); DoWork(2); //result.AsyncWaitHandle.WaitOne(); caller.EndInvoke(result); DoWork(3); Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} done the work."); } /// <summary> /// 做作业 /// </summary> /// <param name="workNo">作业编号</param> private static void DoWork(int workNo) { Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> work #{workNo} started with thread #{Thread.CurrentThread.ManagedThreadId}."); Thread.Sleep(1000);//模拟耗时 Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> work #{workNo} done with thread #{Thread.CurrentThread.ManagedThreadId}."); }
我们使用
BeginInvoke来异步执行作业1,同时可以执行作业2,调用
EndInvoke的时候,当前线程被阻塞直到作业1完成。我们也可以使用
result.AsyncWaitHandle.WaitOne()来等待异步作业完成,同样会阻塞当前线程。此外,可以为异步作业增加回调,异步作业在完成时会执行回调函数。
基于事件的异步模式EAP
事件大家不会陌生,我们在Winform编程的时候,总会用到事件。下面是利用
BackgroundWorker实现的一个基于事件的简单异步过程。我们给异步对象(这里是BackgroundWorker)订阅
DoWork和
RunWorkCompleted事件,当调用
RunWorkerAsync时,触发异步对象的工作事件,此时会开辟一个新线程来执行目标操作。目标操作完成时,触发工作完成事件,执行后续操作。与
IAsyncResult模式不同的是,作业完成后的后续操作会在另外的一个线程执行,而
IAsyncResult模式中,完成回调会在目标操作的执行线程中执行。
public static class EventBasedAsync { private static readonly BackgroundWorker worker = new BackgroundWorker(); static EventBasedAsync() { worker.DoWork += Worker_DoWork; worker.RunWorkerCompleted += Worker_RunWorkerCompleted; } public static void Run() { Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} will do some work."); worker.RunWorkerAsync(1); DoWork(2); DoWork(3); Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} done the work."); } private static void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {//作业完成后,会开辟新的线程执行指定的操作 Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} did something when work completed."); } private static void Worker_DoWork(object sender, DoWorkEventArgs e) {//作业会运行在新的线程里 DoWork((int)e.Argument); } /// <summary> /// 做作业 /// </summary> /// <param name="workNo">作业编号</param> private static void DoWork(int workNo) { Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> work #{workNo} started with thread #{Thread.CurrentThread.ManagedThreadId}."); Thread.Sleep(3000);//模拟耗时 Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> work #{workNo} done with thread #{Thread.CurrentThread.ManagedThreadId}."); } }
实际上,我们可以利用
AsyncOperationManager实现自己的异步对象,可以使用dnSpy对
BackgroundWorker进行反编译观察具体的实现过程。
基于任务的异步模式TAP
在《C#并行编程(4):基于任务的并行》中,我们已经总结过
Task和
Task<T>的用法,这里主要关注的是C#的
async/await语法与
Task的结合用法。
在C#中,我们使用async标记定义一个异步方法,使用await来等待一个异步操作。简单的用法如下:
public async Task DoWorkAsync() { await Task.Delay(1000); } public async Task<int> DoWorkAndGetResultAsync() { await Task.Delay(1000); return 1; }
用
async/await编写异步过程很方便,但异步方法的执行过程是怎样呢?下面的例子展示了一个异步操作的调用过程,我们以这个例子来分析异步方法的调用过程。
public static async Task Run() { Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} will do some work."); Task workTask1 = DoWork(1); // 不使用await调用的异步方法,与正常方法一样 //await workTask1; Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} got task #{workTask1.Id} by async call."); Task workTask2 = DoWork(2); await workTask2; Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} got task #{workTask2.Id} by async call."); Task workTask3 = DoWork(3); await workTask3; Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} got task #{workTask3.Id} by async call."); Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} done the work."); } /// <summary> /// 异步作业 /// </summary> /// <param name="workNo">作业编号</param> /// <returns>异步任务</returns> private static async Task DoWork(int workNo) { Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> work #{workNo} started with thread #{Thread.CurrentThread.ManagedThreadId}."); DateTime now = DateTime.Now; await Task.Run(() => { Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> work #{workNo} was running by task #{Task.CurrentId} with thread #{Thread.CurrentThread.ManagedThreadId}."); while (now.AddMilliseconds(3000) > DateTime.Now) {// 模拟计算过程 } }); Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> work #{workNo} done with thread #{Thread.CurrentThread.ManagedThreadId}."); }
先来看一下例子的输出:
19:07:33.032779=> thread #10 will do some work.
19:07:33.039762=> work #1 started with thread #10.
19:07:33.075664=> thread #10 got task #2 by async call.
19:07:33.075664=> work #2 started with thread #10.
19:07:33.078658=> work #2 was running by task #3 with thread #11.
19:07:33.082647=> work #1 was running by task #1 with thread #6.
19:07:36.040739=> work #1 done with thread #6.
19:07:36.077638=> work #2 done with thread #11.
19:07:36.077638=> thread #11 got task #4 by async call.
19:07:36.077638=> work #3 started with thread #11.
19:07:36.077638=> thread #11 got task #7 by async call.
19:07:36.077638=> thread #11 done the work.
19:07:36.077638=> work #3 was running by task #6 with thread #12.
19:07:39.077652=> work #3 done with thread #12.
在上面的输出中,我们单看work #1,它由thread #10启动,计算过程在thread #6中执行并结束,最后任务在thread #10中返回,这里我们没有使用
await来等待work #1的异步任务;假如我们使用
await等待异步任务,如work #2,它在thread #10中启动,计算过程在thread #11中执行并结束,任务最后在thread #11中返回。大家可能发现了两者的不同:
await改变了
Run()方法的执行线程,从
DoWork()方法的执行也能够看出,
await会改变异步方法的执行线程!
实际上,编译器会把异步方法转换成状态机结构,执行到
await时,编译器把当前正在执行方法(任务)挂起,当await的任务执行完成时,编译器再恢复挂起的方法,所以我们的输出中,异步方法
await前面和后面的代码,一般是在不同的线程中执行的。编译器通过这种状态机的机制,使得等待异步操作的过程中线程不再阻塞,进而增强响应性和线程利用率。
理解异步方法的执行机制后,相信对异步的应用会变得更加娴熟,这里就不再总结异步的具体用法。
- 【面向对象语言系列】关于 C# 数据可视化,你需要知道的事
- 【面向对象语言系列】关于 C# XML处理,你需要知道的事
- 微软缘何“.NET研究”认为VB与C#需要异步语法
- 【面向对象语言系列】关于 C# 异步编程, 你需要知道的事
- !!c# 委托与多线程 -- 相比background来说,本方法针对当需要通过异步代理发起UI操作,而后线程中进行sleep操作的时使用
- 【面向对象语言系列】关于 C# 事件处理,你需要知道的事
- 【面向对象语言系列】关于 C# 错误处理,你需要知道的事
- 【面向对象语言系列】关于 C# 数据库,你需要知道的事
- VS2015中C#版本6.0的新特性 你需要知道
- 使用C#异步调用WebServices时需要注意的问题
- 【面向对象语言系列】关于 C# 调试,你需要知道的事
- C#实现多级子目录Zip压缩解压实例 NET4.6下的UTC时间转换 [译]ASP.NET Core Web API 中使用Oracle数据库和Dapper看这篇就够了 asp.Net Core免费开源分布式异常日志收集框架Exceptionless安装配置以及简单使用图文教程 asp.net core异步进行新增操作并且需要判断某些字段是否重复的三种解决方案 .NET Core开发日志
- c#需要知道的25个基本概念
- 【面向对象语言系列】关于 C# 数据可视化,你需要知道的事
- c#使用foreach需要知道的
- C# 一些自己需要知道的事情
- 【面向对象语言系列】关于C#设计模式,你需要知道的事
- 【面向对象语言系列】关于 C# JSON处理,你需要知道的事
- VS2015中C#版本6.0的新特性 你需要知道
- 【面向对象语言系列】关于 C# 测试,你需要知道的事