63

我看不出 C#(和 VB)的新异步功能和 .NET 4.0 的Task Parallel Library之间有什么不同。以 Eric Lippert的代码为例

async void ArchiveDocuments(List<Url> urls) {
    Task archive = null;
    for(int i = 0; i < urls.Count; ++i) {
        var document = await FetchAsync(urls[i]);
        if (archive != null)
            await archive;
        archive = ArchiveAsync(document);
    }
}

await关键字似乎有两个不同的目的。第一次出现 ( FetchAsync) 似乎意味着“如果稍后在方法中使用此值并且其任务未完成,请等到它完成后再继续。” 第二个实例 ( archive) 似乎意味着“如果此任务尚未完成,请立即等待它完成。” 如果我错了,请纠正我。

难道就不能这么简单地写成这样吗?

void ArchiveDocuments(List<Url> urls) {
    for(int i = 0; i < urls.Count; ++i) {
        var document = FetchAsync(urls[i]);       // removed await
        if (archive != null)
            archive.Wait();                       // changed to .Wait()
        archive = ArchiveAsync(document.Result);  // added .Result
    }
}

我已将第一个替换awaitTask.Result实际需要该值的位置,将第二个await替换Task.Wait()为实际发生等待的位置。该功能(1)已经实现,并且(2)在语义上更接近代码中实际发生的情况。

我确实意识到一个async方法被重写为状态机,类似于迭代器,但我也看不到它带来了什么好处。任何需要另一个线程来操作的代码(例如下载)仍然需要另一个线程,并且任何不需要的代码(例如从文件中读取)仍然可以利用 TPL 仅与单个线程一起工作。

我显然在这里遗漏了一些巨大的东西。任何人都可以帮助我更好地理解这一点吗?

4

7 回答 7

71

我认为误解出现在这里:

await 关键字似乎有两个不同的目的。第一次出现 (FetchAsync) 似乎意味着“如果稍后在方法中使用此值并且其任务未完成,请等到它完成后再继续。” 第二个实例(存档)似乎意味着“如果此任务尚未完成,请立即等待它完成。” 如果我错了,请纠正我。

这实际上是完全不正确的。这两者具有相同的含义。

在您的第一种情况下:

var document = await FetchAsync(urls[i]);

这里发生的是运行时说“开始调用 FetchAsync,然后将当前执行点返回给调用此方法的线程”。这里没有“等待”——相反,执行返回到调用同步上下文,事情一直在搅动。在未来的某个时刻,FetchAsync 的 Task 将完成,此时,此代码将在调用线程的同步上下文中恢复,并且将发生下一条语句(分配文档变量)。

然后将继续执行,直到第二次 await 调用——此时,同样的事情会发生——如果Task<T> (归档)没有完成,执行将被释放到调用上下文——否则,归档将被设置。

在第二种情况下,情况就大不相同了——在这里,您是显式阻塞的,这意味着调用同步上下文将永远不会有机会执行任何代码,直到您的整个方法完成。当然,仍然存在异步,但异步完全包含在此代码块中 - 在您的所有代码完成之前,此粘贴代码之外的任何代码都不会在此线程上发生。

于 2010-10-29T17:26:59.210 回答
26

安德斯在他所做的第 9 频道现场采访中将其归结为一个非常简洁的答案。我强烈推荐它

新的 Async 和 await 关键字允许您在应用程序中编排并发。它们实际上并没有在您的应用程序中引入任何并发性。

TPL,更具体地说,Task 是您可以用来实际同时执行操作的一种方式。新的 async 和 await 关键字允许您以“同步”或“线性”方式组合这些并发操作。

因此,您仍然可以在程序中编写线性控制流,而实际计算可能会或可能不会同时发生。当计算确实同时发生时,等待和异步允许您组合这些操作。

于 2010-10-29T17:55:35.977 回答
25

这是个很大的差异:

Wait()阻挡,await不阻挡。如果您ArchiveDocuments() 在 GUI 线程上运行异步版本,则 GUI 将在获取和归档操作运行时保持响应。如果您将 TPL 版本与 一起使用Wait(),您的 GUI 将被阻止。

请注意,async在不引入任何线程的情况下设法做到这一点 - 在await, 控制只是简单地返回到消息循环。一旦等待的任务完成,该方法的其余部分(继续)将在消息循环中排队,并且 GUI 线程将继续ArchiveDocuments从它停止的地方运行。

于 2010-10-29T17:20:52.913 回答
6

将程序控制流转变为状态机的能力使这些新关键字很有趣。将其视为屈服控制,而不是价值观。

观看Anders谈论新功能的第 9 频道视频。

于 2010-10-29T17:27:15.323 回答
4

这里的问题是签名ArchiveDocuments具有误导性。它有一个明确的回报,void但实际上回报是Task。对我来说 void 意味着同步,因为没有办法“等待”它完成。考虑函数的替代签名。

async Task ArchiveDocuments(List<Url> urls) { 
  ...
}

对我来说,当它以这种方式编写时,差异更加明显。该ArchiveDocuments功能不是同步完成的,而是稍后完成的。

于 2010-10-29T17:25:39.040 回答
1

await 关键字不引入并发性。它就像 yield 关键字,它告诉编译器将您的代码重组为由状态机控制的 lambda。

要查看没有“等待”的等待代码会是什么样子,请参阅这个极好的链接:http: //blogs.msdn.com/b/windowsappdev/archive/2012/04/24/diving-deep-with-winrt-and-await .aspx

于 2015-01-07T03:35:21.933 回答
0

调用FetchAsync()仍然会阻塞,直到它完成(除非调用中的语句await?)关键是控制权返回给调用者(因为ArchiveDocuments方法本身被声明为async)。因此调用者可以愉快地继续处理 UI 逻辑、响应事件等。

完成FetchAsync()后,它会中断调用者以完成循环。它会命中ArchiveAsync()并阻止,但ArchiveAsync()可能只是创建一个新任务,启动它,然后返回任务。这允许第二个循环开始,而任务正在处理。

第二个循环命中FetchAsync()并阻塞,将控制权返回给调用者。完成FetchAsync()后,它再次中断调用者继续处理。然后它点击await archive,它将控制权返回给调用者,直到Task在循环 1 中创建完成。一旦该任务完成,调用者再次被中断,第二个循环调用ArchiveAsync(),它获取一个已启动的任务并开始循环 3,重复ad nauseum

关键是在重型起重器执行时将控制权返回给调用者。

于 2010-10-29T17:16:06.483 回答