前言:
此时我们对“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异步异常处理