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

Task.ContinueWith的msdn文档只有一个代码示例,其中一个任务(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} );

我的问题很简单;使用.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);

在建议的修订版中,避免完全调用ContinueWith,第二段代码仍在后台运行,并且没有上下文切换代码可以访问闭包的状态...我不明白吗?感觉有点愚蠢,我做了一些谷歌搜索,也许只是没有按正确的词组进行搜索。

更新:Hans Passant发布了指向更多MSDN注释的链接之后。这很有帮助,激发了一些我可以“谷歌搜索”的新东西。 (谷歌,用动词表示,带有一个小的“ g”,just in case ChrisF want's to edit my post again and capitalise it. ;-D),但是仍然没有明确说明,例如,this SO discussion给出了一个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? 

本着学习和探索更多有关ContinueWith的精神,以上代码可以安全地编写为...吗?

var task = Task.Factory.StartNew(() => { 
  Whatever();
  Callback();
);  

...如果不是,那么也许不是为什么我们可能会导致我们以某种清晰的方式回答问题的原因,即一个示例,该示例表明该替代项必须写为x,这将使可读性差,安全性差,可测试的?比使用.ContinueWith

当然,如果任何人都能想到一个简单的现实生活场景,其中ContinueWith提供真正的好处,那将是头等奖,因为这意味着正确记住它会容易得多。

参考方案

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

自从“主流” 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;
}

(省略错误处理和适当处理:))

一切都很好,但在阻止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;
}

哦,哇现在您可以看到为什么每个人都只是启动一个新线程而不是使用异步代码来破坏代码,是吗?请注意,这完全没有错误处理。您能想象适当的可靠代码看起来如何吗?它并不漂亮,大多数人从来没有打扰过。

但是Task随.NET 4.0一起提供。基本上,这为处理异步操作提供了一种全新的方式,该方式受到了函数式编程的极大启发(如果您感兴趣的话,Task基本上是一种共鸣)。连同改进的编译器一起,这允许将上面的整个代码重写为如下形式:

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()
  );
}

有趣的是,我们基本上仍然有一些看起来像同步代码的东西-我们只需要在存在异步调用的地方添加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;
}

await“神奇地”为我们处理了所有这些异步回调,为我们提供了与原始同步代码几乎完全相同的代码-而不需要多线程或阻塞UI。最酷的部分是,您可以以与方法同步的相同方式来处理错误-tryfinallycatch ...它们的工作原理与一切都是同步的一样。它并不能使您摆脱异步代码的所有棘手问题(例如,您的UI代码变得可重入,类似于您使用Application.DoEvents的情况),但总体而言,它做得很好:)

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

LeetCode题解水壶问题

给你一个装满水的 8 升满壶和两个分别是 5 升、3 升的空壶,请想个优雅的办法,使得其中一个水壶恰好装 4 升水,每一步的操作只能是倒空或倒满。题解:``` .js/** * 思路: * 每个容器有两个选择,比如:A,可以倒入B,或者倒入C * 同样,B可以倒入A,也可以倒入C * 那么每次就有8种可能 * * 每产生一种可能,顺着这种可能的结果,继续去遍…

使用php echo定义一个javascript var - javascript

我已经看到了对该问题的多个答复-因此,我敢肯定有人会很快将其标记为重复,但是我在任何其他线程中都没有看到该特定问题的答案。我有两个测试文件:a.php和a.js在a.php中,我定义$q = $_GET['q']; 效果很好。然后,我调用一个单独的a.js文件,其中有:var partNumber = " <?php ec…

从具有返回类型Task的方法返回值 - c#

我可能在这里错过了一些东西。语法不正确。static Task<int> MathOperation(int number) { //return new Task(new Func(TestMethod(number))); } static int LongRunningMethod(int number) { // some long ru…

在Spring中实例化一个新线程以定期执行任务 - java

我有一个用Spring制作的仪表板,它必须控制某些任务的执行。基本思想是有一个线程将该任务定期发送到远程跟踪器。如何实例化该线程?我已经阅读了一些,有人说使用线程不是一个好主意。这会导致Spring生命周期出现问题吗?还有另一种方法可以使方法定期调用吗? 参考方案 Spring支持任务计划。在此处查找更多信息:http://static.springsour…

如何使用asyncio安排和取消任务 - python

我正在编写一个客户端服务器应用程序。连接后,客户端会每秒向服务器发送一个“心跳”信号。在服务器端,我需要一种机制,可以在其中添加要异步执行的任务(或协程或其他东西)。而且,当客户端停止发送“心跳”信号时,我想从客户端取消任务。换句话说,当服务器启动任务时,它具有某种超时或ttl,例如3秒。当服务器收到“心跳”信号时,它将计时器重置三秒钟,直到任务完成或客户端…