我是C#中async/await功能的新手,经过一些研究,我认为我已经很好地理解了这些关键字想要实现的目标。但我想到了这个问题: 有些事情异步/等待使得使用任务类型无法完成的事情成为可能?请考虑以下示例:
static async Task<int> jobAsync()
{
// I could be writing here "await someTask()"
await Task.Delay(2000);
return 1;
}
static async void simpleAsync()
{
int i = await jobAsync();
Console.WriteLine("Async done. Result: " + i.ToString());
}
static void simpleTask()
{
var t = Task.Run(() => {
//I could be writing here "return someTask();"
Thread.Sleep(2000); return 1; });
t.ContinueWith(tsk => { Console.WriteLine("Task done. Result: " + tsk.Result); });
}
现在,两个函数 "simpleTask()" 和 "simpleAsync()" 给出相同的结果,例如,如果调用到 Main 方法中:
static void Main(string[] args)
{
simpleTask();
//simpleAsync();
Console.WriteLine("Doing other things...");
Console.ReadLine();
}
当然,这只是一个简单的例子,但是是什么让async/await真正有用呢?在什么情况下? 谢谢。
什么 c# async/await 可以做任务类型不能做的事情?
异步代码已经存在了很长时间。如果我不得不猜测,可能是 1960 年代。在 .NET 时间范围内,从一开始就存在异步代码模式 - 在 .NET 1.0 中。
因此,对上述问题的一个(有点迂腐)答案是"什么都没有"。async
和await
不会带来新功能;异步代码一直是可能的。
是什么让异步/等待真正有用?在什么情况下?
async
和await
的主要好处是代码可维护性。我有一个演讲,探讨了异步设计的演变,从事件到回调再到承诺,最后是async
/await
。在每一步中,代码都更容易维护,但在async
和await
出现之前,它永远不会接近与同步代码等效的可维护性。
具体来说,你的simpleTask
是使用带有延续的承诺;这是下一个最容易维护的模式。它类似于async
/await
,直到您尝试使用状态构建某些东西。试着用承诺来做这件事,你就会明白我的意思:
Task<int> GetDataCoreAsync();
async Task<int> GetDataAsync()
{
int retries = 10;
while (retries > 0)
{
try { return await GetDataCoreAsync(); }
catch (Exception ex)
{
--retries;
if (retries == 0)
throw;
}
}
}
了解async
和任务之间区别的关键在于我们所说的"消息泵"。多线程处理在现代 .NET 应用程序中的工作方式存在一个很大的循环。循环下降到您的代码中并执行所有内容。当控制权返回到消息泵时,其他事情可以轮到它们。
如果有人还记得以前的WinForms时代,则UI更新存在问题。有时,当程序处于循环状态时,UI 会完全冻结。解决方案是将Application.DoEvents();
添加到循环中。没有多少人真正理解这是做什么的。这会告诉消息泵休息一下并检查可能正在等待运行的其他代码。这包括在鼠标单击时调用的代码,从而神奇地解冻 UI。async
是同一概念的更现代方法。每当程序执行到达await
时,它就会以它认为合适的方式运行代码,而不会阻塞泵。
另一方面,任务总是(除了少数例外)启动一个新线程来处理阻塞问题。这有效,但async
可能会想出一个更好(更优化)的解决方案。任务的优点是能够并行运行多段同步代码,这允许在同一函数中继续执行,然后再完成。
这就引出了一个问题:我们什么时候会在下面演示的任务中使用async
?
Task.Run(async () => await MyFuncAsync());
评论中的任何想法将不胜感激。
我们可以深入研究大量非常详细的要点,但是您刚刚给出的示例中的一个简单而明显的要点是,您的 Main 方法被迫等待用户输入一行才能安全地结束程序。
如果这样做,则可以允许程序在所有异步工作完成后自动结束:
static async Task Main(string[] args)
{
var task = simpleAsync();
Console.WriteLine("Doing other things...");
await task;
}
static async Task simpleAsync()
{
int i = await jobAsync();
Console.WriteLine("Async done. Result: " + i.ToString());
}
您会注意到,为了使它起作用,除了 async/await 之外,我们实际上还利用了 Task 类。这一点很重要:async
/await
利用任务类型。这不是一个非此即彼的命题。
详细地说,重要的是要认识到您提供的simpleTask
示例不是关于"使用 Task 类型",而是"使用Task.Run()
"。Task.Run()
在完全不同的线程上执行给定的代码,这在编写 Web 应用程序和基于 GUI 的应用程序时会产生后果。而且,正如 Kevin Gosse 在下面的评论中指出的那样,您可以在返回的Task
上调用.Wait()
,以便在后台工作完成之前阻止您的主线程。如果您的代码在基于 GUI 的应用程序的 UI 线程上运行,这同样会产生一些严重的影响。
使用任务通常应该使用(通常避免Task.Run()
、.Wait()
和.Result
)可以为您提供良好的异步行为,并且比启动一堆不必要的线程开销更少。
如果您愿意接受这一点,那么async
/await
与不使用它的更好比较是利用Task.Delay()
和ContinueWith()
等方法,从每个方法返回Task
s 以保持异步,但从不使用async
关键字。
一般来说,使用async
/await
可以获得的大多数行为都可以通过编程直接复制Task
。您看到的最大区别在于可读性。在您的简单示例中,您可以看到异步如何使程序更简单:
static async Task simpleAsync()
{
int i = await jobAsync();
Console.WriteLine("Async done. Result: " + i.ToString());
}
static Task simpleTask()
{
return jobAsync().ContinueWith(i =>
Console.WriteLine("Async done. Result: " + i.ToString()));
}
但是,事情变得非常粗糙的地方是当您处理逻辑分支和错误处理时。这是async
/await
真正闪耀的地方:
static async Task simpleAsync()
{
int i;
try
{
i = await jobAsync();
}
catch (Exception e)
{
throw new InvalidOperationException("Failed to execute jobAsync", e);
}
Console.WriteLine("Async done. Result: " + i.ToString());
if(i < 0)
{
throw new InvalidOperationException("jobAsync returned a negative value");
}
await doSomethingWithResult(i);
}
您也许能够提出模仿此方法行为的非async
代码,但它确实很难看,并且在 IDE 中进行逐步调试会很困难,并且堆栈跟踪不会清楚地显示引发异常时发生的情况。这就是async
神奇地为你照顾的幕后魔力。