Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement cancellation of shared Task:s in C#

All, here is a question about design/best practices of a complex case of canceling Task:s in C#. How do you implement cancellation of a shared task?

As a minimal example, lets assume the following; we have a long running, cooperatively cancellable operation 'Work'. It accepts a cancellation token as argument and throws if it has been canceled. It operates on some application state and returns a value. Its result is independently required by two UI components.

While the application state is unchanged, the value of the Work function should be cached, and if one computation is ongoing, a new request should not start a second computation but rather start waiting for a result.

Either of the UI components should be able to cancel it's Task without affecting the other UI components task.

Are you with me so far?

The above can be accomplished by introducing an Task cache that wraps the real Work task in TaskCompletionSources, whose Task:s are then returned to the UI components. If a UI component cancels it's Task, it only abandons the TaskCompletionSource Task and not the underlying task. This is all good. The UI components creates the CancellationSource and the request to cancel is a normal top down design, with the cooperating TaskCompletionSource Task at the bottom.

Now, to the real problem. What to do when the application state changes? Lets assume that having the 'Work' function operate on a copy of the state is not feasible.

One solution would be to listen to the state change in the task cache (or there about). If the cache has a CancellationToken used by the underlying task, the one running the Work function, it could cancel it. This could then trigger a cancellation of all attached TaskCompletionSources Task:s, and thus both UI components would get Canceled tasks. This is some kind of bottom up cancellation.

Is there a preferred way to do this? Is there a design pattern that describes it some where?

The bottom up cancellation can be implemented, but it feel a bit weird. The UI-task is created with a CancellationToken, but it is canceled due to another (inner) CancellationToken. Also, since the tokens are not the same, the OperationCancelledException can't just be ignored in the UI - that would (eventually) lead to an exception being thrown in the outer Task:s finalizer.

like image 601
4ZM Avatar asked May 21 '12 15:05

4ZM


2 Answers

Here's my attempt:

// the Task for the current application state
Task<Result> _task;
// a CancellationTokenSource for the current application state
CancellationTokenSource _cts;

// called when the application state changes
void OnStateChange()
{
    // cancel the Task for the old application state
    if (_cts != null)
    {
        _cts.Cancel();
    }

    // new CancellationTokenSource for the new application state
    _cts = new CancellationTokenSource();
    // start the Task for the new application state
    _task = Task.Factory.StartNew<Result>(() => { ... }, _cts.Token);
}

// called by UI component
Task<Result> ComputeResultAsync(CancellationToken cancellationToken)
{
    var task = _task;
    if (cancellationToken.CanBeCanceled && !task.IsCompleted)
    {
        task = WrapTaskForCancellation(cancellationToken, task);
    }
    return task;
}

with

static Task<T> WrapTaskForCancellation<T>(
    CancellationToken cancellationToken, Task<T> task)
{
    var tcs = new TaskCompletionSource<T>();
    if (cancellationToken.IsCancellationRequested)
    {
        tcs.TrySetCanceled();
    }
    else
    {
        cancellationToken.Register(() =>
        {
            tcs.TrySetCanceled();
        });
        task.ContinueWith(antecedent =>
        {
            if (antecedent.IsFaulted)
            {
                tcs.TrySetException(antecedent.Exception.GetBaseException());
            }
            else if (antecedent.IsCanceled)
            {
                tcs.TrySetCanceled();
            }
            else
            {
                tcs.TrySetResult(antecedent.Result);
            }
        }, TaskContinuationOptions.ExecuteSynchronously);
    }
    return tcs.Task;
}
like image 51
dtb Avatar answered Nov 14 '22 22:11

dtb


It sounds like you want a greedy set of task operations - you have a task result provider, and then construct a task set to return the first completed operation, ex:

// Task Provider - basically, construct your first call as appropriate, and then 
//   invoke this on state change

public void OnStateChanged()
{
    if(_cts != null)
       _cts.Cancel();

    _cts = new CancellationTokenSource();
    _task = Task.Factory.StartNew(() =>
       {
           // Do Computation, checking for cts.IsCancellationRequested, etc
           return result;
       });
}

// Consumer 1

var cts = new CancellationTokenSource();
var task = Task.Factory.StartNew(() =>
  {
       var waitForResultTask = Task.Factory.StartNew(() =>
          {
              // Internally, this is invoking the task and waiting for it's value
              return MyApplicationState.GetComputedValue();
          });

       // Note this task cares about being cancelled, not the one above
       var cancelWaitTask = Task.Factory.StartNew(() =>
         {
              while(!cts.IsCancellationRequested)
                 Thread.Sleep(25);

              return someDummyValue;
         });

       Task.WaitAny(waitForResultTask, cancelWaitTask);

       if(cancelWaitTask.IsComplete)
          return "Blah"; // I cancelled waiting on the original task, even though it is still waiting for it's response
       else
          return waitForResultTask.Result;
  });

Now, I haven't fully tested this, but it should allow you to "cancel" waiting on a task by cancelling the token (and thus forcing the "wait" task to complete first and hit the WaitAny), and allow you to "cancel" the computation task.

The other thing is to figure out a clean way of making the "cancel" task wait without horrible blocking. It's a good start I think.

like image 33
Tejs Avatar answered Nov 14 '22 23:11

Tejs