龙空技术网

Task 使用详细「基础操作,异步原则,异步函数,异步模式」

中年农码工 1148

前言:

此时我们对“net异步异常处理”大约比较注重,小伙伴们都想要分析一些“net异步异常处理”的相关文章。那么小编也在网摘上收集了一些有关“net异步异常处理””的相关知识,希望看官们能喜欢,你们一起来学习一下吧!

线程是创建并发的底层工具,对于开发者而言,想实现细粒度并发具有一定的局限性,比如将小的并发组合成大的并发,还有性能方面的影响。

Task可以很好的解决这些问题,Task是一个更高级的抽象概念,代表一个并发操作,但不一定依赖线程完成。

Task从Framework4.0开始引入,Framework4.5又添加了一些功能,比如Task.Run(),async/await关键字等,

在.NET Framework4.5之后,基于任务的异步处理已经成为主流模式, (Task-based Asynchronous Pattern,TAP)基于任务的异步模式。

在使用异步函数之前,先看下Task的基本操作。

一. Task 基本操作1.1 Task 启动方式

Task.Run(()=>Console.WriteLine("Hello Task"));

Task.Factory.StartNew(()=>Console.WriteLine("Hello Task"));

Task.Run是Task.Factory.StartNew的快捷方式。

启动的都是后台线程,并且默认都是线程池的线程

+ View Code

如果Task是长任务,可以添加TaskCreationOptions.LongRunning参数,使任务不运行在线程池上,有利于提升性能。

+ View Code

1.2 Task 返回值/带参数

Task 有一个泛型子类Task<TResult>,允许返回一个值。

+ View Code

通过任务的Result属性获取返回值,这是会堵塞线程,尤其是在桌面客户端程序中,谨慎使用Task.Result,容易导致死锁!

同时带参数的方式也不是很合理,后面可以被async/await方式直接替代。

1.3 Task 异常/异常处理

当任务中的代码抛出一个未处理异常时,调用任务的Wait()或者Result属性时,异常会被重新抛出。

+ View Code

对于自治任务(没有wait()和Result或者是延续的任务),使用静态事件TaskScheduler.UnobservedTaskException可以在全局范围订阅未观测的异常。

以便记录错误日志

1.4 Task 延续

延续通常由一个回调方法实现,该方法会在任务完成之后执行,延续方法有两种

(1)调用任务的GetAwaiter方法,将返回一个awaiter对象。这个对象的OnCompleted方法告知任务当执行完毕或者出错时调用一个委托。

+ View Code

如果learnTask任务出现错误,延续代码awaiter.GetResult()将重新抛出异常,其中GetResult可以直接得到原始的异常,如果使用Result属性,只能解析AggergateException.

这种延续方法更适用于富客户端程序,延续可以提交到同步上下文,延续回到UI线程中。

当编写库文件,可以使用ConfigureAwait方法,延续代码会运行在任务运行的线程上,从而避免不必要的切换开销。

var awaiter =learnTask.ConfigureAwait(false).GetAwaiter(); 

(2)另一种方法使用ContiuneWith

Task<string> learnTask = Task.Run(Learn);

learnTask.ContinueWith(antecedent =>

{

var result = learnTask.Result;

Console.WriteLine(result);

});

string Learn()

{

Console.WriteLine("Learn Method Executing");

Thread.Sleep(1000);

return "Learn End";

}

当任务出现错误时,必须处理AggregateException, ContiuneWith更适合并行编程场景。

1.5 TaskCompletionSource类使用

从如下源码中可以看出当实例化TaskCompletionSource时,构造函数会新建一个Task任务。

public class TaskCompletionSource

{

private readonly Task _task;

/// <summary>Creates a <see cref="TaskCompletionSource"/>.</summary>

public TaskCompletionSource() => _task = new Task();

/// <summary>

/// Gets the <see cref="Tasks.Task"/> created

/// by this <see cref="TaskCompletionSource"/>.

/// </summary>

/// <remarks>

/// This property enables a consumer access to the <see cref="Task"/> that is controlled by this instance.

/// The <see cref="SetResult"/>, <see cref="SetException(Exception)"/>, <see cref="SetException(IEnumerable{Exception})"/>,

/// and <see cref="SetCanceled"/> methods (and their "Try" variants) on this instance all result in the relevant state

/// transitions on this underlying Task.

/// </remarks>

public Task Task => _task;

}

它的真正的作用是创建一个不绑定线程的任务。

eg: 可以使用Timer类,CLR在定时之后触发一个事件,而无需使用线程。

实现通用Delay方法:

Delay(5000).GetAwaiter().OnCompleted(()=>{ Console.WriteLine("Delay End"); });

Task Delay(int millisecond)

{

var tcs = new TaskCompletionSource<object>();

var timer = new System.Timers.Timer(millisecond) { AutoReset = false };

timer.Elapsed += delegate

{

timer.Dispose();

tcs.SetResult(null);

};

timer.Start();

return tcs.Task;

}

这个方法类似Task.Delay()方法。

二. 异步原则(补充)

同步操作:先完成其工作再返回调用者

异步操作:大部分工作则是在返回调用者之后才完成的,也称非阻塞方法。

异步编程的原则:

(1)以异步的方式编写运行时间很长(或者可能很长)的函数,会在一个新的线程或者任务上调用这些函数,从而实现需要的并发性。

(2)异步方法的并发性是在长时间运行的方法内启动的,而不是从这个方法外启动的。

I/O密集的并发性的实现不需要绑定线程(如1.5节的例子所示),因此可以提高可伸缩性和效率。富客户端应用程序可以减少工作线程的代码,因此可以简化工作线程安全性的实现。

Task支持延续,因此非常适合进行异步编程的,如1.5节的Delay方法。

在计算密集的方法中,我们使用Task.Run创建线程相关的异步性。但是异步编程的不同点在于,更希望将异步放在底层调用图上,

因此富客户端应用程序的高层方法就可以一直在UI线程上运行,访问控件、共享状态而不用担心会出现线程安全问题。

看Task.Run的例子:

//粗粒度并发

Task.Run(() => DisplayPrimeCounts());

/// <summary>

/// 显示素数个数

/// </summary>

void DisplayPrimeCounts()

{

for (int i = 0; i < 10; i++)

Console.WriteLine(GetPrimesCount(i * 1000000 + 2, 1000000) +

" primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));

Console.WriteLine("Done!");

}

/// <summary>

/// 获取素数个数

/// </summary>

int GetPrimesCount(int start, int count)

{

return

ParallelEnumerable.Range(start, count).Count(n =>

Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0));

}

这是一种粗粒度并发,如果想实现细粒度并发,需要编写异步的方法。

看异步版本:

DisplayPrimeCountsAsync();

Task DisplayPrimeCountsAsync()

{

var machine = new PrimesStateMachine();

machine.DisplayPrimeCountsFrom(0);

return machine.Task;

}

class PrimesStateMachine

{

TaskCompletionSource<object> _tcs = new TaskCompletionSource<object>();

public Task Task { get { return _tcs.Task; } }

/// <summary>

/// 异步显示素数个数

/// </summary>

/// <param name="i"></param>

public void DisplayPrimeCountsFrom(int i)

{

var awaiter = GetPrimesCountAsync(i * 1000000 + 2, 1000000).GetAwaiter();

awaiter.OnCompleted(() =>

{

Console.WriteLine(awaiter.GetResult()+" primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));

if (i++ < 10) DisplayPrimeCountsFrom(i);

else { Console.WriteLine("Done"); _tcs.SetResult(null); }

});

}

/// <summary>

/// 异步获取素数个数

/// </summary>

/// <param name="start"></param>

/// <param name="count"></param>

/// <returns></returns>

Task<int> GetPrimesCountAsync(int start, int count)

{

return Task.Run(() =>

ParallelEnumerable.Range(start, count).Count(n =>

Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));

}

}

可以看到改造以后的实现方式,很复杂。 GetPrimesCountAsync改为方法内部启动异步,DisplayPrimeCountsFrom通过TaskCompletionSource实现异步。

这时async和await登场!

async和await关键字极大地简化了程序的复杂度。

async/await版本:

DisplayPrimeCountsAsync();

/// <summary>

/// 异步显示素数个数

/// </summary>

async Task DisplayPrimeCountsAsync()

{

for (int i = 0; i < 10; i++)

Console.WriteLine(await GetPrimesCountAsync(i * 1000000 + 2, 1000000) +

" primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1));

Console.WriteLine("Done!");

}

/// <summary>

/// 异步获取素数个数

/// </summary>

Task<int> GetPrimesCountAsync(int start, int count)

{

return Task.Run(() =>

ParallelEnumerable.Range(start, count).Count(n =>

Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));

}

从编程形式上看,有点类似同步方法一样直观简洁。其实async/await编译器也是将其转换为一个状态机。通常我们称之为C#语法糖。

编译器背后的原理可以参考这篇文章:

三. 异步函数

这章开始进入异步函数的使用,由上面一章已经引出async/await关键字。可以使用同步的代码风格编写异步代码,极大地降低了异步编程的复杂度。

简单捋下async/await

如下语句中使用了await附加了延续,statement(s)是expression的延续。

这个“等待”被编译器转化为如下同等功能的代码。

这是如果想要成功编译就必须添加async修饰符,如下图提示。

async修饰符会指示编译器将await作为一个关键字而非标识符,来避免二义性(C#5之前有可能作为标识符使用),添加async修饰符的方法称为异步函数。

3.1 富客户端异步函数Demo

通过WPF的例子展示异步函数在富客户端应用程序中的作用:在执行计算密集的方法时,仍然保持UI的响应,不堵塞UI线程。

先看同步调用的情况:

private void ExecuteTaskOnClick(object sender, RoutedEventArgs e)

{

TextBoxMessage.Text = "Call Worker" + Environment.NewLine;

DoSomething();//同步调用

}

private void DoSomething()

{

Thread.Sleep(3000);//模拟计算密集耗时

TextBoxMessage.Text += "Calculate Done" + Environment.NewLine;

}

上图可以清楚的看到,当使用同步调用耗时方法时,UI线程无法响应用户事件请求,TextBox的信息显示也是等耗时方法结束后才更新。

原因是在耗时方法执行期间,UI线程已经被阻塞,UI线程接收的处理请求都会进入请求队列,无法及时响应(包括鼠标键盘的事件请求,控件更新),很影响用户体验。

下面看异步版本:

btnExecuteTaskAsync.Click += (sender, args) => ExecuteTaskAsync();

private async void ExecuteTaskAsync()

{

btnExecuteTaskAsync.IsEnabled = false;

TextBoxMessage.Text = "Call Worker Async" + Environment.NewLine;

await DoSomethingAsync();//异步调用

TextBoxMessage.Text += "Calculate Async Done" + Environment.NewLine;

btnExecuteTaskAsync.IsEnabled = true;

}

private async Task DoSomethingAsync()

{

await Task.Run(() =>

{

Thread.Sleep(3000); //模拟计算密集耗时

});

}

更改为异步版本后,在执行耗时任务时,UI线程没有被堵塞,可以正常响应用户事件和控件更新,提高了用户体验。

3.2 异步调用执行过程

根据3.1节的例子,整个调用过程如下:

当用户点击按钮时触发事件,事件调用ExecuteTaskAsync 方法,ExecuteTaskAsync 方法调用DoSomethingAsync方法,而后调用await,而await会使执行点返回给调用者,

当DoSomethingAsync方法完成(或者出现错误)时,执行点会从停止之处恢复执行DoSomethingAsync后面的代码。

ExecuteTaskAsync 方法则会'租用'UI线程的时间,即ExecuteTaskAsync 方法在消息循环注1中是以伪并发的方式执行的(执行会在UI线程的其他事件处理中穿插进行)。

在整个伪并发的过程中,只有await的过程中才会进行抢占,这就简化了线程的安全性。DoSomethingAsync会运行在工作线程上,正真的并发发生在DoSomethingAsync方法的Task.Run部分,在Task.Run部分尽量避免访问共享状态和UI组件。

本小节结尾完善一下上面的例子代码:

btnExecuteTaskAsync.Click += (sender, args) => ExecuteTaskAsync();

private async void ExecuteTaskAsync()

{

try

{

btnExecuteTaskAsync.IsEnabled = false;

TextBoxMessage.Text = "Calculate Async Start" + Environment.NewLine;

TextBoxMessage.Text += await DoSomethingAsync(); //异步调用

btnExecuteTaskAsync.IsEnabled = true;

}

catch (Exception e)

{

TextBoxMessage.Text += $"Error: {e.Message}" + Environment.NewLine;

}

finally

{

btnExecuteTaskAsync.IsEnabled = true;

}

}

private async Task<string> DoSomethingAsync()

{

await Task.Delay(3000); //模拟计算密集耗时

return "Calculate Async Done";

} 

增加了ExecuteTaskAsync方法的异常处理,给DoSomethingAsync方法添加了返回值Task<TResult>

还有一些关于优化方面的内容,简单提一下:

同步完成:执行过程在await之前就返回给调用者,同时这个方法会返回一个已经结束的任务。编译器会在同步完成的情况下跳过延续代码,会awaiter的IsCompleted属性来实现这种优化。

避免大量回弹: 对于一个在循环中多次调用的异步方法,通过调用ConfigureAwait方法可以避免该方法重复回弹到UI消息循环中。

它会阻止任务将延续提交到同步上下文中,将开销降低到了上下文切换的级别,该优化比较适合编写程序库。

四. 异步模式4.1 取消操作

在并发操作启动之后,需要能够取消任务,看如下示例:

private CancellationTokenSource? cts;

btnExecuteTaskAsync.Click += (sender, args) => ExecuteTaskAsync();

btnCancel.Click += (sender, args) => ExecuteCancelTask();

private async void ExecuteTaskAsync()

{

cts = new CancellationTokenSource();

try

{

btnExecuteTaskAsync.IsEnabled = false;

TextBoxMessage.Text = "Calculate Async Start" + Environment.NewLine;

TextBoxMessage.Text += await DoSomethingAsync(cts.Token); //异步调用

btnExecuteTaskAsync.IsEnabled = true;

}

catch (OperationCanceledException)

{

TextBoxMessage.Text += "任务已经取消!" + Environment.NewLine;

}

catch (Exception e)

{

TextBoxMessage.Text += $"Error: {e.Message}" + Environment.NewLine;

}

finally

{

btnExecuteTaskAsync.IsEnabled = true;

}

}

private async Task<string> DoSomethingAsync(CancellationToken cancellationToken)

{

for (int i = 0; i < 3; i++)

{

await Task.Delay(1000); //模拟计算密集耗时

cancellationToken.ThrowIfCancellationRequested();

}

return "Calculate Async Done";

}

private void ExecuteCancelTask()

{

cts?.Cancel();

}

在第3章结尾示例的基础上,添加了异步函数可取消功能。

通过实例化CancellationTokenSource类,可以得到取消令牌Token,当取消令牌调用Cancel()方法时,就会将IsCancellationRequested属性设置为True,同时任务会抛出OperationCanceledException。

在设计上将检查方法取消操作和启动取消操作分离开来,具有一定的安全性。

检查取消在CancellationTaken类上,取消动作在CancellationTokenSource类上。

看实际效果:

4.2 进度报告

一些异步操作需要在运行时报告其执行进度。一种简单的方案时向异步方法传入一个Action委托,在进度发生变化时就触发方法,在上面例子上添加了进度报告,如下:

private async void ExecuteTaskAsync()

{

cts = new CancellationTokenSource();

try

{

btnExecuteTaskAsync.IsEnabled = false;

TextBoxMessage.Text = "Calculate Async Start" + Environment.NewLine;

var result = await DoSomethingAsync(

(percent) => { TextBoxMessage.Text += "Current progress is " + percent + Environment.NewLine; },

cts.Token); //异步调用

TextBoxMessage.Text += result;

btnExecuteTaskAsync.IsEnabled = true;

}

catch (OperationCanceledException)

{

TextBoxMessage.Text += "任务已经取消!" + Environment.NewLine;

}

catch (Exception e)

{

TextBoxMessage.Text += $"Error: {e.Message}" + Environment.NewLine;

}

finally

{

btnExecuteTaskAsync.IsEnabled = true;

}

}

private async Task<string> DoSomethingAsync(Action<string> progressReport, CancellationToken cancellationToken)

{

for (int i = 1; i <= 10; i++)

{

await Task.Delay(500); //模拟计算密集耗时

progressReport($"{i * 10}%".ToString());

cancellationToken.ThrowIfCancellationRequested();

}

return "Calculate Async Done";

}

实现是简单,但是在富客户端应用程序中,有潜在的线程安全问题,由并发性对外暴露所产生的风险。

CLR拥有一对专门针对进度报告的类型:IProgress<T>接口和Progress<T>类 ,它们的作用包装一个委托,以便是UI应用程序可以通过同步上下文安全地报告进度。

private async void ExecuteTaskAsync()

{

cts = new CancellationTokenSource();

try

{

btnExecuteTaskAsync.IsEnabled = false;

TextBoxMessage.Text = "Calculate Async Start" + Environment.NewLine;

//通过Progress<T>构造函数接受一个Action<T>委托并对其进行包装

var result = await DoSomethingAsync(new Progress<string>((percent) =>

{

TextBoxMessage.Text += "Current progress is " + percent + Environment.NewLine;

})

, cts.Token); //异步调用

TextBoxMessage.Text += result;

btnExecuteTaskAsync.IsEnabled = true;

}

catch (OperationCanceledException)

{

TextBoxMessage.Text += "任务已经取消!" + Environment.NewLine;

}

catch (Exception e)

{

TextBoxMessage.Text += $"Error: {e.Message}" + Environment.NewLine;

}

finally

{

btnExecuteTaskAsync.IsEnabled = true;

}

}

private async Task<string> DoSomethingAsync(IProgress<string> progressReport, CancellationToken cancellationToken)

{

for (int i = 1; i <= 10; i++)

{

await Task.Delay(500); //模拟计算密集耗时

progressReport.Report($"{i * 10}%".ToString());

cancellationToken.ThrowIfCancellationRequested();

}

return "Calculate Async Done";

}

对上面的例子稍作改造,就实现使用IProgress<T>和Progress<T>来完成进度报告。

4.3 基于任务的异步模式TAP

一个TAP方法:

返回一个“热”Task或者Task<TResult>拥有Async后缀,除一些特殊情况或者是任务组合器若支持取消和进度报告,则需要拥有接受CancellationTaken或者IProgress<T>的重载。快速返回调用者对于I/O密集型任务不绑定线程

本文主要参考书籍: C#7.0核心技术指南

注1:UI线程上的消息循环的伪代码如下:

分类: C# 深入系列

标签: #net异步异常处理