C#的多线程——使用async和await来完成异步编程(Asynchronous Programming with async and await)

https://msdn.microsoft.com/zh-cn/library/mt674882.aspx

侵删

 

更新于:2015年6月20日

欲获得最新的Visual Studio 2017 RC文档,参考Visual Studio 2017 RC Documentation

使用异步编程,你可以避免性能瓶颈和提升总体相应效率。然而,传统的异步方法代码的编写方式比较复杂,导致它很难编写,调试和维护。

Visual Studio 2012引入了一个简单的异步编程的方法,依赖.NET Framework 4.5和对应的运行时版本的支持。这样,开发者的一些复杂的工作就交给了编译器去完成,并且使你的代码保证了同步编程代码一样的逻辑结构。这样你就可以省了很多编写异步代码的时间。

本节主要介绍了何时和如何使用异步编程,包含了一些链接来展示详情和例子。

使用异步来提升相应性

异步编程对于一些存在潜在的阻塞可能的行为很重要,例如当你的应用程序需要访问网络资源的时候。连接到网络资源有时候很慢或者有延迟,这时如果这个访问时同步的,整个应用程序就要等待这个行为的结束。如果网络访问行为是异步的,这个应用程序就可以同时执行其他不依赖网络资源的工作直到这个潜在的阻塞行为结束。

下面的表格展示了一些异步编程提升响应性的场景。其中列出的.NET Framework 4.5和运行时的API包含的方法支持异步编程。

应用程序功能

支持的包含异步方法的API

网络连接

HttpClient, SyndicationClient

文件资源

StorageFile, StreamWriter, StreamReader, XmlReader

图片

MediaCapture, BitmapEncoder, BitmapDecoder

WCF

Synchronous and Asynchronous Operations

 

特别是在能够访问UI线程的应用程序中,异步编程非常有价值。因为所有与UI有关的行为都共享一个线程,如果有任何一个线程在同步进行的时候被阻塞,你的应用程序就会停止响应,尽管应用程序只是在等待,你却仍然以为它已经挂了。

当你使用异步方法, 应用程序会继续相应UI。你可以改变窗口大小或者最小化它,例如你可以关闭应用程序,如果你不想等它结束的话。

这种基于异步的编程方法让你在编写异步的时候多了一个选择——它相当于提供了一个自动转化机制。也就是说,你可以花更少的精力来完成传统异步编程的效果。

异步的方法更加容易编写

C#中的asyncawait关键字是异步编程的核心。通过使用这两个关键字,你可以使用 .NET Framework 或者运行时的资源来创造异步方法——几乎和你写同步方法一样。你通过使用async和await定义的方法就相当于异步方法。

下面的例子展示了一个异步方法。几乎所有的代码都看上去那么熟悉。注释段解释了一些你来用完成整个异步编程添加的特性。

在这个主题的结尾,你可以找到一个完整的WPF例子代码,你可以从Async Sample: Example from "Asynchronous Programming with Async and Await"下载。

// 在方法签名中需要注意的三个地方:  
//  - 方法必须有async修饰.   
//  - 返回类型必须是Task或者Task<T>(看 "Return Types" 小节.)  
//    这里是Task<int>因为返回声明返回了一个整型.  
//  - 方法名以async结尾  
async Task<int> AccessTheWebAsync()  
{   
    // 你需添加一个对System.Net.Http的引用来声明client
    HttpClient client = new HttpClient();  
  
    // GetStringAsync方法返回一个Task<string>.这个意味着当你await这个  
    // 任务的时候,你会得到一个string (urlContents).  
    Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");  
  
    // 你可以完成一些不依赖GetStringAsync方法返回的字符串的任务  
    DoIndependentWork();  
  
    // await操作符中止了AccessTheWebAsync方法.  
    //  - AccessTheWebAsync 无法继续知道 getStringTask 完成.  
    //  - 与此同时,控制器返回AccessTheWebAsync的调用.  
    //  - 当 getStringTask 完成的时候,控制器继续进行任务.   
    //  - await操作符获得getStringTask返回的字符串.  
    string urlContents = await getStringTask;  
  
    // 返回声明指定了一个整型结果  
    // 任何awaiting AccessTheWebAsync的方法都会得到这个长度值 
    return urlContents.Length;  
}  

在开始调用GetStringAsync 和await这个方法完成之间,如果AccessTheWebAsync方法不包含任何其他的工作的话,你可以通过调用和await下面的声明来简化你的代码。

string urlContents = await client.GetStringAsync();  

下面总结了之前例子中异步方法的特性:

  • 方法签名包含async 修饰符
  • 异步方法名称按照惯例以Async结尾
  • 返回值是以下几种类型之一:
  • Task<TResult>:如果你的方法包含一个操作符是TResult的返回声明
  • Task :如果你的方法没有任何返回值或者返回声明里面没有操作符
  • Void:如果你在写一个异步事件句柄

                  更多的信息可以在下面的“返回值类型和参数”获得。

  • 方法通常包含至少一个await表达式,这个await在方法标记了一个方法无法继续执行,直到需要等待的方法完成的点。在这个等待期间,方法是被暂停着的,控制器返回到方法的调用者。下一个小节会介绍这个暂停点都发生了什么。

在异步方法中,你使用这些关键字和类型去指定要做什么,编译器就会完成剩下的工作,包括继续追踪控制器在暂停的方法中返回之后要发生什么。一些例程,例如循环和异常处理,在传统的异步编程代码中很难被处理。在一个async方法中,你就像写同步代码一样地去编程,就能解决问题了。

更多的关于之前.NET Framework版本中的异步信息,参考TPL and Traditional .NET Framework Asynchronous Programming

在一个异步方法中发生了什么

在异步编程中最重要的事情就是了解控制流如何在方法与方法之间移动,下图展示了这个过程:

Trace an async program

图中的数字对应以下的步骤:

  • 一个事件句柄被调用然后等待AccessTheWebAsync 这个异步方法。
  • AccessTheWebAsync 创建了一个HttpClient实例,然后调用GetStringAsync 异步方法去把一个网页以字符串形式下载。
  • GetStringAsync 方法中发生了一些事情使这个方法停止了,可能它需要等待网站响应或者其他的一些阻塞动作。为了防止阻塞资源,GetStringAsync 方法把控制还给了它的调用者AccessTheWebAsync。GetStringAsync 返回了一个Task<TResult> ,TResult是一个string类型,AccessTheWebAsync 方法把任务交给了getStringTask 变量。这个task表示正在进行的GetStringAsync的调用,当这个调用完成之后会返回一个string的值。
  • 因为getStringTask 还在await中,所以AccessTheWebAsync 可以继续做其他不依赖GetStringAsync返回值的工作。这个工作可以用DoIndependentWork的调用来表示
  • DoIndependentWork 是一个完成其他的工作然后返回到它的调用者的同步方法。
  • AccessTheWebAsync 已经完成了不需要getStringTask返回值的工作。AccessTheWebAsync 接下来想要计算并且返回已经下载的string长度,但是这个方法无法计算,直到getStringTask返回一个字符串。因此,AccessTheWebAsync 使用了一个await操作符来停止整个AccessTheWebAsync方法的调用。AccessTheWebAsync返回一个Task<int>给调用者,task意味着一个产生字符串长度的承诺。注意,如果GetStringAsync在AccessTheWebAsync等待它之前结束,控制器仍然在AccessTheWebAsync方法之中。如果getStringTask已经完成并且AccessTheWebSync不需要等待它的结果的话,暂停然后AccessTheWebAsync方法返回的异步开销就会被浪费。 在调用者内部(这个例子中的事件句柄),方法继续运行。在方法await之前,调用者可能会进行其他不需要依赖AccessTheWebAsync返回的工作,或者如果没有其他工作的话调用者可能立即await。这个事件句柄等待AccessTheWebAsync方法,AccessTheWebAsync方法等待GetStringAsync方法。
  • GetStringAsync 完成并且产生了一个string返回值。这个string返回值 并不是你想的那种返回给GetStringAsync调用者的值(要记住这个方法已经在步骤3的时候返回了一个task)。不同的是,这个string返回值被保存在task中,这个task表示getStringTask方法的结束。await操作符获得getStringTask的返回结果。赋值声明把这个结果赋值给urlContents变量。
  • 当AccessTheWebAsync得到了string结果之后,它就可以计算string长度。 AccessTheWebAsync的工作也完成了,这个等待着的句柄就可以继续。在这个专题结尾的例子中,你就可以确定这个事件句柄获得并且打印出了string返回值的长度。

如果你是异步编程的初学者,那就花一点事件考虑同步和异步编程的不同之处。一个同步方法只有等到它的工作都完成了(步骤5)才会返回,而一个异步方法在等待的时候(步骤3和6)会返回一个task。当异步方法最终完成他的工作的时候,task会被标记成完成,并且它的结果,如果有的话,会被保存在task中。

想获得更多的信息,请参阅Control Flow in Async Programs (C#)

异步方法的API

你可能会想知道去哪里找到类似于GetStringAsync 的异步方法。.NET Framework 4.5或者更高的版本都支持async和await。你可以通过观察方法名后面是否存在“Async”后缀,返回类型是Task 还是Task<TResult>来判断他们是不是异步的。例如,System.IO.Stream 类就包含类似于CopyToAsync, ReadAsyncWriteAsync 的方法,作为CopyTo, ReadWrite的补充。

运行时也包含很多你可以在窗口应用程序中使用async和await的方法。更多信息和例子,参阅Quickstart: using the await operator for asynchronous programming, Asynchronous programming (Windows Store apps), and WhenAny: Bridging between the .NET Framework and the Windows Runtime (C#).

线程

异步方法是一个非阻塞的操作。当await的任务运行的时候,异步方法中的await表达式不会阻塞当前线程。并且这个表达式会继续执行方法中剩下的代码然后把控制还给异步方法的调用者。

async和await关键字不会生成额外的线程。异步方法不需要多线程,因为一个异步方法并不是在它自己的线程中运行的。这个方法在当前的同步上下文中运行,并且仅仅在这个方法被激活的时候才耗费时间。你可以使用Task.Run 来把对CPU性能敏感的任务设置成后台线程,但是后台线程不能等待其他任务完成再继续执行。

几乎所有情况下都建议使用基于async的异步编程方法。而且这种方式比对IO性能敏感的BackgroundWorker更好,因为代码更加简单并且你不需要去关心资源争夺的情况。和 Task.Run结合使用的话,异步编程比对CPU性能敏感的BackgroundWorker更好,因为异步编程将Task.Run分配到线程池的工作和你代码中的线程协调细节分开。

async和await

如果你使用async修饰符来定义一个方法的话,方法就具有以下两个功能:

  • 异步方法可以使用await来指定一个等待点。await操作符告诉编译器这个异步方法不能通过,直到需要等待的方法执行完毕。与此同时,控制器返回到该方法调用者。

       async方法中的await造成的停止,不会导致方法的推出,并且finally语句块不会执行。

  • 异步方法可以被调用它的方法await

一个异步方法通常包含一个或者多个await操作符,但是await操作符就算不存在也不会造成编译错误。如果一个异步方法没有使用await操作符的话,整个方法就会和同步方法一样,除了有一个async操作符。编译器会对这个方法发出一个警告。

async和await是上下文关键字,获取更多相关的信息和例子,可以参考:

返回值类型和参数

 

在.NET Framework中一个异步方法一般会返回一个Task 或者一个 Task<TResult>类型。在异步方法中,一个await操作符应用在一个从其他异步方法返回的任务中。

如果方法包含指定了TResult类型的 return 声明,你可以定义一个Task<TResult>作为返回值。

如果你的方法没有返回声明或者声明中不返回操作符,你可以使用Task作为返回值。

下面的例子展示如何声明和调用返回一个Task 或者一个 Task<TResult>的例子:

// 签名中制定一个 Task<TResult>  类型
async Task<int> TaskOfTResult_MethodAsync()  
{  
    int hours;  
    // . . .  
    // 返回声明中制定一个返回类型  
    return hours;  
}  
  
// 调用TResult_MethodAsync  
Task<int> returnedTaskTResult = TaskOfTResult_MethodAsync();  
int intResult = await returnedTaskTResult;  
// 或者使用一个单行的声明  
int intResult = await TaskOfTResult_MethodAsync();  
  
// 签名指定一个 Task 
async Task Task_MethodAsync()  
{  
    // . . .  
    // 这个方法没有返回声明
}  
  
// 调用ethodAsync  
Task returnedTask = Task_MethodAsync();  
await returnedTask;  
//或者使用一个单行声明
 await Task_MethodAsync(); 

 

每一个返回的task都代表一个正在进行的工作。一个task包括了这个异步方法的运行状态和最终的结果或者抛出的异常。

一个异步方法也可以有一个void的返回类型。这个返回类型主要在定义事件句柄的时候用到,对于定义事件句柄,void的返回类型是必须的。异步事件句柄经常作为异步程序的起点。

一个void返回类型的异步方法不能被await并且这个方法的调用者不能捕获这个方法抛出的异常。

一个异步方法不能声明 ref 或者out 参数,但是这个方法可以调用包含这种类型参数的方法。

参考Async Return Types (C#)来获取更多的相关信息和例子。更多的关于如何铺货异步方法中的异常信息,参考try-catch

运行时中的异步API包含了以下其中一种的返回类型,他们都类似于task:

想获得更多信息,参见 Quickstart: using the await operator for asynchronous programming.

命名惯例

按照惯例,你要在异步方法中添加async 修饰符。

当一个事件,基类或者接口暗示了其他的名字,例如你不可以重命名一个事件句柄,例如Button1_Click,你可以忽视这个惯例。(蛤??)

相关的主题和例子(Visual Studio

标题 介绍 例子
Walkthrough: Accessing the Web by Using async and await (C#) 展示了如何把一个同步的WPF解决方案改成一个异步WPF解决方案。这个应用程序下载了几个页面。 Async Sample: Accessing the Web Walkthrough
How to: Extend the async Walkthrough by Using Task.WhenAll (C#) 添加了Task.WhenAll到之前的修改中。WhenAll让所有的下载同时开始。  
How to: Make Multiple Web Requests in Parallel by Using async and await (C#) 介绍了如何同时开启多个任务。


Async Sample: Make Multiple Web Requests in Parallel

Async Return Types (C#) 介绍了async方法可以返回的类型并且介绍了何时应该返回何种类型比较合适  
Control Flow in Async Programs (C#) 在一个异步程序中追溯一系列await表达式中的控制流 Async Sample: Control Flow in Async Programs
Fine-Tuning Your Async Application (C#) 展示如何在你的异步解决方案中添加以下的功能
- Cancel an Async Task or a List of Tasks (C#)
- Cancel Async Tasks after a Period of Time (C#)
- Cancel Remaining Async Tasks after One Is Complete (C#)
- Start Multiple Async Tasks and Process Them As They Complete (C#)
Async Sample: Fine Tuning Your Application
Handling Reentrancy in Async Apps (C#) 展示了如何当异步操作在执行的时候重新启动如何处理  
WhenAny: Bridging between the .NET Framework and the Windows Runtime (C#) 展示了如何在.NET Framework和运行时的IAsyncOperations中关联两个task类型,因此你可以和运行时方法搭配使用WhenAny<TResult> Async Sample: Bridging between .NET and Windows Runtime (AsTask and WhenAny)
Async Cancellation: Bridging between the .NET Framework and the Windows Runtime (C#) 展示了如何在.NET Framework和运行时的IAsyncOperations中关联两个task类型,因此你可以和运行时方法搭配使用CancellationTokenSource Async Sample: Bridging between .NET and Windows Runtime (AsTask & Cancellation)
Using Async for File Access (C#) 列举展示了使用async和await来获取文件的好处。  
Task-based Asynchronous Pattern (TAP) 介绍了.NET Framework中的一个新的异步模式。
这种模式基于TaskTask<TResult> 类型
 
Async Videos on Channel 9 提供了一些关于异步编程的视频  

一个例子

下面是之前提到的WPF的项目中MainWindow.xaml.cs中的一段代码。你可以从Async Sample: Example from "Asynchronous Programming with Async and Await"下载这个例子。

 

using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Text;  
using System.Threading.Tasks;  
using System.Windows;  
using System.Windows.Controls;  
using System.Windows.Data;  
using System.Windows.Documents;  
using System.Windows.Input;  
using System.Windows.Media;  
using System.Windows.Media.Imaging;  
using System.Windows.Navigation;  
using System.Windows.Shapes;  
  
// Add a using directive and a reference for System.Net.Http;  
using System.Net.Http;  
  
namespace AsyncFirstExample  
{  
    public partial class MainWindow : Window  
    {  
        // Mark the event handler with async so you can use await in it.  
        private async void StartButton_Click(object sender, RoutedEventArgs e)  
        {  
            // Call and await separately.  
            //Task<int> getLengthTask = AccessTheWebAsync();  
            //// You can do independent work here.  
            //int contentLength = await getLengthTask;  
  
            int contentLength = await AccessTheWebAsync();  
  
            resultsTextBox.Text +=  
                String.Format("\r\nLength of the downloaded string: {0}.\r\n", contentLength);  
        }  
  
        // Three things to note in the signature:  
        //  - The method has an async modifier.   
        //  - The return type is Task or Task<T>. (See "Return Types" section.)  
        //    Here, it is Task<int> because the return statement returns an integer.  
        //  - The method name ends in "Async."  
        async Task<int> AccessTheWebAsync()  
        {   
            // You need to add a reference to System.Net.Http to declare client.  
            HttpClient client = new HttpClient();  
  
            // GetStringAsync returns a Task<string>. That means that when you await the  
            // task you'll get a string (urlContents).  
            Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");  
  
            // You can do work here that doesn't rely on the string from GetStringAsync.  
            DoIndependentWork();  
  
            // The await operator suspends AccessTheWebAsync.  
            //  - AccessTheWebAsync can't continue until getStringTask is complete.  
            //  - Meanwhile, control returns to the caller of AccessTheWebAsync.  
            //  - Control resumes here when getStringTask is complete.   
            //  - The await operator then retrieves the string result from getStringTask.  
            string urlContents = await getStringTask;  
  
            // The return statement specifies an integer result.  
            // Any methods that are awaiting AccessTheWebAsync retrieve the length value.  
            return urlContents.Length;  
        }  
  
        void DoIndependentWork()  
        {  
            resultsTextBox.Text += "Working . . . . . . .\r\n";  
        }  
    }  
}  
  
// Sample Output:  
  
// Working . . . . . . .  
  
// Length of the downloaded string: 41564.  
posted @ 2016-08-19 20:12  balavatasky  阅读(645)  评论(0编辑  收藏  举报