异步方法的两种调用方式。C#

3
这两种方法有什么区别吗?或者说这两种情况下程序是否运行方式相似?如果有差异,您能说出这些差异是什么吗?
第一种方法:
Task myTask = MyFunctionAsync();
await myTask;

第二种方法:
await MyFunctionAsync();

1
在异常情况下,使用第一种方法可能会获得更有用的堆栈跟踪。 - Theodor Zoulias
4个回答

4
简短版:在有趣的方式下,不是真正的限制。
长版:可等待对象并不仅限于Task / Task ,因此可以(事实上是微不足道地)创建编译良好的代码。
await MyFunctionAsync();

但是不能通过编译:
Task myTask = MyFunctionAsync();
await myTask;

之所以如此,是因为MyFunctionAsync()返回的不是任务。使用ValueTask<int>就足够了,但如果你想要,可以创建其他异步等待对象。但是:如果我们用var替换Task,也就是说,

var myTask = MyFunctionAsync();
await myTask;

现在和以前唯一的区别就是,如果我们需要的话,我们可以在代码中的其他点引用myTask。这并不是特别罕见的情况; 两种主要情况是:

  • 结合并发代码上的多个检查,可能使用WhenAnyWhenAll
  • (通常针对ValueTask[<T>])检查等待期间是否同步完成,以避免在同步情况下状态机开销

3

它们实际上是相同的。不同之处在于第一种方式让您在等待响应之前可以执行更多步骤。因此,您可以在第一种方法中同时启动许多任务,然后使用 await Task.WhenAll(myListOfTasks) 一起等待它们。

例如:

var myTasks = myEmployees.Select(e => ProcessPayrollAsync(e));
await Task.WhenAll(myTasks);

如果需要并发,我建议使用第一种方法;如果是简单情况,使用第二种方法更为简短。


3
在这种特殊情况下,两种形式的代码都以相似的方式执行。然而,请考虑以下情况:
public async Task<int> CalculateResult(InputData data) {
    // This queues up the work on the threadpool.
    var expensiveResultTask = Task.Run(() => DoExpensiveCalculation(data));

    // Note that at this point, you can do some other work concurrently,
    // as CalculateResult() is still executing!

    // Execution of CalculateResult is yielded here!
    var result = await expensiveResultTask;

    return result;
}

如上面代码中的注释所指出,在任务运行和 await 调用之间,您可以执行任何其他并发代码。

欲了解更多信息,请阅读文章。


2
我理解async-await的最佳方式是通过Eric Lippert在这个采访中描述的烹饪类比。在采访的中间部分搜索async-await。
在这里,他描述了一个厨师制作早餐的过程。一旦他放上水壶煮茶水,他不会闲待等待水开。相反,他会把面包放在烤面包机里,茶叶放在茶壶里,开始切番茄:每当他必须等待其他设备或其他厨师完成工作时,他都不会闲着等待,而是开始下一个任务,直到他需要前面任务的结果。
异步等待也是如此:每当另一个进程必须执行某些操作时,在您的线程无法做任何事情但必须等待另一个进程完成时,线程可以四处查看以查看是否可以做其他事情。通常在涉及到另一个耗时进程时,您会看到异步等待:将数据写入文件,从数据库或互联网查询数据。每当您的线程必须执行此操作时,线程可以命令其他进程执行某些操作,同时继续执行其他操作:
Task<string> taskReadFile = ReadMyFileAsync(...);

// not awaiting, we can do other things:
DoSomethingElse();

// now we need the result of the file:
string fetchedData = await taskReadFile;

那么会发生什么呢?ReadMyFileAsync是异步的,因此你知道在它的深处,该方法会等待。实际上,如果你忘记await,编译器会警告你。

一旦线程看到了await,它就知道需要等待await的结果,所以它不能继续执行。相反,它会向上调用堆栈继续处理(在我的示例中是DoSomethingElse()),直到它看到一个await。然后再次向上调用堆栈并继续处理,以此类推。

因此,实际上你的第一个和第二个方法之间没有真正的区别。可以将其与以下内容进行比较:

double x = Math.Sin(4.0)

对比

double a = 4.0;
double x = Math.Sin(a);

官方上唯一的区别在于在这些语句之后你仍然可以使用 a。同样,在 await 后,你可以使用来自任务的信息:

Task<MyData> myTask = FetchMyDataAsync(...);
MyData result = await myTask;

// if desired you can investigate myTask
if (result == null)
{
    // why is it null, did someone cancel my task?
    if (Task.IsCanceled)
    {
        Yell("Hey, who did cancel my task!");
    }
}

但大多数时候,您可能对该任务不感兴趣。如果在任务执行期间没有其他事情可做,我会等待它完成:

MyData fetchedData = await FetchMyDataAsync(...)

关于第一种方法。如果线程在方法中看到await,它会离开该方法并继续向下执行堆栈。我运行了代码并发现即使我不调用这些任务的await,我也可以看到它们的结果。为什么在我没有等待包含它们的方法的情况下,可等待操作在执行并返回结果?(我正在使用UI线程)我认为只有当我等待由该方法返回的任务时,线程才会离开该方法并继续执行。 - Allaev Bekzod
1
即使没有等待任务,它也可以完成。您可以定期检查属性“IsCompleted”,而不是等待,一旦返回true,就可以获取“Task.Result”。但是,这将是一个相当繁忙的等待。异步等待发明的主要原因是为了给调用者一个简单的方法来继续处理,而不是闲等另一个进程完成。 - Harald Coppoolse

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