.NET4.5 异步编程 async和await

时间:2023-12-17 18:15:44

msdn介绍:https://msdn.microsoft.com/zh-cn/library/hh191443.aspx

其实很简单,标记了async的方法为异步方法,从方法的左大括号开始同步执行,直到第一个await出现就开始异步执行,主线程等待,等带await这行代码异步完了再回到主线程,然后继续往下执行。
如果后面又遇到带await语句的,又异步执行,执行完了就回来,继续同步往下。依此类推。
这样做其实就把我们以前编写等待句柄接收信号的代码给省掉了,就一个await就搞定。

Visual Basic 中的 AsyncAwait 关键字,以及 C# 中的 asyncawait 关键字都是异步编程的核心。 通过使用这两个关键字,你可以使用 .NET framework 或 Windows 运行时中的资源轻松创建异步方法(几乎与创建同步方法一样轻松)。 通过使用被称为异步方法的 async 和 await 定义的异步方法。

以下特征总结了使上一个示例成为异步方法的原因。

  • 方法签名包含一个 Asyncasync 修饰符。

  • 按照约定,异步方法的名称以“Async”后缀为结尾。

  • 返回类型为下列类型之一:

    • 如果你的方法有操作数为 TResult 类型的返回语句,则为 Task<TResult>

    • 如果你的方法没有返回语句或具有没有操作数的返回语句,则为 Task

    • 如果你编写的是异步事件处理程序,则为 Void(Visual Basic 中为 Sub)。

    有关详细信息,请参见本主题后面的“返回类型和参数”。

  • 方法通常包含至少一个 await 表达式,该表达式标记一个点,在该点上,直到等待的异步操作完成方法才能继续。 同时,将方法挂起,并且控件返回到方法的调用方。 本主题的下一节将解释悬挂点发生的情况。

在异步方法中,可使用提供的关键字和类型来指示需要完成的操作,且编译器会完成其余操作,其中包括持续跟踪控件以挂起方法返回等待点时发生的情况。 一些常规流程(例如,循环和异常处理)在传统异步代码中处理起来可能很困难。 在异步方法中,元素的编写频率与同步解决方案相同且此问题得到解决。

.NET4.5 异步编程 async和await

关系图中的数值对应于以下步骤。

  1. 事件处理程序调用并等待 AccessTheWebAsync 异步方法。

  2. AccessTheWebAsync 可创建 HttpClient 实例并调用 GetStringAsync 异步方法以下载网站内容作为字符串。

  3. GetStringAsync 中发生了某种情况,该情况挂起了它的进程。 可能必须等待网站下载或一些其他阻止活动。 为避免阻止资源,GetStringAsync 会将控制权出让给其调用方 AccessTheWebAsync。

    GetStringAsync 返回 Task<TResult>,其中 TResult 为字符串,并且 AccessTheWebAsync 将任务分配给 getStringTask 变量。 该任务表示调用 GetStringAsync 的正在进行的进程,其中承诺当工作完成时产生实际字符串值。

  4. 由于尚未等待 getStringTask,因此,AccessTheWebAsync 可以继续执行不依赖于 GetStringAsync 得出的最终结果的其他工作。 该任务由对同步方法 DoIndependentWork 的调用表示。

  5. DoIndependentWork 是完成其工作并返回其调用方的同步方法。

  6. AccessTheWebAsync 已用完工作,可以不受 getStringTask 的结果影响。 接下来,AccessTheWebAsync 需要计算并返回该下载字符串的长度,但该方法仅在具有字符串时才能计算该值。

    因此,AccessTheWebAsync 使用一个 await 运算符来挂起其进度,并把控制权交给调用 AccessTheWebAsync 的方法。 AccessTheWebAsync 将 Task(Of Integer)Task<int> 返回至调用方。 该任务表示对产生下载字符串长度的整数结果的一个承诺。

    .NET4.5 异步编程 async和await注意

    如果 GetStringAsync(因此 getStringTask)在 AccessTheWebAsync 等待前完成,则控件会保留在 AccessTheWebAsync 中。 如果异步调用过程 (AccessTheWebAsync) 已完成,并且 AccessTheWebSync 不必等待最终结果,则挂起然后返回到 getStringTask 将造成成本浪费。

    在调用方内部(此示例中的事件处理程序),处理模式将继续。 在等待结果前,调用方可以开展不依赖于 AccessTheWebAsync 结果的其他工作,否则就需等待片刻。 事件处理程序等待 AccessTheWebAsync,而 AccessTheWebAsync 等待 GetStringAsync

  7. GetStringAsync 完成并生成一个字符串结果。 字符串结果不是通过按你预期的方式调用 GetStringAsync 所返回的。 (记住,该方法已返回步骤 3 中的一个任务)。 相反,字符串结果存储在表示完成 getStringTask 方法的任务中。 await 运算符从 getStringTask 中检索结果。 赋值语句将检索到的结果赋给 urlContents。

  8. 当 AccessTheWebAsync 具有字符串结果时,该方法可以计算字符串长度。 然后,AccessTheWebAsync 工作也将完成,并且等待事件处理程序可继续使用。 在此主题结尾处的完整示例中,可确认事件处理程序检索并打印长度结果的值。

如果你不熟悉异步编程,请花 1 分钟时间考虑同步行为和异步行为之间的差异。 当其工作完成时(第 5 步)会返回一个同步方法,但当其工作挂起时(第 3 步和第 6 步),异步方法会返回一个任务值。 在异步方法最终完成其工作时,任务会标记为已完成,而结果(如果有)将存储在任务中。

示例:

public class MyClass
{
public MyClass()
{
DisplayValue(); //这里不会阻塞
System.Diagnostics.Debug.WriteLine("MyClass() End.");
}
public Task<double> GetValueAsync(double num1, double num2)
{
return Task.Run(() =>
{
for (int i = 0; i < 1000000; i++)
{
num1 = num1 / num2;
}
return num1;
});
}
public async void DisplayValue()
{
double result = await GetValueAsync(1234.5, 1.01);//此处会开新线程处理GetValueAsync任务,然后方法马上返回
//这之后的所有代码都会被封装成委托,在GetValueAsync任务完成时调用
System.Diagnostics.Debug.WriteLine("Value is : " + result);
}
}

  

现有已写好的帮助类。使用很简单,将方法名作为参数传进去就行了,最常用的是把很耗时的序列化函数传进去,以免阻塞UI进程,造成卡顿现象,影响用户体验。

	public static class TaskAsyncHelper
{
/// <summary>
/// 将一个方法function异步运行,在执行完毕时执行回调callback
/// </summary>
/// <param name="function">异步方法,该方法没有参数,返回类型必须是void</param>
/// <param name="callback">异步方法执行完毕时执行的回调方法,该方法没有参数,返回类型必须是void</param>
public static async void RunAsync(Action function, Action callback)
{
Func<System.Threading.Tasks.Task> taskFunc = () =>
{
return System.Threading.Tasks.Task.Run(() =>
{
function();
});
};
await taskFunc();
if (callback != null)
callback();
} /// <summary>
/// 将一个方法function异步运行,在执行完毕时执行回调callback
/// </summary>
/// <typeparam name="TResult">异步方法的返回类型</typeparam>
/// <param name="function">异步方法,该方法没有参数,返回类型必须是TResult</param>
/// <param name="callback">异步方法执行完毕时执行的回调方法,该方法参数为TResult,返回类型必须是void</param>
public static async void RunAsync<TResult>(Func<TResult> function, Action<TResult> callback)
{
Func<System.Threading.Tasks.Task<TResult>> taskFunc = ()=>
{
return System.Threading.Tasks.Task.Run(()=>
{
return function();
});
};
TResult rlt = await taskFunc();
if(callback != null)
callback(rlt);
}
}

  

避免async void

异步方法返回类型有3种,void,Task和Task<T>,void尽量不要使用。

原理剖析:

使用async void标记的方法有不同的错误处理语义。async Task或async Task<T>方法抛出异常时,异常会被捕获并放到Task对象上。然而,标记为async void的方法没有Task对象,所以async void方法抛出的任何异常都会直接放到SynchronizationContext(异步上下文)上,它是在async void方法开始的时候激活的。下面是一个例子:

.NET4.5 异步编程 async和await
//async void 方法不能被捕获的异常
private async void ThrowExceptionAsync()
{
throw new InvalidOperationException();
}
public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
try
{
ThrowExceptionAsync();
}
catch (Exception )
{
//异常不会被捕获
throw;
}
}
.NET4.5 异步编程 async和await

async void有不同的组成语法。返回Task或Task<T>的async方法可以使用await Task.WhenAny或Task.WhenAll等轻易组合。而返回void的async方法没有提供简单的方式来通知它们已经完成的调用代码。启用若干个async void方法很容易,但不容易决定它们什么时候完成。async void方法开始和完成时会通知它们的SynchronizationContext,但是自定义的SynchronizationContext对于常规应用代码是一个复杂的解决方案。

async void方法测试很困难。由于错误处理和组合的差异,编写调用async void方法的单元测试很困难。

很明显,async void方法与async Task方法相比有很多劣势,但在一个特殊场合很有用,那就是异步的事件句柄。它们直接将异常抛出到SynchronizationContext,这与同步的事件句柄表现很相似。同步的事件句柄通常是私有的,因此它们不能被组合或者直接测试。我想采取的方法是在异步事件句柄中最小化代码,比如,让它await一个包含实际逻辑的async Task方法,代码如下:

.NET4.5 异步编程 async和await
private async void button1_Click(object sender, EventArgs e)
{
await Button1ClickAsync();
}
public async Task Button1ClickAsync()
{
//处理异步工作
await Task.Delay(1000);
}
.NET4.5 异步编程 async和await

总之,对于async Task和async void,你应该更喜欢前者。async Task方法更容易错误处理,组合和测试。对于异步的事件句柄异常,必须返回void。

一直使用async

这句话的意思是,不要不经过认真考虑就混合同步和异步代码。特别地,在异步代码上使用Task.Wait或Task.Result是一个馊主意。

下面是一个简单的例子:一个方法阻塞了异步方法的结果。在控制台程序中会工作的很好,但是从GUI或者ASP.Net上下文中调用的时候就会死锁。死锁的实际原因是当调用Task.Wait的时候进一步开启了调用栈。

.NET4.5 异步编程 async和await
//阻塞异步代码时的一个常见死锁问题
public static class DeadlockDemo
{
private static async Task DelayAsync()
{
await Task.Delay(1000);
}
// 调用 GUI 或 ASP.NET 上下文的时候会造成死锁
public static void Test()
{
// 开始延迟.
var delayTask = DelayAsync();
// 等待延迟
delayTask.Wait();
}
}
.NET4.5 异步编程 async和await

造成这种死锁的根本原因是等待处理上下文的方式。默认情况下,当一个未完成的Task处于被等待状态时,当前上下文会被捕获并且当此任务完成时恢复该方法。这个上下文如果不为null就是当前的SynchronizationContext,在这种情况下,它是当前的TaskScheduler(任务调度者)。GUI 和ASP.NET应用有一个SynchronizationContext,它只允许一次运行一大块代码。当await完成时,它尝试在捕获的上下文内执行异步方法的剩余部分。但是该上下文已经有一个线程了,它在(同步地)等待这个async方法的完成。它们每一个都在等待另一个,造成了死锁。

注意控制台程序不会造成这种死锁。它们有个线程池SynchronizationContext而没有一次执行一大坨代码的SynchronizationContext,因此当await完成时,它在线程池线程上调度该async方法的剩余部分。该方法可以完成,它完成了返回task,并没有死锁。

总之,应该避免混合async和阻塞的代码。这样做的话会造成死锁,更复杂的错误处理和上下文线程不可预测的阻塞。

配置上下文

可以查看我的另一篇博客《Async and Await 异步和等待》的“避免上下文”部分。

这里稍加补充如下:

除了性能方面之外,ConfigureAwait还有另一个重要的方面:它可以避免死锁。在“一直使用async”的代码示例中,再次思考一下:如果你在DelayAsync代码行添加“ConfigureAwait(false)”,那么死锁就会避免。这次,当await完成时,它尝试在线程池上下文内执行async方法的剩余部分。该方法可以完成,完成后返回task,并且没有死锁。这项技术对于逐渐将应用从同步转为异步特别有用。

建议将ConfigureAwait用在方法中的每个await之后。只有当未完成的Task被等待时,才会唤起上下文被捕获;如果Task已经完成了,那么上下文不会被捕获。

.NET4.5 异步编程 async和await
async Task MyMethodAsync()
{
//这里的代码运行在原始 context.
await Task.FromResult(1);
//这里的代码运行在原始 context.
await Task.FromResult(1).ConfigureAwait(continueOnCapturedContext: false);
// 这里的代码运行在原始 context.
var random = new Random();
int delay = random.Next(2); // delay是 0 or 1
await Task.Delay(delay).ConfigureAwait(continueOnCapturedContext: false);
// 这里的代码不确定是否运行在原始 context. }
.NET4.5 异步编程 async和await

每个异步方法都有自己的上下文,因此如果一个异步方法调用另一个异步方法,那么它们的上下文是独立的。

.NET4.5 异步编程 async和await
private async Task HandleClickAsync()
{
// 这里可以使用ConfigureAwait
await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext: false);
}
private async void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
try
{
// 这里不能使用 ConfigureAwait
await HandleClickAsync();
}
finally
{
// 返回到这个方法的原始上下文
button1.Enabled = true;
}
}
.NET4.5 异步编程 async和await

今天就写到这里吧,还有很多很高级的用法,需要自己好好研究一下才能分享出来,希望大家多多支持!