2
votes

I am working in C# on .NET 4.0 and have started replacing a number of nested BackgroundWorker setups with Task<T>.

The "nesting" is of this form:

var secondWorker = new BackgroundWorker();
secondWorker.DoWork += (sender, args) =>
{
    MoreThings();
};

var firstWorker = new BackgroundWorker();
firstWorker.DoWork += (sender, args) =>
{
    args.Result = this.Things();
};

firstWorker.RunWorkerCompleted += (sender, args) =>
{
    var result = (bool)args.Result;

    // possibly do things on UI

    if (result) { secondWorker.RunWorkerAsync(); }
};

secondWorker here plays the role of a callback for firstWorker. The equivalent when using Task<T>, as I understand it, are continuations with ContinueWith(); however, that doesn't allow me to decide whether to actually run the continuation from the control flow in a particular case.

A - from my understanding very unclean - workaround would be this:

var source = new CancellationTokenSource();
var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();

Task.Factory.StartNew(() => { return this.Things(); })
    .ContinueWith(t =>
        {
            // do things on UI

            if (!t.Result) { source.Cancel(); }
        }, CancellationToken.None, TaskContinuationOptions.NotOnFaulted, uiScheduler)
    .ContinueWith(t => { this.MoreThings(); }, source.Token);

This kind of works, but from all the examples I've seen, in this form (accessing the CancellationTokenSource from within the continuation chain - although the task that does is not using the token) it rather looks like abuse of the CancellationToken mechanism. How bad is this really? What would be the proper, "idiomatic" way to cancel the continuation chain based on information determined inside its flow?

(This code on the outside has the intended effect, but I assume it is the wrong way to solve the task with respect to using the existing tools. I am not looking for critique of my "solution" but for the proper way to do it. That's why I am putting this on SO rather than Code Review.)

1
In your example, source.Cancel() wouldn't actually do anything, as your not cooperatively checking if a cancelation was requested in your second ContinueWith. Is that what you want done? - Yuval Itzchakov
@YuvalItzchakov No, that works because the cancelled token is passed to the last ContinueWith() call which then doesn't run the continuation because of the cancellation. - TeaDrivenDev

1 Answers

1
votes

With continuations, C# also provides the async method feature. In this scheme, your code would look something like this:

async Task<bool> Things() { ... }
async Task MoreThings() { ... }

async Task RunStuff()
{
    if (await Things())
    {
        await MoreThings();
    }
}

The exact specifics depend on your implementation details. The important thing here is that via async and await, C# will automatically generate a state machine that will abstract all the tedium away from dealing with continuations, in a way that makes it easy to organize them.

EDIT: it occurred to me that your actual Things() and MoreThings() methods, you may not want to actually convert to async, so you can do this instead:

async Task RunStuff()
{
    if (await Task.Run(() => Things()))
    {
        await Task.Run(() => MoreThings());
    }
}

That will wrap your specific methods in an asynchronous task.

EDIT 2: it having been pointed out to me that I overlooked the pre-4.5 restriction here, the following should work:

void RunStuff()
{
    Task.Factory.StartNew(() => Things()).ContinueWith(task =>
    {
        if (task.Result)
        {
            Task.Factory.StartNew(() => MoreThings());
        }
    });
}

Something like that, anyway.