await和ContinueWith的区别

158

请问在下面的例子中,awaitContinueWith是否可以互换使用?这是我第一次尝试使用TPL,已经阅读了所有的文档,但不理解它们之间的区别。

Await:

String webText = await getWebPage(uri);
await parseData(webText);

ContinueWith

Task<String> webText = new Task<String>(() => getWebPage(uri));
Task continue = webText.ContinueWith((task) =>  parseData(task.Result));
webText.Start();
continue.Wait();

在特定情况下,是否有一种更受青睐?


5
如果在第二个示例中删除了“Wait”调用,那么这两个代码片段(大部分)将是等价的。 - Servy
4
可能是Is Async await keyword equivalent to a ContinueWith lambda?的重复问题。 - Stephen Cleary
你的 getWebPage 方法不能同时在两个代码中使用。在第一个代码中,它具有 Task<string> 返回类型,而在第二个代码中,它具有 string 返回类型。因此,基本上你的代码无法编译。- 如果要精确的话。 - Royi Namir
在特定情况下,一个是否比另一个更受青睐?ContinueWith在async\await被添加之前就已经存在了。在async\await出现后,ContinueWith可能应该被标记为过时的,但由于某种原因它并没有被标记。 - osexpert
2个回答

141

以下是我最近用来说明使用异步解决问题的不同之处和各种问题的代码片段序列。

假设您的基于GUI的应用程序中有一些事件处理程序需要很长时间,因此您希望使其异步化。以下是您开始使用的同步逻辑:

while (true) {
    string result = LoadNextItem().Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
        break;
    }
}

LoadNextItem返回一个Task,最终会产生一些你想要检查的结果。如果当前结果是你要找的那个,你就在UI上更新一些计数器的值,并从方法中返回。否则,你将继续处理来自LoadNextItem的更多项。

异步版本的第一个想法:只需使用continuations!暂时先忽略循环部分。我的意思是,可能会出什么问题呢?

return LoadNextItem().ContinueWith(t => {
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
});

太好了,现在我们有了一个不会阻塞的方法!但它可能会崩溃。任何对UI控件的更新都应该在UI线程上进行,所以您需要考虑到这一点。幸运的是,有一个选项可以指定如何安排续集,并且有一个默认选项:

return LoadNextItem().ContinueWith(t => {
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
},
TaskScheduler.FromCurrentSynchronizationContext());

太好了,现在我们有了一个不会崩溃的方法!它会默默地失败。Continuations是单独的任务,其状态与前置任务无关。因此,即使LoadNextItem出现故障,调用者也只会看到一个已成功完成的任务。好的,那么如果有异常,就将其传递:

return LoadNextItem().ContinueWith(t => {
    if (t.Exception != null) {
        throw t.Exception.InnerException;
    }
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
},
TaskScheduler.FromCurrentSynchronizationContext());

太好了,现在这个确实有效。对于单个项目而言是如此。现在,如何循环呢?事实证明,与原始同步版本逻辑等效的解决方案将类似于以下内容:

Task AsyncLoop() {
    return AsyncLoopTask().ContinueWith(t =>
        Counter.Value = t.Result,
        TaskScheduler.FromCurrentSynchronizationContext());
}
Task<int> AsyncLoopTask() {
    var tcs = new TaskCompletionSource<int>();
    DoIteration(tcs);
    return tcs.Task;
}
void DoIteration(TaskCompletionSource<int> tcs) {
    LoadNextItem().ContinueWith(t => {
        if (t.Exception != null) {
            tcs.TrySetException(t.Exception.InnerException);
        } else if (t.Result.Contains("target")) {
            tcs.TrySetResult(t.Result.Length);
        } else {
            DoIteration(tcs);
        }});
}

或者,您可以使用 async 来完成同样的操作:

async Task AsyncLoop() {
    while (true) {
        string result = await LoadNextItem();
        if (result.Contains("target")) {
            Counter.Value = result.Length;
            break;
        }
    }
}

那现在好多了,是吧?

2
谢谢,解释得非常好。 - Elger Mensonides
1
这是一个很好的例子。 - Royi Namir

126
在第二段代码中,你是在同步等待 continuation 完成。而在第一种情况下,只要遇到第一个还未完成的 await 表达式,该方法将立即返回给调用者。
它们非常相似,因为它们都安排了 continuation,但是一旦控制流稍微复杂一些,await 就会导致简单得多的代码。此外,如评论中 Servy 所指出的那样,等待一个任务将“展开”聚合异常,这通常会导致更简单的错误处理。此外,使用 await 将隐式地在调用上下文中安排 continuation(除非使用 ConfigureAwait)。虽然这不是无法“手动”完成的事情,但使用 await 更容易实现。
我建议你尝试使用 await 和 Task.ContinueWith 实现一系列稍微大一点的操作 - 这可能会令人大开眼界。

3
两种代码片段之间的错误处理也不同。在这方面,使用await通常比ContinueWith更容易处理。 - Servy
1
调度也非常不同,即parseData执行的上下文。 - Stephen Cleary
1
当你说“使用await将隐式调度在调用上下文中继续执行”时,你能解释一下这样做的好处以及在另一种情况下会发生什么吗? - Harrison
5
想象一下你正在编写一个WinForms应用程序 - 如果你编写一个异步方法,默认情况下该方法内的所有代码都将在UI线程中运行,因为延续将在那里调度。如果您不指定延续应在哪里运行,我不知道默认值是什么,但很可能最终会在线程池线程上运行......此时,您无法访问UI等内容。 - Jon Skeet
1
@DavidKlempfner:是的。在这种特定情况下,使用Result是可以的,因为它仅在已完成(可能是由于故障)的任务上调用。 - Jon Skeet
显示剩余7条评论

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