在异步方法中,返回Task和等待Task之间的区别是什么?

10
下面的方法有什么区别吗?是否有一个更好一些?
public static async Task SendAsync1(string to, string subject, string htmlBody) {
  // ...
  await smtp.SendMailAsync(message);
  // No return statement
}

public static Task SendAsync2(string to, string subject, string htmlBody) {
  // ...
  return smtp.SendMailAsync(message);
}

这个方法将从MVC控制器方法中调用,例如:

public async Task<ActionResult> RegisterUser(RegisterViewModel model)
{
  // ...
  await Mailer.SendAsync(user.Email, subject, body);
  return View(model);
}

也许如果使用SendAsync1,维护状态(通过async/await状态机制)会在两个地方产生成本,但是也许async/await状态机器足够聪明,可以将其优化掉...只是一个想法,不过很有趣。 - brumScouse
3个回答

12
有两个实际的不同之处:
  1. 第二种选项不会创建状态机制,也就是不支持async-await用法。这对性能有轻微积极影响。
  2. 异常处理会有一些不同。当你将一个方法标记为async时,任何异常都会存储在返回的任务中(包括异步部分和同步部分),只有在等待任务时才会抛出。而如果不使用async,同步部分的异常会像其他方法一样被处理。
我的建议:使用第二种选项以获得更好的性能,但要注意异常和错误。
下面是一个展示区别的例子:
public static async Task Test()
{
    Task pending = Task.FromResult(true);
    try
    {
        pending = SendAsync1();
    }
    catch (Exception)
    {
        Console.WriteLine("1-sync");
    }

    try
    {
        await pending;
    }
    catch (Exception)
    {
        Console.WriteLine("1-async");
    }

    pending = Task.FromResult(true);
    try
    {
        pending = SendAsync2();
    }
    catch (Exception)
    {
        Console.WriteLine("2-sync");
    }

    try
    {
        await pending;
    }
    catch (Exception)
    {
        Console.WriteLine("2-async");
    }
}

public static async Task SendAsync1()
{
    throw new Exception("Sync Exception");
    await Task.Delay(10);
}

public static Task SendAsync2()
{
    throw new Exception("Sync Exception");
    return Task.Delay(10);
}

输出:

1-async
2-sync

“不会创建状态机制” - 这是什么意思?你怎么知道并且在哪里可以看到它?这就像无形的人在无形的湖中,我根本无法理解它 :( 我是说从物理上来讲 - 到底发生了什么? - monstro
@monstro http://sharplab.io/#v2:CYLg1APgAgDABFAjAbgLAChYMQVjejKAZgQCY4BhDAbwznoRKgA4EA2OAZQFMA7YAIIBnAJ68AxogAUASjoNa6BsoQBOLn2ABZAIYBLADbCx42fmUBfefWuN2G/sYmlZtxSoZQA7A+36johJmtlZKDLbE9jz8uoZOpnJh9O4eCD5QbAB0FAD2ALYADgbcAC7cwBnmDKEWQA= - i3arnon

6

首先,我认为你示例中的代码无法编译。你需要从SendAsync2中删除“async”关键字。

如果这样做了,那么这些方法可以互换使用,所以在这种情况下没有区别。我更喜欢不带async/await的方法。

然而,在一些情况下看起来似乎没有区别,但细节却有所不同。例如,请考虑以下代码:

async Task<X> Get() 
{
     using (something) 
     {
          return await GetX();
     }
}

如果你要更改它为:
Task<X> Get() 
{
    using (something)
    {
        return GetX(); 
    }
}

那么,using块不再保护封装在x中的执行内容,而something将比第一种情况下更早地被处理掉。例如,当something是Entity Framework上下文时非常重要。

同样道理适用于try块中的return await


关于使用/try-catch可能存在的陷阱,你的观察很棒! - Eamon Nerbonne

5
你的第一个方法使用了await关键字,这将导致编译器创建一个状态机,因为一旦 await 关键字被触发,该方法将返回给调用者,一旦等待完成,它就必须从离开的地方恢复执行。
但是,由于你在等待之后没有进行任何操作(没有后续操作),因此不需要那个状态机。
在这种情况下,第二种方法更为适合,并且可以从中省略async关键字,因为你不需要等待任何东西。

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