воскресенье, 19 февраля 2012 г.

Отменяемая асинхронная операция при помощи Task Parallel Library .NET 4

Недавно в одной WinForms программе мне понадобилось воспользоваться асинхронными операции, чтобы не блокировать графический интерфейс во время длительных расчетов. Это можно реализовать, конечно же, очень многими способами, от “ручного” программирования потоков (Threads) до использования таких конструкций как BackgroundWorker. Но мне захотелось (наконец-то!) попробовать новые библиотеки для параллельного программирования появившиеся в .NET 4 версии.

Итак, основные требования: длительная операция запускается пользователем, не должна блокировать графический интерфейс, должна быть отменяемой, результаты расчета отображаются на главной форме. Существенным облегчением было то, что одновременно могла быть запущена только одна операция.

Вот что у меня получилось:


    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
    using System.Threading;
    using System.Threading.Tasks;

    namespace WindowsFormsApplication1
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }

            private void startButton_Click(object sender, EventArgs e)
            {
                StopCalcIfRun();
                BeginCalc();
            }

            private void stopButton_Click(object sender, EventArgs e)
            {
                StopCalcIfRun();
            }

            // --------------------------------------

            class CalcResult
            {
                public int IntResult;
            }

            // объект задачи одновременно является флагом, нулевое значение которого указывает на то, запущена ли операция
            volatile Task calcTask = null;

            void StopCalcIfRun()
            {
                Task task = calcTask;

                if (task != null)
                {
                    // извлекаем CancellationTokenSource для отмены операции
                    CancellationTokenSource ts = (CancellationTokenSource)task.AsyncState;
                    ts.Cancel();

                    try
                    {
                        task.Wait();
                    }
                    catch (AggregateException e)
                    {
                        // Если e.InnerExceptions содержит исключение TaskCanceledException,
                        // то это указывает на "успешную" отмену задачи, а не на сбой
                    }
                }
            }

            void BeginCalc()
            {
                TaskScheduler uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();

                CancellationTokenSource tockenSource = new CancellationTokenSource();
                CancellationToken tocken = tockenSource.Token;

                // Создаем и запускаем длительную операцию.
                // Вниманине, tockenSource "закрепляется" за задачей как State Object, и используется для ее отмены в StopCalcIfRun методе
                calcTask = Task.Factory.StartNew((xxx) =>
                {
                    try
                    {
                        // уже отменили задачу?
                        tocken.ThrowIfCancellationRequested(); // *** Отмена задачи ***
                        CalcResult result = new CalcResult();

                        // длительные вычисления
                        result.IntResult++;
                        System.Threading.Thread.Sleep(1000);
                     
                        if (tocken.IsCancellationRequested) { // *** Отмена задачи ***
                            // ... освобождаем ресурсы
                            tocken.ThrowIfCancellationRequested();
                        }

                        // продолжаем длительные вычисления
                        result.IntResult++;
                        System.Threading.Thread.Sleep(1000);
                     
                        if (tocken.IsCancellationRequested) { // *** Отмена задачи ***
                            // ... освобождаем ресурсы
                            tocken.ThrowIfCancellationRequested();
                        }
                     
                        return result;
                    }
                    finally
                    {
                        calcTask = null;
                    }
                }, tockenSource, tocken);

                // Это продолжение будет вызвано в случае возникновения исключения (см. TaskContinuationOptions).
                // Исключение TaskCanceledException отменяющее задачу является корректным завершением задачи
                // и к таким исключениям не относится!
                calcTask.ContinueWith((ant) =>
                {
                     MessageBox.Show(ant.Exception.InnerException.ToString());
                }
                    , System.Threading.CancellationToken.None
                    , TaskContinuationOptions.OnlyOnFaulted
                    , uiTaskScheduler);

                // Продолжение если задача отменяется.
                // ant.Result есть null, не стоит к нему обращатся
                calcTask.ContinueWith((ant) =>
                {
                    resultTextBox.Text += "Canceled;";
                }
                    , System.Threading.CancellationToken.None
                    , TaskContinuationOptions.OnlyOnCanceled
                    , uiTaskScheduler);

                // Полное завершение задачи => отображаем результаты
                calcTask.ContinueWith((ant) =>
                {
                    resultTextBox.Text += ant.Result.IntResult + ";";
                }
                    , System.Threading.CancellationToken.None
                    , TaskContinuationOptions.OnlyOnRanToCompletion
                    , uiTaskScheduler);
            }
        }
    }


Ресурсы:

Threading in C#, Joseph Albahari
http://www.albahari.com/threading/part5.aspx

Перевод части статьи Джозефа Албахари (Joseph Albahari) от Sergey Teplyakov
http://sergeyteplyakov.blogspot.com/2010/09/52.html

Все дело в SynchronizationContext [RU. MSDN Magazine]
http://msdn.microsoft.com/ru-ru/magazine/gg598924.aspx

Отмена задач [RU. MSDN Статья]
http://msdn.microsoft.com/ru-ru/library/dd997396.aspx