1

I am trying to chain Task<T> objects in C# as done in JavaScript and without blocking the UI thread.

I see there is a similar question here, but it uses the non-generic Task object as a return type of the process functions. I try to do the same with Task<T>.

I also see that here is a closer question to my needs, but the accepted answer seems to use .Result twice, which I guess will block the UI thread. Also, note that I chain tasks dynamically, so I can't follow some easy workarounds. And also, the Then implementation given here seems synchronous too (I am not sure if simply changing the TaskContinuationOptions on this old sample code will do what I want).

Here is what I have right now, but I can't even make it compile without blocking the thread:

    // Initial dummy task.
    private Task<bool> taskChain = Task.Factory.StartNew<bool>(() => true);

    // Chain dynamically on button click.
    private async void DoSth_Click(object sender, RoutedEventArgs e)
    {
        var data = ....;
        System.Threading.Tasks.Task<bool> del = async (t) => { return await ConvertAsync(data); };
        taskChain = taskChain.ContinueWith<bool>(() => del);
        var res = await taskChain;
    }

I have tried various different approaches, but I don't see how I can turn Task<T> to Func<Task<T>, T> that ContinueWith<bool>() seems to require (at least without doing some nasty UI thread blocking operation).

I would expect this to be easy, but I don't quite see the solution here... Isn't there a good and easy way to do this?

(Note: I guess I should probably call Unwrap() after the ContinueWith() but this seems like a detail at this point...)

Community
  • 1
  • 1
NoOne
  • 3,851
  • 1
  • 40
  • 47
  • What are the tasks that you want to chain? Are they the result of invoking async methods? – Yacoub Massad Mar 15 '16 at 20:45
  • @YacoubMassad Yes, there are all async operations. – NoOne Mar 15 '16 at 20:47
  • @Kalten While you *could* refactor the OP's code to not use `ContinueWith`, it would require the use of multiple methods, not one (or anonymous `async` methods, as the OP is kinda sorta trying to do), and I don't think it'd be cleaner. – Servy Mar 15 '16 at 20:48
  • @Kalten I can't call them the one after the other in hardcoded code, because the user chains the tasks dynamically, it's not in predetermined order. – NoOne Mar 15 '16 at 20:49
  • 1
    u're just saying why other questions aren't answering your needs but dont explain what do you really need. what is "but I can't even make it compile without blocking the thread", how is compiling has anything to do with runtime errors? – Ori Refael Mar 15 '16 at 20:56
  • What happens if `ConvertAsync` returns false? – Yacoub Massad Mar 15 '16 at 21:00
  • @YacoubMassad The that value is stored in `res`, just like if it returns `true`. Subsequent code in the method can then do whatever it wants with that return value. – Servy Mar 15 '16 at 21:01
  • @OriRefael I basically needed to make it compile without making the code block the UI thread. That's all. I knew that using a few `.Result` would solve the compilation problem and chain the tasks, but it would also block the UI thread. Anyway, the question is answered now. Thanks. – NoOne Mar 15 '16 at 21:09

2 Answers2

4

UnWrap is your friend here. It'll allow you to have a continuation method that resolves to a Task, and then get a Task that represents that task before the continuation has even fired.

Also note that FromResult should be used to create an already completed task.

private Task<bool> taskChain = Task.FromResult(true);
private async void DoSth_Click(object sender, RoutedEventArgs e)
{
    var data = CreateData();
    taskChain = taskChain.ContinueWith(t => ConvertAsync(data))
        .Unwrap();
    var res = await taskChain;
}

Note that I'd advise against doing this in-line in a click handler. Create a class that is able to queue tasks, and then use that. Of course, such a queue class is just following this same pattern:

public class TaskQueue
{
    private Task previous = Task.FromResult(false);
    private object key = new object();

    public Task<T> Enqueue<T>(Func<Task<T>> taskGenerator)
    {
        lock (key)
        {
            var next = previous.ContinueWith(t => taskGenerator()).Unwrap();
            previous = next;
            return next;
        }
    }
    public Task Enqueue(Func<Task> taskGenerator)
    {
        lock (key)
        {
            var next = previous.ContinueWith(t => taskGenerator()).Unwrap();
            previous = next;
            return next;
        }
    }
}

This would allow you to write:

private TaskQueue taskQueue = new TaskQueue();
private async void DoSth_Click(object sender, RoutedEventArgs e)
{
    var data = CreateData();
    var res = await TaskQueue.Enqueue(ConvertAsync(data));
}

Now your mechanism of queuing tasks is separated from the business logic of what this click handler needs to do.

Servy
  • 202,030
  • 26
  • 332
  • 449
  • That was so easy that I feel embarrassed I haven't figure it out myself. I was obsessively using `ContinueWith()` instead... :( Thanks! :) And also thanks for the extra suggestions. :) – NoOne Mar 15 '16 at 21:01
  • That `TaskQueue` class is well thought-through. Very good! Thanks again. :) – NoOne Mar 15 '16 at 21:24
3

The easiest way to "chain" is to just await:

// Initial dummy task.
private Task taskChain = Task.FromResult(true);

// Chain dynamically on button click.
private async void DoSth_Click(object sender, RoutedEventArgs e)
{
  var data = ....;
  taskChain = ChainedConvertAsync(taskChain, data);
  var res = await taskChain;
  ...
}

private async Task<Result> ChainedConvertAsync(Task precedent, Data data)
{
  await precedent;
  return await ConvertAsync(data);
}

On a side note, avoid StartNew and ContinueWith; they are dangerous APIs due to their default schedulers.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Oh... Right ! I can do that too! I haven't thought of that either! Can you suggest an article explaining in more detail why `ContinueWith` is dangerous? Is it because it changes the context? I guess it makes it safe if I use `TaskScheduler.FromCurrentSynchronizationContext` as you mention here: http://stackoverflow.com/a/8767406/964053, right? Or there are still problems with Exception handling, etc? – NoOne Mar 17 '16 at 18:14
  • 1
    @NoOne: It's the same reasons as [why `StartNew` is dangerous](http://blog.stephencleary.com/2013/08/startnew-is-dangerous.html). More detail [here](http://blog.stephencleary.com/2015/01/a-tour-of-task-part-7-continuations.html). In summary: `ContinueWith` does not understand `async` delegates, has a bad default scheduler, and has non-optimal default continuation options. – Stephen Cleary Mar 17 '16 at 18:33