我没有看到C#(和VB)的新异步功能与.NET 4.0的Task Parallel Library之间存在差异。举个例子,Eric Lippert的代码from here:
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
}
}
我已将第一个await
替换为实际需要值的Task.Result
,而第二个await
替换为Task.Wait()
,其中等待实际发生。功能(1)
已经实现,(2)
在语义上更接近代码中实际发生的内容。
我确实意识到async
方法被重写为状态机,类似于迭代器,但我也看不出它带来了什么好处。任何需要另一个线程运行的代码(例如下载)仍然需要另一个线程,任何不需要的代码(例如从文件中读取)仍然可以利用TPL只使用一个线程。
我显然在这里遗漏了一些巨大的东西;任何人都可以帮助我更好地理解这一点吗?
答案 0 :(得分:71)
我认为这里出现了误解:
似乎await关键字有两个不同的用途。第一次出现(FetchAsync)似乎意味着,“如果稍后在方法中使用此值并且其任务未完成,请等到它完成后再继续。”第二个实例(存档)似乎意味着,“如果此任务尚未完成,请立即等待直到完成。”如果我错了,请纠正我。
这实际上是完全错误的。这两者具有相同的含义。
在你的第一个案例中:
var document = await FetchAsync(urls[i]);
这里发生的是,运行时说“开始调用FetchAsync,然后将当前执行点返回给调用此方法的线程。”这里没有“等待” - 相反,执行返回到调用同步上下文,并且事情继续搅动。在将来的某个时刻,FetchAsync的任务将完成,此时,此代码将在调用线程的同步上下文中恢复,并且将发生下一个语句(分配文档变量)。
执行将继续,直到第二次等待调用 - 此时,同样的事情将发生 - 如果Task<T>
(存档)未完成,执行将被释放到调用上下文 - 否则,档案将被设定。
在第二种情况下,事情是非常不同的 - 在这里,你明确地阻塞,这意味着在整个方法完成之前,调用同步上下文永远不会有机会执行任何代码。当然,仍然存在异步,但是异步完全包含在这个代码块中 - 在所有代码完成之前,在此线程上不会发生此粘贴代码之外的代码。
答案 1 :(得分:25)
存在巨大差异:
Wait()
块,await
不会阻止。如果在GUI线程上运行ArchiveDocuments()
的异步版本,则GUI将在提取和归档操作运行时保持响应。
如果您使用带有Wait()
的TPL版本,您的GUI将被阻止。
请注意async
设法在不引入任何线程的情况下执行此操作 - 在await
处,控件只返回到消息循环。一旦等待的任务完成,方法的其余部分(继续)将在消息循环中排队,并且GUI线程将继续运行ArchiveDocuments
,直到它停止。
答案 2 :(得分:24)
Anders在接下来的Channel 9 Live采访中将其归结为一个非常简洁的答案。我强烈推荐它
新的Async和await关键字允许您在应用程序中协调并发。它们实际上并没有在您的应用程序中引入任何并发性。
TPL,更具体地说,任务是 单向 ,您可以使用它来实际同时执行操作。新的async和await关键字允许您以“同步”或“线性”方式组合这些并发操作。
因此,您仍然可以在程序中编写线性控制流,而实际计算可能会同时发生也可能不会同时发生。当计算同时发生时,await和async允许你撰写这些操作。
答案 3 :(得分:6)
将程序控制流程转换为状态机的能力使这些新关键词变得有意义。将其视为让步控制,而不是值。
查看Anders的this Channel 9 video,了解新功能。
答案 4 :(得分:4)
这里的问题是ArchiveDocuments
的签名具有误导性。它有明确的void
返回值,但实际上返回的是Task
。对我来说,虚空意味着同步,因为没有办法“等待”它完成。考虑函数的替代签名。
async Task ArchiveDocuments(List<Url> urls) {
...
}
当我用这种方式书写时,差异就更明显了。 ArchiveDocuments
函数不是同步完成但后续完成的函数。
答案 5 :(得分:0)
对FetchAsync()
的调用仍会阻塞直到它完成(除非调用await
中的语句?)关键是控件返回给调用者(因为ArchiveDocuments
方法本身声明为async
)。因此,调用者可以愉快地继续处理UI逻辑,响应事件等。
当FetchAsync()
完成时,它会中断调用者以完成循环。它命中ArchiveAsync()
并阻塞,但ArchiveAsync()
可能只是创建一个新任务,启动它并返回任务。这允许第二个循环开始,而任务正在处理。
第二个循环命中FetchAsync()
并阻塞,将控制权返回给调用者。当FetchAsync()
完成时,它会再次中断调用者以继续处理。然后命中await archive
,它将控制返回给调用者,直到在循环1中创建的Task
完成。一旦该任务完成,调用者将再次被中断,第二个循环调用ArchiveAsync()
,它将获得一个已启动的任务并开始循环3,重复 ad nauseum 。
当重型举重器正在执行时,关键是将控制权返回给调用者。
答案 6 :(得分:0)
await关键字不引入并发。它就像yield关键字一样,它告诉编译器将代码重构为由状态机控制的lambda。
要查看在没有“等待”的情况下等待代码会是什么样子,请看这个优秀的链接:http://blogs.msdn.com/b/windowsappdev/archive/2012/04/24/diving-deep-with-winrt-and-await.aspx