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

时间:2013-12-21 17:18:22

标签: c# .net task-parallel-library task

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笔记的链接。这很有帮助,引发了一些我可以“谷歌”的新东西。 (google,如动词,带有小'g',just in case ChrisF want's to edit my post again and capitalise it. ;-D但仍然没有带来任何清晰度,例如,this SO discussion给出了{{{3}}的示例1}}并提出一个有趣的问题,“回调方法执行时它究竟是如何确定的?”。我可能错了,但在我看来,对于最常见的用法,只需附加延续代码就可以在代码被“安排”(执行)时100%清楚。在附加代码的情况下,它将在上面的行完成后“立即”执行,并且在ContinueWith的情况下,很好......“它取决于”,即你需要知道任务的内部类库以及使用的默认设置和调度程序。所以,这显然是一个巨大的权衡,到目前为止提供的所有例子都没有解释为什么或你什么时候准备进行这种交易呢?如果它确实是一种权衡,而不是对ContinueWith预期用途的误解。

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

ContinueWith's

本着学习和探索更多关于// 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提供真正的好处,那么这将是一等奖,因为这意味着它会更容易记住它正确。

6 个答案:

答案 0 :(得分:2)

好的,所以经过一些阅读和实验,看起来简短的答案(至少对我来说)是:如果你无法访问.NET 4.5& async和await,你绝对需要保存每一个线程,然后使用ContinueWith可以保存一个线程(或者保存你阻塞线程池线程),并且可能(如下面我的spike测试代码所示)代价相当多的可测试性和可读性。在大多数情况下,pre .net 4.5将代码附加到后台任务的末尾是最简单的,但如前所述,它将阻塞线程池线程,如果你有权访问.net 4.5并使用await和async模式,它将不会而且几乎总是更简单,更清洁。深度(或甚至不是非常深)使用嵌套的Continuations(回调)会非常快速地导致难以读取,测试和维护的代码。

我创建了一个(尽可能简单)示例winform应用程序(下面的代码),它有三个按钮,每个按钮以三种不同的方式执行相同的任务,以便尝试提出一个示例(广泛地)三种不同的方法,你可以自己决定。

示例代码的“肉”位于三个按钮后面的代码中;

  • _button使用延续。 (结果非常嵌套,即使对于这个简单的场景。)
  • _button2使用专门的后台任务并“调用”UI更新代码。
  • _button3使用新的异步和等待语法。

我在LinqPad中对代码进行了原型设计,因此您应该可以直接剪切并粘贴到Linqpad中,然后它就会运行。如果要编译/运行visual studio中的代码,则需要将Console.WriteLine's替换为Log4netnlog或类似,以查看线程ID输出。

我在代码中的关键点上添加了DumpThread(..)'s,以便您可以在代码中的各个点查看当前线程的ID,以查看线程何时(或是否)更改。单击button3例如会生成如下所示的输出;

点击三个按钮中的每一个都会产生以下输出

Main UI thread id : (19)
Form initialisation thread id : (19)

Nested Tasks + Delay && ContinueWith
---------------
Code behind outter thread id : (19)
continuation 1 thread id : (19)
continuation 2 thread id : (19)
continuation 3 thread id : (19)
continuation 4 thread id : (19)

bground monitor Task + final ContinueWith
---------------
code behind thread thread id : (19)
monitoring thread thread id : (4)
Invoker 1 thread id : (19)
Invoker 2 thread id : (19)
Invoker 3 thread id : (19)
Invoker 4 thread id : (19)

async && await (.net 4.5)
---------------
codebehind thread id : (19)
async step 1 thread id : (19)
async step 2 thread id : (19)
async step 3 thread id : (19)
async step 4 thread id : (19)

以下是包含3个按钮的最小winforms应用程序的代码;

    void Main()
{
        DumpThread("Main UI");
        Application.Run(new Form1());
    }

    public static void DumpThread(string msg) {
        Console.WriteLine(string.Format(msg + " thread id : ({0})",Thread.CurrentThread.ManagedThreadId));
    }

    public class Form1 : Form
    {
        private Label[] _lights = Enumerable.Range(1,4).Select (i => new Label() { Left = 100 + ((i-1) *25), Top = 90, Text=i.ToString(), BackColor = Color.Green, Width=20, Visible = true }).ToArray();
        private Label _threadLabel = new Label() { Left = 20, Top = 90, Text="Steps", Width = 50 };
        private TextBox _textbox = new TextBox() { Left = 100,  Top = 120, Text = "blah blah" };    
        private Label _label = new Label() { Left = 20, Top = 120, Text="Label" };
        private Button _button = new Button() { Left = 20, Top = 150, Width = 250,  Text = "Nested Tasks + Delay && ContinueWith" };
        private Button _button2 = new Button() { Left = 20, Top = 180, Width = 250,  Text = "bground monitor Task + final ContinueWith" };
        private Button _button3 = new Button() { Left = 20, Top = 210, Width = 250,  Text = "async && await (.net 4.5)" };      

        public Form1()
        {
            InitialiseControls();
            DumpThread("Form initialisation");

            // Task & ContinueWith
            // ********************
            // very nested continuations are hard to test or debug in more complex scenarios, 
            // considering the code below is a super simple contrived example and it's already not easy to read!
            _button.Click+= delegate 
            { 
                Heading(_button.Text);          
                DumpThread("Code behind outter");
                    Buttons(Enabled:false);
                    // simulate long running task followed by UI update
                    Task.Delay(500).ContinueWith(ant0 => 
                    { 
                        DumpThread("continuation 1");
                        _lights[0].Visible = true; 
                        Task.Delay(500).ContinueWith( ant1 => 
                        {
                            DumpThread("continuation 2");
                            _lights[1].Visible = true; 
                            Task.Delay(500).ContinueWith( ant2 => 
                            {
                                DumpThread("continuation 3");
                                _lights[2].Visible = true; 
                                Task.Delay(500).ContinueWith( ant3 => 
                                {
                                    DumpThread("continuation 4");                               
                                    _lights[3].Visible = true; 
                                        Task.Delay(500).ContinueWith( ant4 => 
                                        {
                                            foreach(var light in _lights) light.Visible = false;
                                            Buttons(Enabled:true);
                                        },TaskScheduler.FromCurrentSynchronizationContext());
                                },TaskScheduler.FromCurrentSynchronizationContext());
                            },TaskScheduler.FromCurrentSynchronizationContext());
                        },TaskScheduler.FromCurrentSynchronizationContext());
                    },TaskScheduler.FromCurrentSynchronizationContext());       
            };

            // ignoring the fact that we could simply foreach(var light in _lights) 
            // done this way in order to help us compare "appending" different blocks of code vs ContinueWith


            // bground Task && no continue with's
            // **********************************
            // traditional means of keeping UI responsive, create a background thread, ensure updates run on control creator's thread
            _button2.Click+= delegate 
            {
                Heading(_button2.Text);
                DumpThread("code behind thread");
                Buttons(Enabled:false);
                Task.Run(()=> {
                    DumpThread("monitoring thread");
                    Thread.Sleep(500); 
                    InvokeOnUIThread(_lights[0],l=> { l.Visible = true; DumpThread("Invoker 1");} );
                    Thread.Sleep(500); 
                    InvokeOnUIThread(_lights[1],l=> { l.Visible = true; DumpThread("Invoker 2");} );
                    Thread.Sleep(500); 
                    InvokeOnUIThread(_lights[2],l=> { l.Visible = true; DumpThread("Invoker 3");} );
                    Thread.Sleep(500); 
                    InvokeOnUIThread(_lights[3],l=> { l.Visible = true; DumpThread("Invoker 4");} );
                    Thread.Sleep(500); 
                    InvokeOnUIThread(_lights[3],l=> { 
                        Buttons(Enabled:true);
                        foreach(Label light in _lights) light.Visible = false;
                    } );
                    // finally
                });
            };          

            _button3.Click+=async delegate 
            {
                Heading(_button3.Text);
                DumpThread("codebehind");
                Buttons(Enabled:false);

                DumpThread("async step 1"); 
                _lights[0].Visible = true; 
                await Task.Delay(500);

                DumpThread("async step 2"); 
                _lights[1].Visible = true; 
                await Task.Delay(500);

                DumpThread("async step 3");
                _lights[2].Visible = true; 
                await Task.Delay(500);

                DumpThread("async step 4"); 
                _lights[3].Visible = true; 
                await Task.Delay(500);

                Buttons(Enabled:true);
                foreach(Label light in _lights) light.Visible = false;              
            };

        }       

        // allow us to invoke a method on the control's UI threads (the thread that created the control)
        private delegate void Invoker(Control control);
        private void InvokeOnUIThread(Control control, Action<Control> action)
        {
            if (!this.InvokeRequired) action(control);
            control.Invoke(new Invoker(action),control);
        }

        private void Buttons(bool Enabled) {
            _button.Enabled = Enabled;
            _button2.Enabled = Enabled;
            _button3.Enabled = Enabled;
        }

        void InitialiseControls()
        {
            //this.SuspendLayout();
            Controls.AddRange(_lights);
            Controls.AddRange(new Control[] {_threadLabel, _button, _textbox, _label,_button2, _button3 });         
            foreach(var l in _lights) l.Visible = false;
            //this.ResumeLayout();
        }   

        private void Heading(string heading) {
            Console.WriteLine();
            Console.WriteLine(heading);
            Console.WriteLine("---------------");
        }

    }

单击每个按钮可模拟4个长时间运行的任务,这些任务一个接一个地执行。在每个任务之间,通过隐藏或显示4个绿色标签控件之一来更新UI以反映进度。上面的代码包括三种不同的方法来做到这一点,而不会阻止或冻结UI。我发现玩一个简单的单页winforms应用程序在尝试不同的异步和线程代码时非常有用。

欢呼,A

答案 1 :(得分:2)

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

自从&#34;主流&#34; 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基本上是一个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()
  );
}

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

最后,在现代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&#34;神奇地&#34;为我们处理所有这些异步回调,给我们留下与原始同步代码几乎完全相同的代码 - 但不需要多线程或阻止UI。最酷的部分是,您可以像处理 同步的方式一样处理错误 - tryfinallycatch ......它们都能正常工作就好像一切都是同步的一样。它并不能保护您免受所有异步代码的棘手(例如,您的UI代码变得可重入,类似于使用Application.DoEvents),但它确实很好整体工作:)

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

答案 2 :(得分:1)

如果符合您的需要,只需将代码添加到原始任务即可。但是,ContinueWith为您提供了更多灵活性。如果您有Task计算结果值,则可能还有几个依赖于该值作为输入的任务。 ContinueWith允许您独立注册所有这些。它还将创建原始任务的代码与连接依赖任务的代码分离。

看看你的第二个例子,“第二个任务代码”现在不可分割地连接到“第一个任务代码” - 这意味着程序的其他任何部分都无法使用第一个任务代码的结果甚至看不到当它可用时,除非你在“任务”框架之外添加额外的通信。

当然你可以自己实现它。但它确实为您节省了一些精力(例如确保取消和例外使整个设置按预期工作)。

答案 3 :(得分:1)

您有特定的延续选项,例如仅在第一个任务失败时运行。 在某些情况下,特别是因为async-await您无法控制第一个任务,所以您可以从框架或应用程序的不同部分获取它。 例子:

    Task task = DoSomethingAsync();
    task.ContinueWith(_ => DoSomethingLong(), TaskContinuationOptions.LongRunning);
    task.ContinueWith(_ => Console.WriteLine(_.Exception), TaskContinuationOptions.OnlyOnFaulted);

答案 4 :(得分:0)

这个答案取决于您的任务执行的时间。正如Jeffrey Richter在CLR via C#中所解释的那样(我强烈推荐这本书),如果您的任务执行时间超过大约五分之一秒,则Windows操作系统必须context switch。在高吞吐量环境中,上下文切换被认为是昂贵的操作。

如果你的所有方法都在~200毫秒内执行,那么将你的方法调用分成单独的任务可能是多余的。当然,除非你需要一些其他帖子中提到的任务调度选项。

http://www.amazon.com/CLR-via-C-Developer-Reference/dp/0735667454

http://msdn.microsoft.com/en-us/library/windows/desktop/ms682105(v=vs.85).aspx

答案 5 :(得分:0)

ContinueWith有价值的案例之一是将续约应用于汇总Task,例如通过Task.WhenAll获得。

Task combination diagram

List<Task> tasks = new List<Task> ();
for (int i = 0; i < 4; i++)
    tasks.Add (Task.Run (() => LongRunningOperation ()));
Task superTask = Task.WhenAll (tasks);
Task jobDone = superTask.ContinueWith (
    prevTask => MessageBox.Show ("Your operation is complete"));
return jobDone;

如果您的任务正在计算部分结果,然后您收集这些结果以形成最终结果,则此功能特别有用:

List<Task<int>> tasks = new List<Task<int>> ();
for (int i = 0; i < 4; i++)
    tasks.Add (Task.Run (() => LongRunningComputation ()));
Task<int[]> allCompleted = Task.WhenAll (tasks);
Task jobDone = allCompleted.ContinueWith (
    aggregateTask => MessageBox.Show ("And the result is: " + aggregateTask.Result.Sum ()));
return jobDone;