.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 Task ReadAsync(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);
Task task = 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
![[C#].net 异步编程的前世今生 & async & await 1 [C#].net 异步编程的前世今生 & async & await Screen Shot 2017 12 14 at 21.52.02](https://www.boydwang.com/wordpress/wp-content/uploads/2017/12/Screen-Shot-2017-12-14-at-21.52.02.png)
![[C#].net 异步编程的前世今生 & async & await 3 [C#].net 异步编程的前世今生 & async & await Screen Shot 2017 12 14 at 21.52.02](http://www.boydwang.com/wordpress/wp-content/uploads/2017/12/Screen-Shot-2017-12-14-at-21.52.02-300x230.png)