4

Consider the following code

Task<T>.Factory.StartNew(() => 
    {
      // block #1: load some data from local file cache
    }
  )
  .ContinueWith(task => 
    {
      // block #2: handle success or failure of load-from-cache operation and surface to application
    }, 
    cancellationToken,
    TaskContinuationOptions.NotOnCanceled,
    TaskScheduler.FromCurrentSynchronizationContext()
  )
  .ContinueWith(task => 
    {
      // block #3: load data from remote data source
    },
    TaskContinuationOptions.NotOnCanceled
  );

In .NET 4.0, this code executes as I expect: the first block runs in a background thread, then the second block runs, and finally the third block runs.

In .NET 4.5, however, the second block never runs, no matter what happens with the first block (success, fault, or cancellation). And the third block doesn't run either, waiting for the non-starting second block.

Application background

This code is in a WPF application. It's running during the initialization of the application, loading some data necessary for the app to start. On the main thread (where I call this async code from), I am waiting for the result to be populated from code block #3 before continuing. If the remote data call times out, initialization will continue with the data from block #1 (cached).

Things I've tried

  1. Add the TaskScheduler.FromCurrentSynchronizationContext() to the StartNew call. This causes the code in the StartNew lambda never to execute (and even if it did, I wouldn't want it to run on the main thread).
  2. Remove the TaskScheduler.FromCurrentSynchronizationContext() from the ContinueWith (and keep either the cancellationToken or the TaskContinuationOptions but not both because no overload supports only those two parameters). This appears to work (the code executes), but I'm concerned about side effects because I'm not sure why it works.
Dan Sinclair
  • 907
  • 1
  • 9
  • 27
  • 6
    Sounds like you're blocking the UI thread, so the continuation can't be posted to it. – Servy May 18 '15 at 20:55
  • Can you add brief about what application(Web, Windows, WPF etc) are you working on? and what is functionality you're trying to achieve in block#1? – vendettamit May 18 '15 at 20:59
  • @vendettamit Background and code intent added. Hopefully that helps. – Dan Sinclair May 18 '15 at 21:10
  • @Servy I think you may be right, but I'm not sure how else to wait for the result while also allowing the main thread to execute the operations from this code. I don't want the main thread to continue executing the rest of the initialization until I get this data back. – Dan Sinclair May 18 '15 at 21:12
  • If you are confirming that the UI thread is indeed blocked, then Servy's comment is the answer to the question. It sounds like then, you need to post a new question asking how to have the UI thread do what you want to do. – Moby Disk May 18 '15 at 21:19
  • 1
    Is the UI hung? Pause the debugger during the hand or during the time that you expect the task to run. What's on the stack? – usr May 18 '15 at 21:52
  • 4
    "On the main thread (where I call this async code from), I am waiting for the result to be populated from code block #3 before continuing." -- Yup, Servy is right. You want block #2 to run on the UI thread, but you block the UI thread until block #3 has completed, and block #3 only runs after block #2. That's never going to work. Why do you need to block the UI thread? If you don't want to run the rest of the initialisation yet, can you not simply move the rest of the initialisation to a continuation that runs after block #3? –  May 18 '15 at 21:56
  • Thanks, everyone, for the comments. This helped me identify the issue, along with @vendettamit's answer below. – Dan Sinclair May 20 '15 at 13:40

1 Answers1

2

Here we have problem with design of ContinueWith and TaskScheduler's properties Current and Default in these two versions of .Net

In .Net 4.0 the TaskScheduler's Current and Default both has same value i.e. ThreadPoolTaskScheduler, which is ThreadPool's context scheduler and it's not the one that updates the UI i.e. SynchronizationContextTaskScheduler which is why your code is running fine in .Net 4.0.

In .Net 4.5 things have changed. So when you say TaskScheduler.Current and TaskScheduler.Default then you'll get two different schedulers (in your case when WPF)

Current is = SynchronizationContextTaskScheduler

Default is = ThreadPoolTaskScheduler

Now coming back your problem, When you use ContinueWith option it has hard coded value for scheduler as TaskScheduler.Current. Specifically in WPF and Asp.net SynchronizationContextTaskScheduler means it's the UI thread synchronization context and once it's blocked nothing else will execute associated with it until the current executing thread is finished which running with in the UI Thread context.

Suggestions(.Net 4.5): Try passing TaskScheduler.Default(NON UI Scheduler) in ContiueWith or avoid using ContinueWith and rather join the tasks in queuing manner.

I hope this will give your clear picture why the behaviour is changing. For more details see this discussion: Why is TaskScheduler.Current the default TaskScheduler?

Community
  • 1
  • 1
vendettamit
  • 14,315
  • 2
  • 32
  • 54
  • 3
    Please place a comment if you're downvoting so that I know where my understanding was wrong. – vendettamit May 19 '15 at 14:20
  • Passing in `TaskScheduler.Default` instead of `TaskScheduler.FromCurrentSynchronizationContext()` resolved the issue for me. There are some other places in the application that follow the same pattern that no longer update things in the UI, but this helps me understand why and will make those easier to fix. Thanks, @vendettamit. – Dan Sinclair May 20 '15 at 13:43