C#5.0的异步等待功能与TPL有何不同?

时间:2010-10-29 17:12:24

标签: c# task-parallel-library c#-5.0 async-await

我没有看到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只使用一个线程。

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

7 个答案:

答案 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