[C#].net 异步编程的前世今生 & async & await

.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

该任务已通过对其自身的 CancellationToken 引发 OperationCanceledException 对取消进行了确认,此时该标记处于已发送信号状态;或者在该任务开始执行之前,已向该任务的 CancellationToken 发出了信号。 有关详细信息,请参阅任务取消

Created

该任务已初始化,但尚未被计划。

Faulted

由于未处理异常的原因而完成的任务。

RanToCompletion

已成功完成执行的任务。

Running

该任务正在运行,但尚未完成。

WaitingForActivation

该任务正在等待 .NET Framework 基础结构在内部将其激活并进行计划。

WaitingForChildrenToComplete

该任务已完成执行,正在隐式等待附加的子任务完成。

WaitingToRun

该任务已被计划执行,但尚未开始执行。

Task的生命周期

async和await

定义一个异步方法

  • 方法的声明中有async修饰符。
  • 按照约定,方法名字里有Async后缀。
  • 返回值类型是下面三种情况:
  • * Task,当方法里没有return语句或者return语句没有操作数。
    * Task\,方法里有return语句,并且返回值类型是TResult。
    * void,编写的是一个异步的event handler。

  • 方法通常包含至少一个await表达式。
  • await针对的是Task对象
  • main方法不能加async关键字,因此在main方法中也不能用await,但是可以调用Task.Wait()或Task.Result()获得结果
// 声明中的要点: 
//  - 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();
}

博主为你专属推荐


结论:
实现1是同时启动了Task1和Task2,而实现2其实是一个顺序执行,因为await关键字,先执行了Task1,待完成后才执行Task2

I/O Bound and CPU Bound

1. 你的代码是否会“等待”某些内容,例如数据库中的数据?

如果答案为“是”,则你的工作是 I/O Bound。

2. 你的代码是否要执行开销巨大的计算?

如果答案为“是”,则你的工作是 CPU Bound。

如果你的工作为 I/O 绑定,请使用 asyncawait(而不使用 Task.Run)。 不应使用任务并行库。 相关原因在深入了解异步的文章中说明。

如果你的工作为 CPU 绑定,并且你重视响应能力,请使用 asyncawait,并在另一个线程上使用 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 Task WhenAny(
	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

Task类

可恶的I/O-Bound工作

异步编程

Asynchronous Programming Patterns

打赏

博主开通了微信公众号,欢迎关注啦

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.