为什么要使用continueWith而不是简单地将延续代码附加到后台任务的末尾?

sno*_*ode 10 .net c# task task-parallel-library

msdn文档Task.ContinueWith只有一个代码示例,其中一个任务(dTask)在后台运行,后跟(使用ContinueWith)第二个任务(dTask2).样品的本质如下所示;

  Task dTask = Task.Factory.StartNew( () => {
                        ... first task code here ...
                        } ); 

  Task dTask2 = dTask.ContinueWith( (continuation) => {
                       ... second task code here ...
                      } );                      
  Task.WaitAll( new Task[] {dTask, dTask2} );
Run Code Online (Sandbox Code Playgroud)

我的问题很简单; 使用.ContinueWith调用第二个代码块的优点是什么,而不是简单地将它附加到第一个代码块,后者已经在后台运行并将代码更改为这样的代码?

  Task dTask = Task.Factory.StartNew( () => {
                        ... first task code here ...
                        if (!cancelled) //and,or other exception checking wrapping etc
                            {
                             ... second task code here ...
                            }
                        } ); 

  Task.Wait(dTask);
Run Code Online (Sandbox Code Playgroud)

在建议的修订版中,ContinueWith完全避免调用,第二个代码块仍然在后台运行,加上没有上下文切换代码来访问闭包的状态......我不明白?感觉有点愚蠢,我做了一些谷歌搜索,也许只是没有找到正确的短语来搜索.

更新: Hans Passant发布了更多MSDN笔记的链接.这很有帮助,引发了一些我可以"谷歌"的新东西.(google,就像动词一样,带有一个小'g',以防ChrisF想要再次编辑我的帖子并将其大写.;-D)但是仍然没有带来任何清晰度,例如,这个SO讨论给出了一个例子的ContinueWith,问一个有趣的问题,"它到底是如何决定的,当回调方法将执行?".我可能错了,但在我看来,对于最常见的用法,只需附加延续代码就可以在代码被"安排"(执行)时100%清楚.在附加代码的情况下,它将在上面的行完成之后"立即"执行,并且在ContinueWith...... 的情况下,"它依赖",即你需要知道Task类库的内部和默认值使用设置和调度程序.所以,这显然是一个巨大的权衡,到目前为止所提供的所有例子都没有解释为什么或你什么时候准备进行这种交易呢?如果它确实是一种权衡,而不是对ContinueWith's预期用途的误解.

以下是我在上面引用的SO问题的摘录:

// Consider this code:
var task = Task.Factory.StartNew(() => Whatever());  
task.ContinueWith(Callback), TaskScheduler.FromCurrentSynchronizationContext())
// How exactly is it determined when the callback method will execute? 
Run Code Online (Sandbox Code Playgroud)

本着学习和探索的精神,ContinueWith上述代码可以安全地写成......?

var task = Task.Factory.StartNew(() => { 
  Whatever();
  Callback();
);  
Run Code Online (Sandbox Code Playgroud)

......如果没有,那么也许之所以可能导致我们没有理由以一些清晰的方式回答这个问题,即一个例子表明备选方案必须写得x不那么可读,安全性更低,更可测试?减 ??而不是使用.ContinueWith.

当然,如果有人能想到一个简单的真实的生活场景,其中ContinueWith提供了实实在在的好处,那么这将是第一名,因为这将意味着它会更容易记错的话.

Lua*_*aan 12

延续的主要原因是组合和异步代码流。

自从“主流”OOP 开始以来,组合已经死了一点,但是随着 C# 采用越来越多的函数式编程实践(和特性),它也开始对组合更加友好。为什么?它使您可以轻松地对代码进行推理,尤其是在涉及异步性时。同样重要的是,它允许您非常轻松地抽象出执行某事的确切方式,这在处理异步代码时再次非常重要。

假设您需要从某个 Web 服务下载一个字符串,并使用它来下载基于该数据的另一个字符串。

在老式的、非异步(和糟糕的)应用程序中,这看起来像这样:

public void btnDo_Click(object sender, EventArgs e)
{
  var request = WebRequest.Create(tbxUrl.Text);
  var newUrl = new StreamReader(request.GetResponse().GetResponseStream()).ReadToEnd();

  request = WebRequest.Create(newUrl);
  var data = new StreamReader(request.GetResponse().GetResponseStream()).ReadToEnd();

  lblData.Text = data;
}
Run Code Online (Sandbox Code Playgroud)

(省略错误处理和正确处置:))

这一切都很好,但它在阻塞 UI 线程方面有一点问题,从而使您的应用程序在两个请求的持续时间内没有响应。现在,对此的典型解决方案是使用类似的BackgroundWorker方法将这项工作委托给后台线程,同时保持 UI 响应。当然,这带来了两个问题——一,你需要确保后台线程永远不会访问任何 UI(在我们的例子中,tbxUrllblData),二,这是一种浪费——我们使用一个线程只是为了阻塞和等待以完成异步操作。

技术上更好的选择是使用异步 API。但是,这些使用起来非常棘手 - 一个简化的示例可能如下所示:

void btnDo_Click(object sender, EventArgs e)
{
  var request = WebRequest.Create(tbxUrl.Text);
  request.BeginGetResponse(FirstCallback, request);

  var newUrl = new StreamReader(request.GetResponse().GetResponseStream()).ReadToEnd();

  request = WebRequest.Create(newUrl);
  var data = new StreamReader(request.GetResponse().GetResponseStream()).ReadToEnd();

  lblData.Text = data;
}

void FirstCallback(IAsyncResult result)
{
  var response = ((WebRequest)result.AsyncState).EndGetResponse(result);

  var newUrl = new StreamReader(response.GetResponseStream()).ReadToEnd();

  var request = WebRequest.Create(newUrl);
  request.BeginGetResponse(SecondCallback, request);
}

void SecondCallback(IAsyncResult result)
{
  var response = ((WebRequest)result.AsyncState).EndGetResponse(result);

  var data = new StreamReader(response.GetResponseStream()).ReadToEnd();

  BeginInvoke((Action<object>)UpdateUI, data);
}

void UpdateUI(object data)
{
  lblData.Text = (string)data;
}
Run Code Online (Sandbox Code Playgroud)

哦,哇。现在你可以明白为什么每个人都启动一个新线程而不是使用异步代码,是吗?请注意,这没有任何错误处理。你能想象一个合适的可靠代码应该是什么样子的吗?它并不漂亮,而且大多数人只是从不打扰。

但随后Task.NET 4.0 出现了。基本上,这启用了处理异步操作的全新方式,深受函数式编程的启发(如果您有兴趣,Task基本上是一个 comonad)。随着编译器的改进,这允许将上面的整个代码重写为如下所示:

void btnDoAsync_Click(object sender, EventArgs e)
{
  var request = WebRequest.Create(tbxUrl.Text);

  request
  .GetResponseAsync()
  .ContinueWith
  (
    t => 
      WebRequest.Create(new StreamReader(t.Result.GetResponseStream()).ReadToEnd())
      .GetResponseAsync(),
    TaskScheduler.Default
  )
  .Unwrap()
  .ContinueWith
  (
    t =>
    {
      lblData.Text = new StreamReader(t.Result.GetResponseStream()).ReadToEnd();
    },
    TaskScheduler.FromCurrentSynchronizationContext()
  );
}
Run Code Online (Sandbox Code Playgroud)

关于这个很酷的事情是我们基本上仍然有一些看起来像同步代码的东西——我们只需要ContinueWith(...).Unwrap()在有异步调用的地方添加。添加错误处理主要是添加另一个ContinueWith带有TaskContinuationOptions.OnlyOnFaulted. 当然,我们链接的任务基本上是“作为价值的行为”。这意味着很容易创建辅助方法来为您完成部分繁重的工作——例如,一个辅助异步方法将整个响应作为字符串异步读取。

最后,现代 C# 中延续的用例并不多,因为 C# 5 添加了await关键字,这使您可以假装异步代码与同步代码一样简单。将await基于代码的代码与我们原始的同步示例进行比较:

async void btnDo_Click(object sender, EventArgs e)
{
  var request = WebRequest.Create(tbxUrl.Text);
  var newUrl = new StreamReader((await request.GetResponseAsync()).GetResponseStream())
               .ReadToEnd();

  request = WebRequest.Create(newUrl);
  var data = new StreamReader((await request.GetResponse()).GetResponseStream())
             .ReadToEnd();

  lblData.Text = data;
}
Run Code Online (Sandbox Code Playgroud)

await“神奇地”处理所有这些异步回调我们,留给我们的代码,几乎是完全一样的原始同步码-但无需多线程或阻塞UI。最酷的部分是,你可以以同样的方式处理错误,如果方法同步的- ,,try ...他们都工作,仿佛一切是同步的相同。它不会使您免受异步代码的所有棘手问题(例如,您的 UI 代码变得可重入,类似于您使用),但总体而言它做得非常好:)finallycatchApplication.DoEvents

很明显,如果您使用 C# 5+ 编写代码,您几乎总是使用await而不是ContinueWith. 还有地方ContinueWith吗?事实是,并不是很多。我仍然在一些简单的辅助函数中使用它,它对于日志记录非常有用(同样,由于任务很容易组合,向异步函数添加日志记录只是使用简单的辅助函数的问题)。