龙空技术网

C# 异步编程

全栈音乐 1421

前言:

而今大家对“net异步异常处理”可能比较注重,大家都想要知道一些“net异步异常处理”的相关文章。那么小编在网摘上搜集了一些有关“net异步异常处理””的相关内容,希望朋友们能喜欢,看官们快快来学习一下吧!

.NET 与C# 的每个版本发布都是有一个“主题”。即:

C#1.0托管代码→C#2.0泛型→C#3.0LINQ→C#4.0动态语言→C#5.0(NET Framework4.5)异步编程。

C#版本

.NET Framework版本

CLR版本

VisualStudio版本

C#1.0

.NET Framework 1.0

CLR1.0

Visual Studio 2002

C#1.2

.NET Framework 1.1

CLR1.1

Visual Studio 2003

C#2.0

.NET Framework 2.0

CLR2.0

Visual Studio 2005

C#3.0

.NET Framework 2.0

CLR2.0

Visual Studio 2008

.NET Framework 3.0

.NET Framework 3.5

C#4.0

.NET Framework 4.0

CLR4.0

Visual Studio 2010

C#5.0

.NET Framework 4.5

CLR4.0

Visual Studio 2012

Visual Studio 2013

C#6.0

.NET Framework 4.6

CLR4.0

Visual Studio 2015

大部分开发人员,在开发多线程应用程序时,都是使用ThreadPoolQueueUserWorkItem方法来发起一次简单的异步操作。然而,这种方式存在许多限制。最大的问题是没有一个内建的机制让你知道操作在什么时候完成,也没有一个机制在操作完成时让你知道是否能获得一个返回值。为了克服这些限制(并解决一些其他问题),Microsoft引入了三种异步编程模式:

.NET1.0异步编程模型 (APM),基于IAsyncResult接口实现。.NET2.0基于事件的异步编程模式(EMP),基于事件实现。.NET4.X基于任务的异步编程模式(TAP),新型异步编程模式,对于.NET4.0之后的异步结构都推荐使用此模式异步编程

一般需要I/O绑定(例如从网络请求数据或访问数据库),则需要使用异步编程。还可以使用 CPU 绑定(例如执行成本高昂的计算)。

C# 5.0拥有语言级别的异步编程模型,它使你能轻松编写异步代码。 它遵循基于任务的异步模式 (TAP)。

异步模型的基本概述

异步编程的核心是 TaskTask<TResult> 对象,它们受关键字 asyncawait 的支持。在大多数情况下十分简单:

对于I/O绑定代码,当你 await 一个操作,它将返回 async 方法中的一个 TaskTask<TResult>

对于 CPU密集型代码,当你 await 一个操作,它将在后台线程通过 Task.Run 方法启动。

I/O密集型示例:从 Web 服务下载数据

你可能需要在按下按钮时从 Web 服务下载某些数据,但不希望阻止 UI 线程。只需执行如下操作即可轻松实现:

private readonly HttpClient _httpClient = new HttpClient();//注册事件downloadButton.Clicked += async (o, e) =>{     var stringData = await _httpClient.GetStringAsync(URL);    DoSomethingWithData(stringData);};
CPU密集型示例:为游戏执行计算

假设你正在编写一个移动游戏,在该游戏中,按下某个按钮将会对屏幕中的许多敌人造成伤害。 执行伤害计算的开销可能极大,而且在 UI 线程中执行计算有可能使游戏在计算执行过程中暂停!

此问题的最佳解决方法是启动一个后台线程,它使用 Task.Run 执行工作,并 await 其结果。 这可确保在执行工作时 UI 能流畅运行。

private DamageResult CalculateDamageDone(){     //进行计算并返回该计算结果。}calculateButton.Clicked += async (o, e) =>{    //CalculateDamageDone()执行其工作。UI线程可以自由地执行其他工作。    var damageResult = await Task.Run(() => CalculateDamageDone());    DisplayDamage(damageResult);};
内部原理

C# 方面,编译器将代码转换为状态机,它将跟踪类似以下内容:碰到 await 时会让步执行(yielding execution),等后台作业完成时继续执行。

需了解的要点异步代码可用于 I/O密集型和 CPU密集型代码,但在每个方案中有所不同。异步代码使用 Task<TResult>Task对象,它们是对后台所完成的任务进行构建。async 关键字将方法转换为异步方法,这使你能在其正文中使用 await 关键字。应用 await 关键字后,它将挂起调用方法,并将控制权返还给调用方,直到等待的任务完成。await仅允许在异步方法中使用 。识别 CPU密集型和 I/O密集型工作

确定所需执行的操作是 I/O密集型或 CPU密集型是关键,因为这会极大影响代码效率,并可能导致某些结构的误用。

以下是编写代码前应考虑的两个问题:

你的代码是否会“等待”某些内容,例如数据库中的数据?如果答案为“是”,则你的工作是 I/O密集型。你的代码是否要执行开销巨大的计算?如果答案为“是”,则你的工作是 CPU密集型。

如果你的工作为 I/O密集型,请使用 asyncawait(而不使用 Task.Run)。

如果你的工作为 CPU密集型,并且你重视响应能力,请使用 asyncawait,使用 Task.Run 在另一个线程上生成任务。如果该任务同时适用于并发和并行,则应考虑使用任务并行库。

也许,你可能会遇到这样的情况:多线程处理时,CPU中上下文切换的开销高于 CPU 计算工作的开销。每种选择都有折衷,应根据自身情况选择正确的折衷方案。

从网络提取数据

此代码片段从 下载 HTML,并对 HTML 中出现字符串“baidu”的次数计数。

private readonly HttpClient _httpClient = new HttpClient();[HttpGet][Route("DotNetCount")]public async Task<int> GetDotNetCountAsync(){    //挂起GetDotNetCountAsync()以允许调用者(Web服务器)接受另一个请求,而不是阻止此请求。    var html = await _httpClient.GetStringAsync(";);    return Regex.Matches(html, @"baidu").Count;}
重要async方法需在其主体中具有await 关键字!如果 await 未用在 async 方法的主体中,C# 编译器将生成一个警告,但此代码将会以类似普通方法的方式运行。这会导致效率低下,因为编译器会使用状态机重新编写该方法的实现,C# 编译器为异步方法生成的状态机将不会完成任何任务。应将“Async”作为后缀添加到所编写的每个异步方法名称中。 这是 .NET 中的惯例,以便更轻松区分同步和异步方法。async void 应仅用于事件处理程序。 async void 是允许异步事件处理程序工作的唯一方法,因为事件不具有返回类型(因此无法利用 TaskTask<T>)。 其他任何对 async void 的使用都不是遵循 TAP 模型的,且可能存在一定使用难度,例如:async void 方法中引发的异常无法在该方法外部被捕获。十分难以测试 async void 方法。采用非阻塞方式编写等待任务的代码 如果将阻塞当前线程作为等待Task完成的一种方式,可能导致死锁和已阻塞的上下文线程,且可能需要更复杂的错误处理。下表提供了关于如何以非阻塞方式处理等待Tasks

使用以下方式…

而不是…

若要执行此操作

await

Task.Wait 或 Task.Result

检索后台任务的结果

await Task.WhenAny

Task.WaitAny

等待任何任务完成

await Task.WhenAll

Task.WaitAll

等待所有任务完成

await Task.Delay

Thread.Sleep

等待一段时间

进一步了解异步Task和Task<TResult>Task 表示不返回值的单个操作。Task<TResult> 表示返回 TResult 类型的值的单个操作。

请务必将任务理解为工作的异步抽象,而非在线程之上的抽象。 默认情况下,任务在当前线程上执行,且在适当时会将任务委托给操作系统。可选择的,通过 Task.Run API 显式请求任务在独立线程上运行。

对一个task(任务),Tasks提供了API用于监控,等待和访问返回值(使用Task<TResult>时)。而关键字await则在语言层面上为tasks的使用提供了更高层别的抽象。

使用await允许你的应用程序或服务在运行一个task时,转让控制权给此task的调度者以更有效地工作,直至此task执行完毕。 任务完成后代码无需依靠回调或事件便可继续执行。语言和任务API 会为你完成此操作。

深入了解 I/O密集型 的任务

以下部分介绍了使用典型异步 I/O 调用时会出现的各种情况。

调用一个async方法并且返回一个可能尚未完成的活动任务。

public Task<string> GetHtmlAsync(){    //这里的执行是同步的    var client = new HttpClient();    return client.GetStringAsync(";);}

使用了 asyncawait 关键字对任务进行操作。

public async Task<string> GetFirstCharactersCountAsync(string url, int count){    //这里的执行是同步的    var client = new HttpClient();    // GetFirstCharactersCountAsync()的执行在这里让步,控制权会移交此方法的caller    // GetStringAsync返回 Task<string>    var page = await client.GetStringAsync(";);    //当client.GetStringAsync任务完成时执行继续,再次变为同步。    if (count > page.Length)    {        return page;    }    else    {        return page.Substring(0, count);    }}

方法GetStringAsync()会调用一系列的底层.net库(可能会调用其它的async方法)直到它通过P/Invoke方式调用到一个本地网络库。这个本地库可能会后续调用系统API(比如Linux上调用socket的write)。在本地/托管的交互边界,会创建一个task对象,并被层层向上传递,中途可能会被操作或是直接返回,最后返回给初始的c这里写代码片aller。

在第二个例子中,GetStringAsync方法会返回一个Task<T>对象。 由于使用了 await,此方法会返回一个新创建的task对象。在这个点,控制权会移交给GetFirstCharactersCountAsync方法的caller。Task<T>的方法和属性使得callers可以监控这个task的进度,这个task会在GetFirstCharactersCountAsync方法的剩余代码执行后才真正结束。

整个的处理过程,一个关键点是:在处理任务的过程中,没有线程在等待,没有线程专用于运行任务。虽然工作是在一定的上下文中处理的(比如,OS必须把数据传递给设备驱动程序并且响应中断),但没有任何线程浪费在等待数据从请求至返回的过程中。这可以大大提升系统的吞吐量,而不是把时间浪费在等待I/O调用完成上。

深入了解 CPU密集型 的任务

使用async编写CPU密集型问题的代码与编写I/O密集型的代码有一点不同。因为工作是在CPU上完成的,在执行计算时是无法将CPU线程释放出来的。asyncawait提供了一种清晰的与后台线程的交互方式,同时又能使async方法的调用者保持响应。不为共享数据提供任何保护。如果你正在使用共享数据,你依然需要应用合适的数据同步策略。

下面是CPU密集型的async方法演示:

public async Task<int> CalculateResult(InputData data){     //使线程池上线程排队    var expensiveResultTask = Task.Run(() => DoExpensiveCalculation(data));    //可以同时执行其他一些工作,因为CalculateResult()仍在执行中!    //CalculateResult的执行在这里让步!     var result = await expensiveResultTask;    return result;}

CalculateResult方法是在调用者的线程上执行的。当其调用Task.Run方法,它将代价昂贵的CPU操作DoExpensiveCalculation向线程池中入队,并返回一个Task<int>句柄。DoExpensiveCalculation()最终会在下一个可用线程上并发执行,可能在另一个单独的CPU核上。当DoExpensiveCalculation()在另一个线程上执行时,是可以并发地做其它工作的,因为调用CalculateResult()的线程还在跑。

一旦执行到await时,CalculateResult()的执行便会移交给它的调用者,允许DoExpensiveCalculation()计算结果时,当前线程依然可以执行其它的工作。一旦计算结束,结果会被入队,等待在主线程上运行。最后,主线程会返回到DoExpensiveCalculation的执行点,取得结果,并继续向下运行。

当你需要处理CPU密集型的工作,但是又需要响应能力时,asyncawait是最佳实践。

异步返回类型

异步方法可以具有以下返回类型:

Task<TResult>(用于有返回值的异步方法)。Task(用于执行操作但不返回任何值的异步方法)。void(用于事件处理程序)。Task<TResult>返回类型

using System;using System.Linq;using System.Threading.Tasks;public class Example{   public static void Main()   {      Console.WriteLine(ShowTodaysInfo().Result);   }   private static async Task<string> ShowTodaysInfo()   {      string ret = $"Today is {DateTime.Today:D}\n" +                   "Today's hours of leisure: " +                   $"{await GetLeisureHours()}";      return ret;   }   static async Task<int> GetLeisureHours()     {         // Task.FromResult 创建指定结果的、成功完成的Task<TResult>         var today = await Task.FromResult<string>(DateTime.Now.DayOfWeek.ToString());         int leisureHours;         if (today.First() == 'S')             leisureHours = 16;         else             leisureHours = 5;         return leisureHours;     }  }// 该示例显示如下输出://       Today is Wednesday, May 24, 2017//       Today's hours of leisure: 5

ShowTodaysInfo 方法中从 await 表达式内调用 GetLeisureHours 时,await 表达式检索存储在由 GetLeisureHours 方法返回的任务中的整数值(leisureHours 的值)。

Task返回类型

不包含 return 语句的异步方法或包含不返回操作数的 return 语句的异步方法通常具有返回类型 Task。 如果此类方法同步运行,它们将返回 void。 如果在异步方法中使用 Task 返回类型,调用方法可以使用 await 运算符暂停调用方的完成,直至被调用的异步方法结束。

如下示例中,WaitAndApologize 异步方法不包含 return 语句,因此此方法返回 Task 对象。 通过这样可等待 WaitAndApologize。 请注意, Task 类型不包含 Result 属性,因为它不具有任何返回值。

using System;using System.Threading.Tasks;public class Example{   public static void Main()   {      DisplayCurrentInfo().Wait();  //异步转同步,等待 Task 完成执行过程   }   static async Task DisplayCurrentInfo()   {      await WaitAndApologize();      Console.WriteLine($"Today is {DateTime.Now:D}");      Console.WriteLine($"The current time is {DateTime.Now.TimeOfDay:t}");      Console.WriteLine("The current temperature is 76 degrees.");   }   static async Task WaitAndApologize()   {       await Task.Delay(2000);        //Task.Delay将以下行延迟两秒。       Console.WriteLine("\nSorry for the delay. . . .\n");     }}// 该示例显示以下输出://       Sorry for the delay. . . .//       //       Today is Wednesday, May 24, 2017//       The current time is 15:25:16.2935649//       The current temperature is 76 degrees.

通过使用 await 语句而不是 await 表达式等待 WaitAndApologize,类似于返回 void 的同步方法的调用语句。 await 运算符的应用程序在这种情况下不生成值。

Void 返回类型

在异步事件处理程序中使用 void 返回类型,这需要 void 返回类型。 对于事件处理程序以外的不返回值的方法,应返回 Task,因为无法等待返回 void 的异步方法。 这种方法的任何调用方必须能够继续完成,而无需等待调用的异步方法完成,并且调用方必须独立于异步方法生成的任何值或异常。

返回 void 的异步方法的调用方无法捕获从该方法引发的异常,且此类未经处理的异常可能会导致应用程序故障。 如果返回 TaskTask<TResult> 的异步方法中出现异常,此异常将存储于返回的任务中,并在等待该任务时再次引发。因此,请确保可以产生异常的任何异步方法都具有返回类型 TaskTask<TResult> ,并确保会等待对方法的调用。

异步其他使用 (C#)

本部分介绍使用 CancellationToken 的示例和一些重要的 Task 方法,例如 Task.WhenAllTask.WhenAny

使用 WhenAnyWhenAll 可以更轻松地启动多个任务并通过监听单个任务待其完成。

集合中的任何任务完成时,WhenAny 将返回完成的任务。集合中的所有任务完成时,WhenAll 将返回完成的任务。Task WhenAll、WhenAny创建任务的任务使用Task.WhenAll

此方法以异步方式等待多个异步操作。

可将 Task.WhenAll 方法应用于任务的集合。 WhenAll 返回单个任务,直到集合中的每个任务都已完成之后,该任务才会完成。 任务会表现为并行运行,但不会创建其他额外线程。 任务可以按任何顺序完成。

var firstTask = new Task<int>(() => TaskMethod("First Task", 3));var secondTask = new Task<int>(() => TaskMethod("Second Task", 2));var whenAllTask = Task.WhenAll(firstTask, secondTask);whenAllTask.ContinueWith(t =>Console.WriteLine("The first answer is {0}, the second is {1}", t.Result[0], t.Result[1]),TaskContinuationOptions.OnlyOnRanToCompletion);  //先驱任务成功完成,才会执行此延续任务firstTask.Start();secondTask.Start();

这里设置了一个名字为whenAllTask的变量,他是用静态方法Task.WhenAll创建的第三个任务。需要注意的是,由于是基于前面两个任务创建的,因此返回的类型为Task<int[]>。在firstTasksecondTask运行完毕后(WhenAll),采用后续操作(ContinueWith),函数中的t其实也就是whenAllTask的传参。后续操作打印出whenAllTask中的Result数组成员,后续操作的状态条件为whenAllTask的状态为OnlyOnRanToCompletion

使用Task.WhenAny

WhenAny表示任意一个Task完成之后,返回这个Task对象

//向应该被取消的 CancellationToken 发送信号static CancellationTokenSource C1 = new CancellationTokenSource();     public async void OpeartTask()        {            List<Task<string>> TaskList = new List<Task<string>>() {                Task.Factory.StartNew(()=> { return WriteHello(10); },C1.Token),                Task.Factory.StartNew(()=> { return WriteHello(20); },C1.Token),                Task.Factory.StartNew(()=> { return WriteHello(30); },C1.Token),                Task.Factory.StartNew(()=> { return WriteHello(40); },C1.Token)            };            var Reuslt = await Task.WhenAny(TaskList);            //IsCompleted 在任务处于以下三个最终状态之一时返回 true:RanToCompletion、Faulted 或 Canceled。            if (Reuslt.IsCompleted)            {                Console.WriteLine("I'm "+Reuslt.Result+" OK,Other Must Stop");                C1.Cancel();            }}

在指定的毫秒数后取消,我们可以通过在创建CancellationTokenSource时设置超时来实现这一功能。

//var cancelTokenSource = new CancellationTokenSource(3000);//除此之外,也可以通过如下代码实现同样的效果。//cancelTokenSource.CancelAfter(3000);  

另一示例

首先建立一个Task List,名字为tasks。而后依次向里添加新任务实例。而后,监控tasks中的任务数量,在循环中创建completedTask任务,用来表示任一个在tasks中已经完成操作的Task,如果一个任务都没运行完成,则WhenAny(tasks).Result阻止调用线程,直至异步操作已完成。这里的WhenAny(tasks).Result指的是Task<int>,也就是完成的Task<int>实例。

var tasks = new List<Task<int>>();for (int i = 1; i < 4; i++){int counter = i;var task = new Task<int>(() => TaskMethod(string.Format("Task {0}", counter), counter));tasks.Add(task);task.Start();}while (tasks.Count > 0){var completedTask = Task.WhenAny(tasks).Result;//这里的WhenAny(tasks).Result是指代Task<int>!tasks.Remove(completedTask);Console.WriteLine("A task has been completed with result {0}.", completedTask.Result);}

标签: #net异步异常处理