.net异步编程的前世今生
.NET Framework 提供了执行异步操作的三种模式:
- 异步编程模型 (APM,Asynchronous Programming Model) 模式(也称 IAsyncResult 模式),在此模式中异步操作需要 Begin 和 End 方法(比如用于异步写入操作的 BeginWrite 和 EndWrite)。 对于新的开发工作不再建议采用此模式
- 基于事件的异步模式 (EAP,Event-based Asynchronous Pattern),这种模式需要 Async 后缀,也需要一个或多个事件、事件处理程序委托类型和 EventArg 派生类型。 EAP 是在 .NET Framework 2.0 中引入的。 对于新的开发工作不再建议采用此模式。
- 基于任务的异步模式 (TAP, Task-based Asynchronous Pattern) 使用一种方法来表示异步操作的启动和完成。 TAP 是在 .NET Framework 4 中引入的,并且它是在 .NET Framework 中进行异步编程的推荐使用方法。 C# 中的 async 和 await 关键词以及 Visual Basic 语言中的 Async 和 Await 运算符为 TAP 添加了语言支持。
现有一个从指定偏移量处起将指定量数据读取到提供的缓冲区中的Read方法:
public class MyClass { public int Read(byte [] buffer, int offset, int count); }
APM:
public class MyClass { public IAsyncResult BeginRead( byte [] buffer, int offset, int count, AsyncCallback callback, object state); public int EndRead(IAsyncResult asyncResult); }
EAP:
public class MyClass { public void ReadAsync(byte [] buffer, int offset, int count); public event ReadCompletedEventHandler ReadCompleted; }
TAP:
public class MyClass { public TaskReadAsync(byte [] buffer, int offset, int count); }
一个例子
同步版
private void btnOldDownload_Click(object sender, EventArgs e) { using(WebClient wc = new WebClient()) { // 我们尝试去下载 python 的安装包。 wc.DownloadFile("https://www.python.org/ftp/python/3.5.2/python-3.5.2-amd64.exe", "python.exe"); } lbMessage.Text = "下载完成。"; }
EAP版
private void OldAsyncDownload_Click(object sender, EventArgs e) { using (WebClient wc = new WebClient()) { // 我们尝试去下载 python 的安装包。 // 下载完成时会有事件通知。 wc.DownloadFileCompleted += Wc_DownloadFileCompleted; wc.DownloadFileAsync(new Uri("https://www.python.org/ftp/python/3.5.2/python-3.5.2-amd64.exe"), "python.exe"); } } private void Wc_DownloadFileCompleted(object sender, AsyncCompletedEventArgs e) { lbMessage.Text = "下载完成。"; }
一个简单的下载逻辑被分隔到了两个方法中,在第一个方法中挂载 DownloadFileCompleted 事件,然后启动下载。下载完成后通过 DownloadFileCompleted 事件处理函数进行通知。
TAP版:
private async void btnMyAsync_Click(object sender, EventArgs e) { using (WebClient wc = new WebClient()) { // 我们尝试去下载 python 的安装包。 Task task = wc.DownloadFileTaskAsync("https://www.python.org/ftp/python/3.5.2/python-3.5.2-amd64.exe", "python.exe"); // 可以在这里执行代码。 await task; } lbMessage.Text = "下载完成。"; }
前台线程和后台线程
.Net的公用语言运行时(Common Language Runtime,CLR)能区分两种不同类型的线程:前台线程和后台线程。这两者的区别就是:应用程序必须运行完所有的前台线程才可以退出;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。
.Net环境使用Thread建立的线程默认情况下是前台线程,即线程属性IsBackground=false,在进程中,只要有一个前台线程未退出,进程就不会终止。主线程就是一个前台线程。而Task.Run使用的是后台线程。后台线程不管线程是否结束,只要所有的前台线程都退出(包括正常退出和异常退出)后,进程就会自动终止。一般后台线程用于处理时间较短的任务,如在一个Web服务器中可以利用后台线程来处理客户端发过来的请求信息。而前台线程一般用于处理需要长时间等待的任务,如在Web服务器中的监听客户端请求的程序,或是定时对某些系统资源进行扫描的程序。
需要明白的概念性问题:
1. 线程是运行在进程上的,进程都结束了,线程也就不复存在了!
2. 只要有一个前台线程未退出,进程就不会终止!即说的就是程序不会关闭!(即在资源管理器中可以看到进程未结束。)
核心:Task
Task和Thread的区别:
1.Task是架构在线程之上的,也就是说任务最终还是要抛给线程去执行。
2.Task底层是使用线程池的,而Thread每次实例化都会创建一个新的线程
Task的状态
成员名称 | 说明 | |
---|---|---|
Canceled |
|
|
Created |
|
|
Faulted |
|
|
RanToCompletion |
|
|
Running |
|
|
WaitingForActivation |
|
|
WaitingForChildrenToComplete |
|
|
WaitingToRun |
|
Task的生命周期
async和await
定义一个异步方法
- 方法的声明中有async修饰符。
- 按照约定,方法名字里有Async后缀。
- 返回值类型是下面三种情况:
- 方法通常包含至少一个await表达式。
- await针对的是Task对象
- main方法不能加async关键字,因此在main方法中也不能用await,但是可以调用Task.Wait()或Task.Result()获得结果
* Task,当方法里没有return语句或者return语句没有操作数。
* Task\
* void,编写的是一个异步的event handler。
// 声明中的要点: // - async关键字。 // - 返回值类型是Task或者Task, // - 方法的名字以Async结尾,这个是命名约定,不是语法要求。 public static async Task<int> RequestWebPageAsync() { HttpClient client = new HttpClient(); Task getTask = client.GetAsync("http://www.helpsd.net"); // 这里可以处理一些不依赖HttpResponseMessage的工作 //DoSomeWork // await 运算符暂停了RequestWebPageAsync // - getTask完成之前,RequestWebPageAsync不再执行, // - 剩余的部分作为getTask的后续task(continuation task), // - 类似于调用了getTask的ContinueWith方法。 // - 同时控制权返回给RequestWebPageAsync的调用者。 // - getTask完成之后,continuation task自动启动,恢复执行 // - await运算符获得getTask的结果。 HttpResponseMessage response = await getTask; byte[] contents = await response.Content.ReadAsByteArrayAsync(); //return语句指定了一个int类型的返回值,这决定了整个方法的返回值是 //Task<int>,如果没有返回值,则整个方法返回值是Task。 return contents.Length; }
async await的执行顺序:
class Program { static void Main(string[] args) { Console.WriteLine("-------main begin-------"); Console.WriteLine("main current threadID:" + Thread.CurrentThread.ManagedThreadId); Tasktask = GetAsync(); Console.WriteLine("Main do things"); // Console.WriteLine("Task return" + task.Result); Console.WriteLine("-------main end-------"); Console.ReadLine(); } static async Task GetLengthAsync() { Console.WriteLine("GetLengthAsync start"); Console.WriteLine("GetLengthAsync current threadID:" + Thread.CurrentThread.ManagedThreadId); Task task = GetStringAsync(); Console.WriteLine("GetLengthAsync current threadID:" + Thread.CurrentThread.ManagedThreadId); Console.WriteLine("GetLengthAsync inprogress"); string str = await task; Console.WriteLine("GetLengthAsync current threadID:" + Thread.CurrentThread.ManagedThreadId); Console.WriteLine("GetLengthAsync end"); return str.Length; } static async Task GetAsync() { Console.WriteLine("GetAsync start"); Console.WriteLine("GetAsync current threadID:" + Thread.CurrentThread.ManagedThreadId); Task t = GetLengthAsync(); Console.WriteLine("GetAsync inprogress"); var result = await t; return result; } static Task GetStringAsync() { Console.WriteLine("GetString Async current threadID:" + Thread.CurrentThread.ManagedThreadId); return Task.Run(() => { Console.WriteLine("task current threadID:" + Thread.CurrentThread.ManagedThreadId); return "finished"; }); } }
从执行结果可以看出,
1. 主线程会一直顺序执行
2. 碰到第一个await
会返回main方法,继续执行,
3. 由于碰到了task.Result
,必须要等待异步执行结果
4. Task.Run启动了一个新线程,而GetLengthAsync
方法中await之前的部分运行在主线程上,而await之后的代码运行在一个新的线程上,可以理解为await关键字把await之后的部分编译成了一个类似回调函数的方法,因此运行在与原来代码不一样的线程上。
思考时间:下面两种写法,它们有什么区别?
现在有两个异步方法
static async Task Task1() { Console.WriteLine("Task 1 begin"); await Task.Delay(5000); Console.WriteLine("Task 1 end"); } static async Task Task2() { Console.WriteLine("Task 2 begin"); await Task.Delay(3000); Console.WriteLine("Task 2 end"); }
实现1.
static async Task CallTask1Task2() { var task1 = Task1(); var task2 = Task2(); await task1; await task2; }
实现2.
static async Task CallTask1AndTask2() { await Task1(); await Task2(); }
[amazon_link asins=’B015316YQE,B072NSJSTT,B01LW72R2M’ template=’CopyOf-ProductGrid’ store=’boyd-23′ marketplace=’CN’ link_id=”]
结论:
实现1是同时启动了Task1和Task2,而实现2其实是一个顺序执行,因为await关键字,先执行了Task1,待完成后才执行Task2
I/O Bound and CPU Bound
1. 你的代码是否会“等待”某些内容,例如数据库中的数据?
如果答案为“是”,则你的工作是 I/O Bound。
2. 你的代码是否要执行开销巨大的计算?
如果答案为“是”,则你的工作是 CPU Bound。
如果你的工作为 I/O 绑定,请使用 async
和 await
(而不使用 Task.Run)。 不应使用任务并行库。 相关原因在深入了解异步的文章中说明。
如果你的工作为 CPU 绑定,并且你重视响应能力,请使用 async
和 await
,并在另一个线程上使用 Task.Run
生成工作。 如果该工作同时适用于并发和并行,则应考虑使用任务并行库。
我要等我的Task小伙伴(WhenAll & WhenAny)
WhenAll
创建一个任务,该任务将在数组中的所有 Task 对象都完成时完成。
public static Task WhenAll( params Task[] tasks )
var firstTask = new Task(() => TaskMethod("First Task", 3)); var secondTask = new Task (() => TaskMethod("Second Task", 2)); var whenAllTask = Task.WhenAll(firstTask, secondTask);
WhenAny
任何提供的任务已完成时,创建将完成的任务。
public static TaskWhenAny( params Task[] tasks )
和Task小伙伴接力跑(ContinueWith)
Task.Run(() => Task1()) .ContinueWith(task =>Task3());
我不想要我的Task小伙伴了(CancellationToken)
在自己的实现逻辑里用
var tokenSource = new CancellationTokenSource(); var token = tokenSource.Token; token.ThrowIfCancellationRequested()
在外部用tokenSource.Cancel();
控制
References
Asynchronous Programming Patterns