locked
How to Correctly Cancel a TPL Task with Continuation

    Dotaz

  • I have a long running operation which I am putting on a background thread using TPL. What I have currently works but I am confused over where I should be handling my `AggregateException` during a cancellation request.

    In a button click event I start my process:

        private void button1_Click(object sender, EventArgs e)
        {
            Utils.ShowWaitCursor();
            buttonCancel.Enabled = buttonCancel.Visible = true;
            try
            {
                // Thread cancellation.
                cancelSource = new CancellationTokenSource();
                token = cancelSource.Token;

                // Get the database names.
                string strDbA = textBox1.Text;
                string strDbB = textBox2.Text;

                // Start duplication on seperate thread.
                asyncDupSqlProcs =
                    new Task<bool>(state =>
                        UtilsDB.DuplicateSqlProcsFrom(token, mainForm.mainConnection, strDbA, strDbB),
                        "Duplicating SQL Proceedures");
                asyncDupSqlProcs.Start();

                //TaskScheduler uiThread = TaskScheduler.FromCurrentSynchronizationContext();
                asyncDupSqlProcs.ContinueWith(task =>
                    {
                        switch (task.Status)
                        {
                            // Handle any exceptions to prevent UnobservedTaskException.            
                            case TaskStatus.Faulted:
                                Utils.ShowDefaultCursor();
                                break;
                            case TaskStatus.RanToCompletion:
                                if (asyncDupSqlProcs.Result)
                                {
                                    Utils.ShowDefaultCursor();
                                    Utils.InfoMsg(String.Format(
                                        "SQL stored procedures and functions successfully copied from '{0}' to '{1}'.",
                                        strDbA, strDbB));
                                }
                                break;
                            case TaskStatus.Canceled:
                                Utils.ShowDefaultCursor();
                                Utils.InfoMsg("Copy cancelled at users request.");
                                break;
                            default:
                                Utils.ShowDefaultCursor();
                                break;
                        }
                    }, TaskScheduler.FromCurrentSynchronizationContext()); // Or uiThread.

                return;
            }
            catch (Exception)
            {
                // Do stuff...
            }
        }

    In the method `DuplicateSqlProcsFrom(CancellationToken _token, SqlConnection masterConn, string _strDatabaseA, string _strDatabaseB, bool _bCopyStoredProcs = true, bool _bCopyFuncs = true)` I have

        DuplicateSqlProcsFrom(CancellationToken _token, SqlConnection masterConn, string _strDatabaseA, string _strDatabaseB, bool _bCopyStoredProcs = true, bool _bCopyFuncs = true)
        {
            try
            {
                for (int i = 0; i < someSmallInt; i++)
                {
                    for (int j = 0; j < someBigInt; j++)
                    {
                        // Some cool stuff...
                    }

                    if (_token.IsCancellationRequested)
                        _token.ThrowIfCancellationRequested();
                }
            }
            catch (AggregateException aggEx)
            {
                if (aggEx.InnerException is OperationCanceledException)
                    Utils.InfoMsg("Copy operation cancelled at users request.");
                return false;
            }
            catch (OperationCanceledException)
            {
                Utils.InfoMsg("Copy operation cancelled at users request.");
                return false;
            }
        }

    In a button Click event (or using a `delegate` (buttonCancel.Click += delegate { /*Cancel the Task*/ }`) I cancel the `Task` as follows:

        private void buttonCancel_Click(object sender, EventArgs e)
        {
            try
            {
                cancelSource.Cancel();
                asyncDupSqlProcs.Wait();
            }
            catch (AggregateException aggEx)
            {
                if (aggEx.InnerException is OperationCanceledException)
                    Utils.InfoMsg("Copy cancelled at users request.");
            }
        }

    This catches the `OperationCanceledException` fine in method `DuplicateSqlProcsFrom` and prints my message, but in the call-back provided by the `asyncDupSqlProcs.ContinueWith(task => { ... });` above the `task.Status` is always `RanToCompletion`; it should be cancelled!

    What is the right way to capture and deal with the `Cancel()` task in this case. I know how this is done in the simple cases shown in [this example from the CodeProject](http://www.codeproject.com/Articles/152765/Task-Parallel-Library-1-of-n#handlingExceptions) and from the [examples on MSDN](http://msdn.microsoft.com/en-us/library/dd537607.aspx) but I am confused in this case when running a continuation.

    How do I capture the cancel task in this case and how to ensure the `task.Status` is dealt with properly?

     


    "Everything should be made as simple as possible, but not simpler" - Einstein

    13. března 2012 14:43

Odpovědi

  • To get an OperationCancelledException, it needs to be thrown with the same token as the one passed to the Task's constructor.

    new Task<bool>(state =>
                        UtilsDB.DuplicateSqlProcsFrom(token, mainForm.mainConnection, strDbA, strDbB),
                        "Duplicating SQL Proceedures",
                        token);

    • Označen jako odpověď Camuvingian 14. března 2012 17:50
    14. března 2012 15:00

Všechny reakce

  • If you want the exception to propagate to the caller, you should replace

    return false;

    in the catch clause with

    throw;

    Using the keyword 'throw' without any parameter rethrows the exception you just caught.

    13. března 2012 15:36
  • I have managed to work that out but I still get `task.Status` as `TaskStatus.Faulted` event when I cancel. The inner exception caught in the countinuation is a OperationCancelledException, but I still get the Faulted condition. Any Ideas on that one?

    Thanks very much for your time.


    "Everything should be made as simple as possible, but not simpler" - Einstein

    13. března 2012 15:43
  • Actually you cannot throw from this execption. It gives an unhandled exception error!?

    "Everything should be made as simple as possible, but not simpler" - Einstein

    13. března 2012 16:19
  • To get an OperationCancelledException, it needs to be thrown with the same token as the one passed to the Task's constructor.

    new Task<bool>(state =>
                        UtilsDB.DuplicateSqlProcsFrom(token, mainForm.mainConnection, strDbA, strDbB),
                        "Duplicating SQL Proceedures",
                        token);

    • Označen jako odpověď Camuvingian 14. března 2012 17:50
    14. března 2012 15:00