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#并行编程(5):需要知道的异步的更多相关文章
- Task C# 多线程和异步模型 TPL模型 【C#】43. TPL基础——Task初步 22 C# 第十八章 TPL 并行编程 TPL 和传统 .NET 异步编程一 Task.Delay() 和 Thread.Sleep() 区别
Task C# 多线程和异步模型 TPL模型 Task,异步,多线程简单总结 1,如何把一个异步封装为Task异步 Task.Factory.FromAsync 对老的一些异步模型封装为Task ...
- C#~异步编程再续~大叔所理解的并行编程(Task&Parallel)
返回目录 并行这个概念出自.net4.5,它被封装在System.Threading.Tasks命名空间里,主要提供一些线程,异步的方法,或者说它是对之前Thread进行的二次封装,为的是让开发人员更 ...
- .Net中的并行编程-4.实现高性能异步队列
上文<.Net中的并行编程-3.ConcurrentQueue实现与分析>分析了ConcurrentQueue的实现,本章就基于ConcurrentQueue实现一个高性能的异步队列,该队 ...
- python网络编程基础(线程与进程、并行与并发、同步与异步、阻塞与非阻塞、CPU密集型与IO密集型)
python网络编程基础(线程与进程.并行与并发.同步与异步.阻塞与非阻塞.CPU密集型与IO密集型) 目录 线程与进程 并行与并发 同步与异步 阻塞与非阻塞 CPU密集型与IO密集型 线程与进程 进 ...
- Python并行编程(十四):异步编程
1.基本概念 除了顺序执行和并行执行的模型以外,还有异步模型,这是事件驱动模型的基础.异步活动的执行模型可以只有一个单一的主控制流,能在单核心系统和多核心系统中运行. 在并发执行的异步模型中,许多任务 ...
- C#并行编程-Task
菜鸟学习并行编程,参考<C#并行编程高级教程.PDF>,如有错误,欢迎指正. 目录 C#并行编程-相关概念 C#并行编程-Parallel C#并行编程-Task C#并行编程-并发集合 ...
- C#的变迁史 - C# 5.0 之并行编程总结篇
C# 5.0 搭载于.NET 4.5和VS2012之上. 同步操作既简单又方便,我们平时都用它.但是对于某些情况,使用同步代码会严重影响程序的可响应性,通常来说就是影响程序性能.这些情况下,我们通常是 ...
- .Net中的并行编程-6.常用优化策略
本文是.Net中的并行编程第六篇,今天就介绍一些我在实际项目中的一些常用优化策略. 一.避免线程之间共享数据 避免线程之间共享数据主要是因为锁的问题,无论什么粒度的锁 ...
- .Net中的并行编程-5.流水线模型实战
自己在Excel整理了很多想写的话题,但苦于最近比较忙(其实这是借口).... 上篇文章<.Net中的并行编程-4.实现高性能异步队列>介绍了异步队列的实现,本篇文章介绍我实际工作者遇到了 ...
随机推荐
- keystone v3.0与2.0的区别
参看http://blog.chinaunix.net/uid-21335514-id-3497996.html
- Openstack 网络服务 Neutron计算节点部署(十)
Neutron计算节点部署 安装组件,安装的服务器是192.168.137.12 1.安装软件包 yum install -y openstack-neutron-linuxbridge ebtabl ...
- Spring MVC 中 @ModelAttribute 注解的妙用
Spring MVC 中 @ModelAttribute 注解的妙用 Spring MVC 提供的这种基于注释的编程模型,极大的简化了 web 应用的开发.其中 @Controller 和 @Rest ...
- JavaScript 中创建三种消息框:警告框、确认框、提示框。
网址:http://www.w3school.com.cn/js/js_popup.asp 警告框 警告框经常用于确保用户可以得到某些信息. 当警告框出现后,用户需要点击确定按钮才能继续进行操作. 语 ...
- 第7月第26天 iOS httpserver
1. cocoahttpserver 1)httpserver [httpServer start:&error] 2)HTTPConnection [newConnection start] ...
- python垃圾回收三之标记清除
#第一组循环引用# a = [1,2] b = [3,4] a.append(b) b.append(a) del a ## #第二组循环引用# c = [4,5] d = [5,6] c.appen ...
- (AC自动机)C - 病毒侵袭持续中
题目链接:https://cn.vjudge.net/contest/280743#problem/C 题目大意:中文题 具体思路:首先取ascii码0-130是肯定不行的了,会超时.然后就开始简化, ...
- js实现避免浏览器拦截弹出新页面的方法
1 问题描述 点击button按钮,提交页面的form表单,后台执行完毕后返回参数,前台页面需要该参数实现跳转,如何实现保留该原来的页面,并在浏览器选项卡新建一个页面,且不被浏览器拦截? 2 方法及问 ...
- 苹果ANCS协议学习【转】
苹果ANCS协议学习 转自:http://www.cnblogs.com/alexcai/p/4321514.html 综述 苹果通知中心(Apple Notification Center Serv ...
- [mysql5.6]主从更换ip之后重新建立同步【转】
情况时这样的: 主从系统 centos6.5 mysql5.6 由于机房迁移ip地址变了,导致原来的主动无法同步,于是需要重新建立主从关系. 主 192.168.1.23 从 192.168.1.22 ...