什么 c# 异步/等待可以做任务类型不能做的事情?



我是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 中。

因此,对上述问题的一个(有点迂腐)答案是"什么都没有"。asyncawait不会带来新功能;异步代码一直是可能的。

是什么让异步/等待真正有用?在什么情况下?

asyncawait的主要好处是代码可维护性。我有一个演讲,探讨了异步设计的演变,从事件到回调再到承诺,最后是async/await。在每一步中,代码都更容易维护,但在asyncawait出现之前,它永远不会接近与同步代码等效的可维护性。

具体来说,你的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()等方法,从每个方法返回Tasks 以保持异步,但从不使用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神奇地为你照顾的幕后魔力。

相关内容

最新更新