2

I'm confused about the TPL ContinueWith method. I don't understand why it's needed. Here's an example from MSDN that shows how to use ContinueWith:

static void SimpleContinuationWithState()
{
   int[] nums = { 19, 17, 21, 4, 13, 8, 12, 7, 3, 5 };
   var f0 = new Task<double>(() => nums.Average());
   var f1 = f0.ContinueWith(t => GetStandardDeviation(nums, t.Result));

   f0.Start();
   Console.WriteLine("the standard deviation is {0}", f1.Result);
}

It seems that I can remove the ContinueWith call without changing the results at all:

static void SimpleContinuationWithState()
{
   int[] nums = { 19, 17, 21, 4, 13, 8, 12, 7, 3, 5 };
   var f0 = new Task<double>(() => GetStandardDeviation(nums, nums.Average()));

   f0.Start();
   Console.WriteLine("the standard deviation is {0}", f0.Result);
}

This standard deviation example must be a contrived example, but I can't think of a reason to ever use ContinueWith. (unless some library call created the Task instead of me) In every case, can't I pull the ContinueWith call into the original Task? It'll still run asynchronously. There must be something I'm not understanding.

user2023861
  • 8,030
  • 9
  • 57
  • 86
  • Look at the other overloads, specifially the ones that take in a [`TaskContinuationOptions`](https://msdn.microsoft.com/en-us/library/system.threading.tasks.taskcontinuationoptions(v=vs.110).aspx) – Scott Chamberlain Sep 12 '16 at 20:08
  • 3
    How would you do this if you were calling a method that already returned a `Task`? The fact that you've got a contrived example where the original part really isn't asynchronous at all is unhelpful here... think about it as "I want to call a web service, then when that returns I want to call a second web service with the results of the first." Now think how you'd do that without ever blocking a thread. – Jon Skeet Sep 12 '16 at 20:14
  • @JonSkeet, I'd mark your comment as the answer if I could. I didn't consider that situation. – user2023861 Sep 14 '16 at 13:37

3 Answers3

3

You're assuming that every single Task is just a synchronous method that's being run in a thread pool thread. That's just not the case, and you shouldn't think of tasks that way. A Task is some kind of work that will finish at some point, possibly generating a value as a result. It could be running a method in a thread pool thread, it could be waiting for an event to fire, it could be waiting for the response to a network request, or for your hard drive to finish writing out some data to a file.

Yes, if your Task is specifically some synchronous code being run in a thread pool thread, and you always want some more synchronous code to run immediately after it on that same thread pool thread, and you're the one in control of creating that Task, and nothing else needs a Task representing that intermediate result, then you can just change what method to use to generate the Task as you showed (and if you are in that situation you're better of doing so). That ends up being a reasonably narrow case though.

Servy
  • 202,030
  • 26
  • 332
  • 449
1

As I describe on my blog, there is exactly one use case for ContinueWith: dynamic task parallelism.

It should not be used for:'

  • Asynchronous code.
  • Data parallelism.
  • Static task parallelism.

"Dynamic task parallelism" is when have a bunch of CPU-bound work to do that you want to use multiple threads for ("parallelism"), and you divide your work into multiple CPU-bound tasks ("task parallelism"), and you don't know how many tasks you'll need until you are already processing it ("dynamic task parallelism").

In other words, you should almost never use ContinueWith.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
0

Calling .Result is a blocking operation, for synchronous code - in particular when you know the result is already there. Using ContinueWith is scheduling a callback for when the result becomes available - async code (although not via the async/await pattern). Note that if the result is already available, the continuation is invoked directly back onto the calling thread.

Your existing code is bad and can cause fatal deadlocks. Don't do it :) You are getting away with it in this case, but there are many many gotchas in anything that does "sync over async" (aka "calling .Wait() or accessing .Result).

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • I'm confused about how my code will deadlock. If my continuation waits on the main thread in any way, I can see how I'd get a deadlock. But the only blocking operation in my example is the `Result` property. – user2023861 Sep 14 '16 at 13:32
  • @user2023861 simple; if the `async` operation tries to do an `await`, by default it tries to invoke via the sync-context; the simplest example is something like winforms, but it applies equally to anything (asp.net, wcf, whatever); now - your UI thread has started an `async` operation, and some cogs are spinning in the background (network, etc); now the UI code from your example doesn't use `await`, but instead calls `.Result` or `.Wait()` - that means the UI thread is blocked waiting for the result. But the worker *itself* tries to send something to the UI thread to complete... – Marc Gravell Sep 14 '16 at 14:59
  • @user2023861 the operation, but that can't get executed until the UI thread is available. And the UI thread can't become available until the async operation completes. In a single word: deadlock. This is not an imaginary situation; there are plenty of questions here on SO asking why their code has blocked. I asked one myself! http://stackoverflow.com/questions/13621647/using-async-even-if-it-should-complete-as-part-of-a-mvc-route-deadlocks-the – Marc Gravell Sep 14 '16 at 15:00
  • I see. I thought you were talking about a potential deadlock in the code in my original post which was copied from MSDN. – user2023861 Sep 14 '16 at 15:18