一年后添加
在使用 async-await 一年多之后,我知道我在原始答案中写的关于 async 的一些内容是不正确的,尽管答案中的代码仍然是正确的。 Hera 是两个链接,极大地帮助了我理解 async-await 的工作原理。
This interview Eric Lippert shows an excellent analogy for async-await。在中间某处搜索 async-await。
In this article, the ever so helpful Eric Lippert shows some good practices for async-await
原答案
好的,这是一个在学习过程中帮助我的完整示例。
假设您有一个速度很慢的计算器,并且您想在按下按钮时使用它。同时,您希望您的 UI 保持响应,甚至可以做其他事情。计算器完成后,您希望显示结果。
当然:为此使用 async / await,而不要使用任何旧方法,例如设置事件标志和等待设置这些事件。
这是慢速计算器:
private int SlowAdd(int a, int b)
{
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(5));
return a+b;
}
如果你想在使用 async-await 时异步使用它,你必须使用 Task.Run(...) 来异步启动它。 Task.Run 的返回值是一个等待任务:
-
Task如果你运行的函数的返回值为void
-
Task<TResult> 如果你运行的函数的返回值为TResult
您可以只启动任务,执行其他操作,并在需要任务结果时键入 await。有一个缺点:
如果您想“等待”,您的函数需要异步并返回 Task 而不是 void 或 Task<TResult> 而不是 TResult。
这是运行慢速计算器的代码。
使用 async 终止异步函数的标识符是一种常见的做法。
private async Task<int> SlowAddAsync(int a, int b)
{
var myTask = Task.Run ( () => SlowAdd(a, b));
// if desired do other things while the slow calculator is working
// whenever you have nothing to do anymore and need the answer use await
int result = await myTask;
return result;
}
旁注:有些人更喜欢 Task.Factory.StartNew 而不是 Start.Run。看看 MSDN 是怎么说的:
MSDN: Task.Run versus Task.Factory.StartNew
SlowAdd 作为异步函数启动,您的线程继续运行。一旦它需要答案,它就会等待任务。返回值为 TResult,在本例中为 int。
如果你没有什么有意义的事情要做,代码看起来像这样:
private async Task`<int`> SlowAddAsync(int a, int b)
{
return await Task.Run ( () => SlowAdd(a, b));
}
注意,SlowAddAsync 被声明为异步函数,所以使用这个异步函数的每个人也应该是异步的,并返回 Task 或 Task<TResult>:
private async Task UpdateForm()
{
int x = this.textBox1.Text;
int y = this.textBox2.Text;
int sum = await this.SlowAddAsync(x, y);
this.label1.Text = sum.ToString();
}
关于 async / await 的好处是您不必摆弄 ContinueWith 来等待前一个任务完成。只需使用 await,您就知道任务已完成并且您有返回值。 await 之后的语句是您通常在 ContinueWith 中执行的操作。
顺便说一句,你的Task.Run不必调用函数,你也可以在里面放一个语句块:
int sum = await Task.Run( () => {
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(5));
return a+b});
然而,一个单独的函数的好处是你让那些不需要/想要/理解异步的人,可以在没有异步/等待的情况下使用该函数。
记住:
每个使用 await 的函数都应该是异步的
每个异步函数都应该返回 Task 或 Task<Tresult>
“但我的事件处理程序无法返回任务!”
private void OnButton1_Clicked(object sender, ...){...}
你是对的,因此这是唯一的例外:
异步事件处理程序可能返回 void
因此,当单击按钮时,异步事件处理程序将使 UI 保持响应:
private async void OnButton1_Clicked(object sender, ...)
{
await this.UpdateForm();
}
但是您仍然必须将事件处理程序声明为异步
许多 .NET 函数都有返回 Task 或 Task<TResult> 的异步版本。
有异步函数
- 互联网
- 流式读写
- 数据库访问
- 等等。
要使用它们,您不必调用 Task.Run,它们已经返回 Task 和 Task<TResult> 只需调用它们,继续做你自己的事情,当你需要答案时等待 Task 并使用 TResult .
开始几个任务并等待它们完成
如果您启动多个任务并希望等待所有任务完成,请使用 Task.WhenAll(...) NOT Task.Wait
Task.Wait 返回一个 void。 Task.WhenAll 返回一个任务,所以你可以等待它。
一旦一个任务完成,返回值已经是await的返回值,但是如果你await Task.WhenAll(new Task[]{TaskA,TaskB,TaskC});
您必须使用 Task<TResult>.Result 属性才能知道任务的结果:
int a = TaskA.Result;
如果其中一项任务引发异常,则会将其作为 InnerExceptions 包装在 AggregateException 中。因此,如果您等待 Task.WhenAll,请准备好捕获 AggregateException 并检查 innerExceptions 以查看您启动的任务引发的所有异常。使用 AggregateException.Flatten 函数可以更轻松地访问异常。
关于取消的有趣阅读:
MSDN about Cancellation in managed threads
最后:您使用 Thread.Sleep(...)。异步版本是 Task.Delay(TimeSpan):
private async Task`<int`> MySlowAdd(int a, int b)
{
await Task.Delay(TimeSpan.FromSeconds(5));
return a+b;
}
如果您使用此功能,您的程序将保持响应。