13

I have some GUI on a bunch of LINQ queries. The queries take some time to execute, so I would like for the GUI to be responsive and show busyindicators and progress bars. Many of the queries are to check for certain conditions existing in the data. If the query returns an empty result, the app shall continue with the next query. If it returns a result, the return set will either be of severity "warnings" or "errors". If it is warnings, execution shall continue. If it is errors, it shall stop.

Much code plays "ping pong" with the threadpool and GUI. Quasi code:

TaskFactory.StartNew(()=>
    {
       Run in background
    }.ContinueInGui(()=>
    {
       Update something
    }).ContinueInBackground(()=>
    {
      Do more work;
    }).ContinueInGui(()=> etc etc

This is tidy and nice. However, I don't see how I can insert conditions to go different continuation routes or break off the continuation chain if errors are found in the data.

There is no method for ContinueWithIf( predicate ,delegate{},TaskScheduler) Do I use TaskCancellation, do I throw an exception? Or is there some simple branching mechanism that I'm not thinking of?

Tormod
  • 4,551
  • 2
  • 28
  • 50

2 Answers2

14

A good option here would be to use a CancelationTokenSource, and just mark it canceled if you want to "break" your continuation chain. By including TaskContinuationOptions.NotOnCanceled in the ContinueWith for subsequent tasks, you can have them not get scheduled at any point by marking a CancelationTokenSource as canceled.

If you really want to use a predicate, instead of setting up the continuation in the main method, you'd need to make a custom method to handle this for you. This can be done by having an extension method that attaches a continuation - that continuation can check the predicate, and fire off the continuation if appropriate. This would look something like:

public static Task ContinueWithIf(this Task task, Func<bool> predicate, Action<Task> continuation, TaskScheduler scheduler)
{
    var tcs = new TaskCompletionSource<object>(); 

    task.ContinueWith( t =>
    {
        if (predicate())
        {
            new TaskFactory(scheduler).StartNew( 
                () => 
                {
                    continuation(task); 
                    tcs.SetResult(null); 
                });
        }
        else
        {
            tcs.TrySetCanceled();
        }
    });

    return tcs.Task;
}

Granted, you'd probably want to make a version for Task<T> in addition, as well as handle the faulted/canceled states on the Task. That being said, it should function correctly.

Reed Copsey
  • 554,122
  • 78
  • 1,158
  • 1,373
  • Thank you. Kinda Aspect Oriented (-ish). But AFAICT it wont branch or break the execution. It will rightly make execution of the body of the next step depend on the predicate. But when chained together, Task3 will still be executed even when the conditional on Task2 failed. So the benefit is marginal unless I reuse some complex predicate. Do you agree with my thoughts? – Tormod Oct 25 '11 at 17:48
  • 1
    @Tormod: I edited to fix a bug (and make it compile) - with the current, the Task2 would show as canceled, so your Task3 can subscribe with `OnlyOnRanToCompletion`, and it would work properly... – Reed Copsey Oct 25 '11 at 17:52
  • Wohoo. +1 just for the taskcompletionsource, a class that I hope to apply much more in the time to come. This looks interesting. I'm still not positive that we found the best way to package this into an extension method that would result in a clean syntax. Preferably also wrapping the appropriate continuationoptions. But everything appears to be solveable. At least I see the control flow that will allow it to happen. And, having thought it over, "cancel" is an appropriate term. – Tormod Oct 25 '11 at 21:01
  • Thanks a million for that example. The OP's question was not the same than mine, but your example helped me a lot. Thanks. – vtortola Mar 23 '12 at 15:39
  • Out of curiosity, what is the reason for a `StartNew` (sub task) inside the predicated condition? couldn't all relevant parameters (scheduler, etc.) be passed directly to the outer `ContinueWith` call? – el2iot2 Feb 01 '13 at 20:18
  • @automatonic They could, but that would change the behavior. You wouldn't be able to do the check until the scheduler made the scheduling happen - if you were waiting on a UI thread scheduler, this would not require the UI thread to be available in order to schedule *unless the predicate succeeded*. If you used it in the other scheduler, it would wait for the scheduler to be free, *then* do the check – Reed Copsey Feb 01 '13 at 21:54
  • @ReedCopsey Ah. That certainly does make sense for OP's situation. Thanks for the clarification. – el2iot2 Feb 01 '13 at 23:08
5

If there are errors, you should consider making your task fault accordingly. Then you can use TaskContinuationOptions.OnlyOnRanToCompletion etc in the ContinueWith call.

Basically there are three possible states at the end of a task's life:

  • RanToCompletion
  • Canceled
  • Faulted

You can make ContinueWith apply to any sensible combination of those statuses, and you can attach different continuations to the same parent task if you want to do different things based on error vs success vs cancellation etc.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • They aren't application errors as such. My app is working as it should. The purpose of the app is to run checks. They may return records that are in "Error"/"Warning"/"Information" category (or empty). If they are in Error category, the user should rectify those issues before rerunning the checks. Think of it as a compiler that can return various sets of issues without it itself is crashing. I will check out the TaskContinuationOptions, but I feel I'm mixing apples and oranges. It's weird if that the dataflow cannot be datadriven without mapping the flow to Task execution error codes. – Tormod Oct 24 '11 at 08:42
  • @Tormod: Right, I do see your point. I don't know of a way to split the chain, I'm afraid. You'd have to call `ContinueWith` later, instead of setting up the chain at the point of task creation. Obviously async/await in C# 5 will make all of this easier... – Jon Skeet Oct 24 '11 at 08:51
  • 1
    @JonSkeet: You should be able to use a CancelationTokenSource to trigger a cancelation, combined with NotOnCanceled/OnlyOnCanceled to "split" the chain. This is effectively "cancelling" the continuation based on a predicate, as an application specific error was received. – Reed Copsey Oct 25 '11 at 17:39
  • @ReedCopsey: Yes, that would do it. It's still somewhat ugly - and wouldn't allow for easy differentiation between that and genuine external cancellation, of course. – Jon Skeet Oct 25 '11 at 17:52
  • @JonSkeet: True - though it depends a bit on the tasks. I kind of see this as a genuine cancelation - you're effectively canceling future tasks based on the result of the current task. – Reed Copsey Oct 25 '11 at 17:53
  • That being said, this is exactly why I'm really looking forward to await/async ;) – Reed Copsey Oct 25 '11 at 17:53
  • @Both : On that, gentlemen, we all agree. – Tormod Oct 25 '11 at 21:02