Task Cancellation and TaskContinuationOptions

2019-02-10 15:58发布

问题:

I was just introduced to Tasks (TPL) yesterday, and so I tried to do a little sample project in order to develop an understanding of how to use them.

My sample project is setup with a start button that begins incrementing a progressbar. A second button to cancel the task. A text box to report when the continuation using the TaskContinuationOptions.OnlyOnRanToCompletion is called, and a text box to report when the continuation using the TaskContinuationOptions.OnlyOnCanceled is called.

I can create and execute a Task, but canceling it in a way that lets a continuation with the TaskContinuationOptions.OnlyOnCanceled flag to fire, has been a problem.

I create the tasks as follows:

private void StartTask()
{
    CancellationTokenSource tokenSource = new CancellationTokenSource();
    CancellationToken token = tokenSource.Token;

    Task task = null;
    task = Task.Factory.StartNew(() => DoWork(tokenSource), tokenSource.Token);

    //A list<CancellationTokenSource> so that I can cancel the task when clicking a button on the UI Thread.
    MyTasks.Add(tokenSource);

    Task completed = task.ContinueWith(result => TaskCompleted(), TaskContinuationOptions.OnlyOnRanToCompletion);
    Task canceled = task.ContinueWith(result => TaskCanceled(), TaskContinuationOptions.OnlyOnCanceled);
}

I cancel the task as follows:

private void CancelTasks()
{
    foreach (CancellationTokenSource tokenSource in MyTasks)
    {
        tokenSource.Cancel();                
    }
}

My worker function is as follows:

private void DoWork(CancellationTokenSource tokenSource)
{
    if (progressBar1.InvokeRequired)
    {
        progressBar1.Invoke(new Action(() => DoWork(tokenSource)));
        return;
    }

    try
    {
        bool dowork = true;
        while (dowork)
        {
            tokenSource.Token.ThrowIfCancellationRequested();

            if (progressBar1.Value == progressBar1.Maximum)
            {
                dowork = false;
            }
            Thread.Sleep(1000);
            progressBar1.PerformStep();
            Application.DoEvents();
        }
        countCompleted++;
    }
    catch (OperationCanceledException)
    {                
    }
}

In other posts that I have read, it has been suggested that tokenSource.Token.ThrowIfCancellationRequested() is what sets the condition evaluated by the TaskContinuationOptions.OnlyOnCanceled.

None of the examples that I have seen include the use of the:

catch (OperationCanceledException)
{
}

However, without it the programs stops when i call the tokenSource.Cancel();

As it stands, when I do call the tokenSource.Cancel(), the Continuation with the TaskContinuationOptions.OnlyOnRanToCompletion runs, instead of the TaskContinuationOptions.OnlyOnCanceled.

Clearly I'm not doing this correctly.

Edit:

Doing some further reading, I found a comment that states that:

"catch (OperationCanceledException) {} will set the task's status as RanToCompletion, not as Canceled"

So removing the catch (OperationCanceledException) {} allows the task's status to to be set to canceled, but the program breaks on the tokenSource.Token.ThrowIfCancellationRequested(); but if I then continue through the break, the continuation task with the TaskContinuationOptions.OnlyOnCanceled runs, which is good.

But how do I call tokenSource.Token.ThrowIfCancellationRequested() without allowing the program to break and while allowing the task status to be set to Canceled?

回答1:

The comments above are correct in terms of the debugger and the options required to prevent the debugger breaking. However, the following should give you a better example of how to use continuations and indeed how to handle exceptions thrown from tasks within those continuations...

A continuation can find out if an exception was thrown by the antecedent Task by the antecedent task's exception property. The following prints the results of a NullReferenceException to the console

Task task1 = Task.Factory.StartNew (() => { throw null; });
Task task2 = task1.ContinueWith (ant => Console.Write(ant.Exception());

If task1 throws an exception and this exception is not captured/queried by the continuation it is considered unhandled and the application dies. With continuations it is enough to establish the result of the task via the Status keyword

asyncTask.ContinueWith(task =>
{
    // Check task status.
    switch (task.Status)
    {
        // Handle any exceptions to prevent UnobservedTaskException.             
        case TaskStatus.RanToCompletion:
            if (asyncTask.Result)
            {
                // Do stuff...
            }
            break;
        case TaskStatus.Faulted:
            if (task.Exception != null)
                mainForm.progressRightLabelText = task.Exception.InnerException.Message;
            else
                mainForm.progressRightLabelText = "Operation failed!";
        default:
            break;
    }
}

If you don't use continuations you either have to wait on the task in a try/catch block or query a task's Result in a try/catch block

int x = 0;
Task<int> task = Task.Factory.StartNew (() => 7 / x);
try
{
    task.Wait();
    // OR.
    int result = task.Result;
}
catch (AggregateException aggEx)
{
    Console.WriteLine(aggEx.InnerException.Message);
}

Hope this helps.