我们应该如何使用async await?

43

我正在研究如何使用async await,但是当我们有多个方法互相调用时,我并不太明白。我们应该总是使用await,还是只有在我们真正准备好使用结果时才使用await?

例如,我们应该像这样做:

async Task<string[]> FooAsync()
{
    var info = await Func1();
    return info.split('.');
}

async Task<string> Func1()
{
    return await Func2();
}

async Task<string> Func2()
{
    return await tcpClient.ReadStringAsync();
}
async Task<string[]> FooAsync()
{
    var info = await Func1();
    return info.split('.');
}

Task<string> Func1()
{
    return Func2();
}

Task<string> Func2()
{
    return tcpClient.ReadStringAsync();
}

根据示例1,我们是否应该在每个方法中都使用await?
还是
根据示例2,我们仅在开始使用结果时才在最上层的方法中使用await?


7
每次调用 await 时,都会创建一段代码并通过状态机运行。通过 返回 一个 任务,有时可以提高效率。但是,在某些情况下,您可能希望检查返回的任务(将被 awaited)是否存在任何异常。我认为,从这里开始,您应该查看 Stephen Cleary 和 Stephen Toubs 关于异步和等待模式的博客。 - TheGeneral
2
这取决于情况。如果您不想对结果进行任何操作,则无需使用await,只需返回该值即可。但是调试会变得有点困难,因为您无法在调试期间在Func2本身中检查返回值,也无法在本地处理异常。 - Panagiotis Kanavos
2
我可以推荐David Fowler的AsyncGuidance。在这种情况下:优先使用 async/await,而不是直接返回 Task - kapsiR
另外,当你等待一个任务时,并不能保证你仍然在同一线程上。但可以说这并不重要。此外,在错误的位置等待任务可能会导致死锁。 - NtFreX
5个回答

32

每次调用 await 时,它都会创建一段代码来捆绑变量,捕获同步上下文(如果适用),并创建一个到 IAsyncStateMachine 的续集。

实际上,在没有 async 关键字的情况下返回一个 Task 可以让你在运行时具有小的效率和节省大量的CIL。请注意,.NET 中的异步特性已经进行了很多优化。还要注意(非常重要的是)在 using 语句中返回一个 Task 很可能会抛出一个 已释放异常

您可以在此处比较CIL和管道差异

因此,如果您的方法只是转发一个 Task 而不想要任何东西,您可以轻松地放弃 async 关键字并直接返回 Task

此外,有时我们需要做更多的转发并涉及分支。这就是 Task.FromResultTask.CompletedTask 的作用,帮助处理方法中可能出现的逻辑。例如,如果你要即时给出一个结果,或者返回一个已经完成Task

最后,当处理异常时,异步和等待模式与使用 Task 返回的方法存在微妙的差别。如果你正在返回一个Task,你可以使用Task.FromException<T> 来弹出任何在返回的 Task上的异常,就像async方法所做的那样。

无意义示例

public Task<int> DoSomethingAsync(int someValue)
{
   try
   {
      if (someValue == 1)
         return Task.FromResult(3); // Return a completed task

      return MyAsyncMethod(); // Return a task
   }
   catch (Exception e)
   {
      return Task.FromException<int>(e); // Place exception on the task
   }
}

简单来说,如果你不太明白发生了什么,只需await就可以了;开销将是最小的。然而,如果你理解如何返回一个任务结果,一个完成的任务,在任务上放置一个异常,或者只是转发。你可以通过放弃使用async关键字直接返回任务并绕过IAsyncStateMachine来节省一些CIL并提高代码性能。


大约在这个时候,我会查找Stack Overflow用户和作者Stephen Cleary,以及Mr. Parallel Stephen Toub。他们有大量博客和书籍专门致力于异步和等待模式,所有陷阱、编码规范和更多信息,您肯定会发现有趣的。


Stephen Cleary的博客相关链接,也在Fabio的答案中提到:省略Async和Await - Luca Cremonesi
await 会确保方法在相同的 UI 上下文中恢复,而不是相同的线程? - variable
@variable 是的,确实处于相同的同步/执行上下文和相同的线程。 - TheGeneral

14

两个选项都是合法的,每个选项都有适用于它比另一个更有效的情况。

当您想要处理异步方法的结果或在当前方法中处理可能的异常时,始终使用await。

public async Task Execute()
{
    try
    {
        await RunAsync();
    }
    catch (Exception ex)
    {
        // Handle thrown exception
    }
}

如果在当前方法中不使用异步方法的结果,请返回Task。这种方法将延迟状态机的创建到调用方或任何最终任务将被等待的地方。正如评论中指出的,可以使执行效率更高一些。

但是有些情况下,即使您不需要处理结果也不想处理可能的异常,仍必须等待任务完成。

public Task<Entity> GetEntity(int id)
{
    using (var context = _contextFactory.Create())
    {
        return context.Entities.FindAsync(id);
    }
}
在上述情景中,FindAsync可能会返回未完成的任务,而这个任务会立即返回给调用方并且会释放在using语句中创建的context对象。
稍后当调用方等待任务时,由于它将尝试使用已释放的对象(context),因此将抛出异常。
public async Task<Entity> GetEntity(int id)
{
    using (var context = _contextFactory.Create())
    {
        return await context.Entities.FindAsync(id);
    }
}

传统上关于异步等待的回答必须包括指向 Stephen Cleary 博客的链接
省略 Async 和 Await


1
当你使用await时,代码将等待异步函数完成。这应该在需要从异步函数获取值的情况下进行,例如这种情况:
int salary = await CalculateSalary();

...

async Task<int> CalculateSalary()
{
    //Start high cpu usage task
    ...
    //End high cpu usage task
    return salary;
}

如果您没有使用await,将会发生以下情况:
int salary = CalculateSalary().Result;

...

async Task<int> CalculateSalary()
{
    //Start high cpu usage task
    ... //In some line of code the function finishes returning null because we didn't wait the function to finish
    return salary; //This never runs
}

等待意味着等待这个异步函数完成。

根据您的需求使用它,您的情况1和2将产生相同的结果,只要在分配info值时等待,代码就会安全。

来源:https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/index


1
等待是一种序列化功能,它允许调用者接收异步方法的结果并对其进行处理。如果您不需要处理异步函数的结果,则不必等待它。
在您的示例中,Func1()Func2()不处理调用的异步函数的返回值,因此不等待它们也可以。

0

我相信第二个会生效,因为await期望返回一个值。 由于它正在等待Func1()返回一个值,Func1()已经在执行Func2()并返回一个值。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接