异步函数
异步函数是对TPL(任务并行库)的上层抽象,使用async/await操作符,异步函数必须返回Task或者 Task<T>类型;返回async void也可以,但有可能会出现危险代码(异常未处理),为了程序规范不建议使用,唯一建议用的地方是在UI事件处理器中。
只能在async方法内部使用await操作符,在其他地方使用会出现编译错误。
不允许在catch、finally、lock和unsafe代码块中使用await操作符,不允许对任何异步函数使用ref或out参数。
Main方法不能使用async修饰。
使用async会有性能损失,编译器会生成一些复杂的程序结构。
使用await操作符获取异步任务结果
当程序运行时运行了两个异步操作。其中一个是标准的TPL模式的代码,第二个使用了C#的新特性async和 await。AsyncWithTPL方法启动了一个任务,运行两秒后返回关于工作线程信息的字符串。然后我们定义了一个后续操作,用于在异步操作完成后打印出该操作结果,还有另一个后续操作,用于万一有错误发生时打印出异常的细节。最终,返回了一个代表其中一个后续操作任务的任务,并等待其在 Main函数中完成。
在AsyncWithAwait方法中,对任务使用await并得到了相同的结果。如果任务完成时带有错误则捕获异常。关键不同的是这实际上是一个异步程序。使用await后,C#立即创建了一个任务,其有一个后续操作任务,包含了await操作符后面的所有剩余代码。这个新任务也处理了异常传播。然后,将该任务返回到主方法中并等待其完成。
因此可以看到程序的第1部分和第2部分在概念上是等同的,但是在第2部分中C#编译器隐式地处理了异步代码。在Windows GUI或ASP.NET之类的环境中不推荐使用Task.Wait和 Task.Result方法,可能会导致死锁。
可以取消GetInfoAsync的异常注释来调试异常处理是否正常工作。
static void Main(string[] args)
{
//1
Task t = AsyncWithTPL();
t.Wait();
//2
t = AsyncWithAwait();
t.Wait();
Console.ReadKey();
}
static Task AsyncWithTPL()
{
Task<string> t = GetInfoAsync("task1");
Task tt = t.ContinueWith(task => Console.WriteLine(t.Result), TaskContinuationOptions.NotOnFaulted);
Task ttt = t.ContinueWith(task => Console.WriteLine(t.Exception.InnerException), TaskContinuationOptions.OnlyOnFaulted);
return Task.WhenAny(tt, ttt);
}
async static Task AsyncWithAwait()
{
try
{
string result = await GetInfoAsync("task2");
Console.WriteLine(result);
}
catch(Exception ex)
{
Console.WriteLine(ex);
}
}
async static Task<string> GetInfoAsync(string name)
{
await Task.Delay(2000);
//throw new Exception("出错了!");
return string.Format("线程ID为{0}的线程正在运行任务 {1},是否线程池线程 {2}", Thread.CurrentThread.ManagedThreadId, name, Thread.CurrentThread.IsThreadPoolThread);
}
在lambda表达式中使用await操作符
用async关键字声明了一个参数为string的lambda表达式,它的返回为Task<string>类型。
static void Main(string[] args)
{
Task t = AsyncProcessing();
t.Wait();
Console.ReadKey();
}
async static Task AsyncProcessing()
{
Func<string, Task<string>> asyncLambda = async name =>
{
await Task.Delay(2000);
return string.Format("线程ID为{0}的线程正在运行任务 {1},是否线程池线程 {2}", Thread.CurrentThread.ManagedThreadId, name, Thread.CurrentThread.IsThreadPoolThread);
};
string res = await asyncLambda("我是异步的lambda");
Console.WriteLine(res);
}
连续的异步任务使用await操作符
AsyncWithAwait方法使用了两个await声明,task2只有等之前的task1完成后才会开始执行。当使用await时如果一个任务已经完成,将会异步地得到该任务结果。否则,当在代码中看到await声明时,通常的行为是方法执行到该await代码行时将立即返回,并且剩下的代码将会在一个后续操作任务中运行。
task2故意抛出一个异常进行异常处理测试。
static void Main(string[] args)
{
Task t = AsyncWithAwait();
t.Wait();
Console.ReadKey();
}
async static Task AsyncWithAwait()
{
try
{
string result = await GetInfoAsync("task1");
Console.WriteLine(result);
result = await GetInfoAsync("task2");
Console.WriteLine(result);
}
catch(Exception ex)
{
Console.WriteLine(ex);
}
}
async static Task<string> GetInfoAsync(string name)
{
Console.WriteLine("任务 {0} 启动",name);
await Task.Delay(2000);
if(name=="task2")
throw new Exception("出错了!");
return string.Format("线程ID为{0}的线程正在运行任务 {1},是否线程池线程 {2}", Thread.CurrentThread.ManagedThreadId, name, Thread.CurrentThread.IsThreadPoolThread);
}
并行的异步任务使用await操作符
下面定义了两个异步任务,分别运行3秒和5秒。然后使用Task.WhenAll辅助方法创建了另一个任务,该任务只有在所有底层任务完成后才会运行。5秒后,我们获取了所有结果,说明了这些任务是同时运行的。
当运行该程序时,这两个任务可能是线程池中的同一个工作线程执行。
注释掉GetIntroAsync方法中的await Task.Delay代码行,并解除对await Task.Run代码行的注释,然后再次运行程序,会看到该情况下两个任务会被不同的工作线程执行。
不同之处是Task.Delay其实使用了一个计时器,过程如下:从线程池中获取工作线程,它将等待Task.Delay方法返回结果。然后,Task.Delay方法启动计时器并指定一块代码,该代码会在计时器时间到了Task.Delay方法中指定的秒数后被调用。之后立即将工作线程返回到线程池中。当计时器事件运行时,从线程池中任意获取一个可用的工作线程(可能就是运行一个任务时使用的线程)并运行计时器提供给它的代码。
当使用Task.Run方法时,从线程池中获取了一个工作线程并将其阻塞几秒。然后获取了第二个工作线程并且也将其阻塞。在这种场景下,我们消费了两个工作线程并且让它们什么都不做,这种方式无法重用已空闲的线程。
应尽量使用Task.Delay而不是Thread.Sleep。
static void Main(string[] args)
{
Task t = AsyncProcessing();
t.Wait();
Console.ReadKey();
}
async static Task<string> GetInfoAsync(string name, int seconds)
{
await Task.Delay(TimeSpan.FromSeconds(seconds));
//await Task.Run(() => Thread.Sleep(TimeSpan.FromSeconds(seconds)));
return string.Format("线程ID为{0}的线程正在运行任务 {1},是否线程池线程 {2}", Thread.CurrentThread.ManagedThreadId, name, Thread.CurrentThread.IsThreadPoolThread);
}
async static Task AsyncProcessing()
{
Task<string> t1 = GetInfoAsync("task1", 3);
Task<string> t2 = GetInfoAsync("task2", 5);
string[] results = await Task.WhenAll(t1, t2);
foreach (var res in results)
{
Console.WriteLine(res);
}
}
处理异步操作中的异常
下面编写了3个场景的异常情况
static void Main(string[] args)
{
Task t = AsyncProcessing();
t.Wait();
Console.ReadKey();
}
async static Task<string> GetInfoAsync(string name, int seconds)
{
await Task.Delay(TimeSpan.FromSeconds(seconds));
throw new Exception($"{name} 出错了!");
}
async static Task AsyncProcessing()
{
//1
Console.WriteLine("1.单个异常");
try
{
string res = await GetInfoAsync("task1", 2);
Console.WriteLine(res);
}
catch(Exception e)
{
Console.WriteLine(e);
}
Console.WriteLine(".....................................................................................");
//2
Console.WriteLine("2.多个异常");
Task<string> t1 = GetInfoAsync("task1", 3);
Task<string> t2 = GetInfoAsync("task2", 2);
try
{
string[] ress = await Task.WhenAll(t1, t2);
Console.WriteLine(ress.Length);
}
catch(Exception e)
{
Console.WriteLine(e);
}
Console.WriteLine(".....................................................................................");
//3
Console.WriteLine("3.聚合多个异常");
t1 = GetInfoAsync("task1", 3);
t2 = GetInfoAsync("task2", 2);
Task<string[]> t3 = Task.WhenAll(t1, t2);
try
{
string[] ress = await t3;
Console.WriteLine(ress.Length);
}
catch
{
var ex = t3.Exception.Flatten();
var ex2 = ex.InnerExceptions;
Console.WriteLine("异常个数:{0}", ex2.Count);
foreach (var e in ex2)
{
Console.WriteLine(e);
Console.WriteLine();
}
}
}
避免使用捕获的同步上下文
同步上下文:在UI线程中访问UI组件的方法。如果要访问UI组件,必须由UI线程操作,否则会抛出异常。如果访问组件的线程不是UI线程,那么通过同步上下文可以安排UI线程完成此操作。
默认情况下,await会尝试捕获同步上下文,并在其中执行代码。如果不需要访问UI组件,则不需要同步上下文,捕获同步上下文运行速度会变慢。
var t =Task.Run (() => {});
//这一句表示不捕获同步上下文
await t.configureAwait(continueonCapturedcontext : false);
使用自定义的awaitable类型
Await表达式的任务被要求是awaitable。如果一个表达式t满足下面任意一条则认为是awaitable的:
1.t是动态编译时的类型2.t有一个名为GetAwaiter的可访问的实例或扩展方法。该方法没有参数和类型参数,并且返回值类型A满足以下所有条件:
(1)A实现了System.Runtime.CompilerServices.INotifyCompletion接口(为简单起见,以后简称为 INotifyCompletion)。
(2)A有一个可访问的、可读的类型为bool的实例属性IsCompleted。
(3)A有一个名为GetResult的可访问的实例方法,该方法没有任何参数和类型参数。
首先定义一个awaitable类型MyAwaitable,并实现GetAwaiter方法,该方法返回一个MyAwaiter类型的实例。MyAwaiter实现了INotifyCompletion接口,拥有类型为bool的 IsCompleted属性,并且有GetResult方法,该方法返回一个字符串类型。最后,创建两个MyAwaitable对象并对其使用await关键字。
基本上,如果IsCompleted属性返回true,则只需同步调用GetResult方法。这种做法防止了该操作已经完成后我们仍然为执行异步任务而分配资源。通过给MyAwaitable对象的构造函数传递completeSync参数来展示该场景。
另外,我们给MyAwaiter 的OnCompleted方法注册了一个回调函数并启动该异步操作。当操作完成时,就会调用提供的回调函数,该回调函数将会通过调用MyAwaiter对象的GetResult方法来获取结果。自定义的awaitable一般不需要使用,使用Task可满足绝大部分场景。
class MyAwaitable
{
private readonly bool completeSync;
public MyAwaitable(bool completeSync)
{
this.completeSync = completeSync;
}
public MyAwaiter GetAwaiter()
{
return new MyAwaiter(completeSync);
}
}
class MyAwaiter:INotifyCompletion
{
private string result = "同步完成";
public bool CompleteSync { get; }
public MyAwaiter(bool completeSync)
{
CompleteSync = completeSync;
}
public bool IsCompleted
{
get
{
return CompleteSync;
}
}
public string GetResult()
{
return result;
}
public void OnCompleted(Action continuation)
{
ThreadPool.QueueUserWorkItem(state =>
{
Thread.Sleep(TimeSpan.FromSeconds(1));
result = GetInfo();
continuation?.Invoke();
});
}
private string GetInfo()
{
return string.Format("线程ID为{0}的线程正在运行,是否线程池线程 {1}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
}
}
//using System.Runtime.CompilerServices;
//using System.Threading;
//using System.Threading.Tasks;
static void Main(string[] args)
{
Task t = AsyncProcessing();
t.Wait();
Console.ReadKey();
}
async static Task AsyncProcessing()
{
var sync = new MyAwaitable(true);
string result = await sync;
Console.WriteLine(result);
var async = new MyAwaitable(false);
result = await async;
Console.WriteLine(result);
}