пятница, 18 января 2013 г.

Task Extensions в помощь

При разработке пользовательского интерфейса очень часто возникают ситуации, когда требуется получить некторые данные из различных источников (база данных, web-сервис, wcf-сервис и т.д.). При этом хорошим тоном будет отображать пользователю, что приложение выполняет запрос, а не "вешать" приложение до окончания выполнения операции. Соответственно выход из этой ситуации - запускать процессы получения данных в отдельных потоках.



Помимо того, что иногда будет требоваться передать данные в поток, так их еще нужно получить обратно в UI-поток и обработать, если есть, возникшие во время запроса ошибки. Задача не из простых. Но если использовать класс Task из .NET Framework 4, то задача немного упрощается, так как специально для этого класса я сделал несколько методов расширений, которые очень помогают в решении вышеописанных задач:

TaskExtensions.cs:
    /// <summary>
    /// Предоставляет доступ к методам расширения для типов Task.
    /// </summary>
    public static class TaskExtensions
    {
        #region Public Members

        /// <summary>
        /// Создает задачу продолжения, которая вызовется при завершении асинхронной операции.
        /// </summary>
        /// <param name="task">Асинхронная операция, которая может вернуть значение.</param>
        /// <param name="callback">Метод, который должен вызываться при завершении соответствующей асинхронной операции.</param>
        /// <param name="state">Состояние, используемое в качестве состояния AsyncState базового объекта Task.</param>
        /// <returns>Возвращает асинхронную операцию, которая не возвращает значение.</returns>
        public static Task ToAsync(this Task task, AsyncCallback callback, object state)
        {
            // Представляет сторону производителя задач Task, не привязанных к делегату и предоставляющих доступ к потребительской стороне через свойство Task.
            var taskCompletionSource = new TaskCompletionSource<object>(state);
            // Создает продолжение, которое выполняется асинхронно после завершения выполнения целевой задачи Task.
            task.ContinueWith(delegate
            {
                // Получает значение, указывающее, завершилась ли задача Task из-за необработанного исключения.
                if (task.IsFaulted)
                {
                    // Пытается перевести подлежащий объект Task в состояние Faulted.
                    if (task.Exception != null) taskCompletionSource.TrySetException(task.Exception.InnerExceptions);
                }
                // Получает значение, указывающее, завершилось ли выполнение данного экземпляра Task из-за отмены.
                else if (task.IsCanceled)
                {
                    // Пытается перевести подлежащий объект Task в состояние Canceled.
                    taskCompletionSource.TrySetCanceled();
                }
                else
                {
                    // Пытается перевести подлежащий объект Task в состояние RanToCompletion.
                    taskCompletionSource.TrySetResult(null);
                }
                // Ссылается на метод, который должен вызываться при завершении соответствующей асинхронной операции.
                if (callback != null) callback(taskCompletionSource.Task);

            }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
            // Возвращаем задачу
            return taskCompletionSource.Task;
        }

        /// <summary>
        /// Создает задачу продолжения, которая вызовется при завершении асинхронной операции.
        /// </summary>
        /// <param name="task">Асинхронная операция, которая может вернуть значение.</param>
        /// <param name="callback">Метод, который должен вызываться при завершении соответствующей асинхронной операции.</param>
        /// <param name="state">Состояние, используемое в качестве состояния AsyncState базового объекта Task.</param>
        /// <param name="taskScheduler">Объект, обрабатывающий низкоуровневую постановку задач в очередь на потоки.</param>
        /// <returns>Возвращает асинхронную операцию, которая не возвращает значение.</returns>
        public static Task ToAsync(this Task task, AsyncCallback callback, object state, TaskScheduler taskScheduler)
        {
            // Представляет сторону производителя задач Task, не привязанных к делегату и предоставляющих доступ к потребительской стороне через свойство Task.
            var taskCompletionSource = new TaskCompletionSource<object>(state);
            // Создает продолжение, которое выполняется асинхронно после завершения выполнения целевой задачи Task.
            task.ContinueWith(delegate
            {
                // Получает значение, указывающее, завершилась ли задача Task из-за необработанного исключения.
                if (task.IsFaulted)
                {
                    // Пытается перевести подлежащий объект Task в состояние Faulted.
                    if (task.Exception != null) taskCompletionSource.TrySetException(task.Exception.InnerExceptions);
                }
                // Получает значение, указывающее, завершилось ли выполнение данного экземпляра Task из-за отмены.
                else if (task.IsCanceled)
                {
                    // Пытается перевести подлежащий объект Task в состояние Canceled.
                    taskCompletionSource.TrySetCanceled();
                }
                else
                {
                    // Пытается перевести подлежащий объект Task в состояние RanToCompletion.
                    taskCompletionSource.TrySetResult(null);
                }
                // Ссылается на метод, который должен вызываться при завершении соответствующей асинхронной операции.
                if (callback != null) callback(taskCompletionSource.Task);

            }, CancellationToken.None, TaskContinuationOptions.None, taskScheduler);
            // Возвращаем задачу
            return taskCompletionSource.Task;
        }

        /// <summary>
        /// Создает задачу продолжения, которая вызовется при завершении асинхронной операции.
        /// </summary>
        /// <typeparam name="TResult">Тип результата, созданного данным объектом Task(Of TResult).</typeparam>
        /// <param name="task">Асинхронная операция, которая может вернуть значение.</param>
        /// <param name="callback">Метод, который должен вызываться при завершении соответствующей асинхронной операции.</param>
        /// <param name="state">Состояние, используемое в качестве состояния AsyncState базового объекта Task(Of TResult).</param>
        /// <returns>Возвращает асинхронную операцию, которая может вернуть значение.</returns>
        public static Task<TResult> ToAsync<TResult>(this Task<TResult> task, AsyncCallback callback, object state)
        {
            // Представляет сторону производителя задач Task(Of TResult), не привязанных к делегату и предоставляющих доступ к потребительской стороне через свойство Task.
            var taskCompletionSource = new TaskCompletionSource<TResult>(state);
            // Создает продолжение, которое выполняется асинхронно после завершения выполнения целевой задачи Task.
            task.ContinueWith(delegate
            {
                // Получает значение, указывающее, завершилась ли задача Task из-за необработанного исключения.
                if (task.IsFaulted)
                {
                    // Пытается перевести подлежащий объект Task(Of TResult) в состояние Faulted.
                    if (task.Exception != null) taskCompletionSource.TrySetException(task.Exception.InnerExceptions);
                }
                // Получает значение, указывающее, завершилось ли выполнение данного экземпляра Task из-за отмены.
                else if (task.IsCanceled)
                {
                    // Пытается перевести подлежащий объект Task(Of TResult) в состояние Canceled.
                    taskCompletionSource.TrySetCanceled();
                }
                else
                {
                    // Пытается перевести подлежащий объект Task(Of TResult) в состояние RanToCompletion.
                    taskCompletionSource.TrySetResult(task.Result);
                }
                // Ссылается на метод, который должен вызываться при завершении соответствующей асинхронной операции.
                if (callback != null) callback(taskCompletionSource.Task);

            }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
            // Возвращаем задачу
            return taskCompletionSource.Task;
        }

        /// <summary>
        /// Создает задачу продолжения, которая вызовется при завершении асинхронной операции с использованием указанного планировщика.
        /// </summary>
        /// <typeparam name="TResult">Тип результата, созданного данным объектом Task(Of TResult).</typeparam>
        /// <param name="task">Асинхронная операция, которая может вернуть значение.</param>
        /// <param name="callback">Метод, который должен вызываться при завершении соответствующей асинхронной операции.</param>
        /// <param name="state">Состояние, используемое в качестве состояния AsyncState базового объекта Task(Of TResult).</param>
        /// <param name="taskScheduler">Объект, обрабатывающий низкоуровневую постановку задач в очередь на потоки.</param>
        /// <returns>Возвращает асинхронную операцию, которая может вернуть значение.</returns>
        public static Task<TResult> ToAsync<TResult>(this Task<TResult> task, AsyncCallback callback, object state, TaskScheduler taskScheduler)
        {            
            // Представляет сторону производителя задач Task(Of TResult), не привязанных к делегату и предоставляющих доступ к потребительской стороне через свойство Task.
            var taskCompletionSource = new TaskCompletionSource<TResult>(state);
            // Создает продолжение, которое выполняется асинхронно после завершения выполнения целевой задачи Task.
            task.ContinueWith(delegate
            {
                // Получает значение, указывающее, завершилась ли задача Task из-за необработанного исключения.
                if (task.IsFaulted)
                {
                    // Пытается перевести подлежащий объект Task(Of TResult) в состояние Faulted.
                    if (task.Exception != null) taskCompletionSource.TrySetException(task.Exception.InnerExceptions);
                }
                // Получает значение, указывающее, завершилось ли выполнение данного экземпляра Task из-за отмены.
                else if (task.IsCanceled)
                {
                    // Пытается перевести подлежащий объект Task(Of TResult) в состояние Canceled.
                    taskCompletionSource.TrySetCanceled();
                }
                else
                {
                    // Пытается перевести подлежащий объект Task(Of TResult) в состояние RanToCompletion.
                    taskCompletionSource.TrySetResult(task.Result);
                }
                // Ссылается на метод, который должен вызываться при завершении соответствующей асинхронной операции.
                if (callback != null) callback(taskCompletionSource.Task);

            }, CancellationToken.None, TaskContinuationOptions.None, taskScheduler);
            // Возвращаем задачу
            return taskCompletionSource.Task;
        }

        #endregion
    }

Данный класс похож на один из Samples for Parallel Programming with the .NET Framework, но у него есть нюанс, о котором я расскажу чуть позже. Описывать сами методы я не буду (так как класс снабжен исчерпывающим количеством комментариев), но как их можно использовать я покажу.
Предположим, что у нас есть некий интерфейс доступа к данным:

IDataAccess.cs:
    /// <summary>
    /// The IDataAccess interface provides all the methods that the DAL supports.
    /// </summary>
    public interface IDataAccess
    {
        Task<IEnumerable<Object>> GetItems(long id);
    }

И есть коллекция объектов, которая отображается в интерфейсе пользователя и заполняется данными, полученными из метода GetItems(long id). Теперь, чтобы получить эти данные, проверить есть ли ошибки и отобразить список, нужно сделать следующее:

Code:
        /// <summary>
        /// Get items collection.
        /// </summary>
        public ObservableCollection<Object> Items { get; private set; }

        /// <summary>
        /// Get data.
        /// </summary>
        /// <param name="id">Id of object.</param>
        private void CommandExecute(long id)
        {
            IDataAccess dataAccess = new DataAccess();
            dataAccess.GetItems(id).ToAsync(Callback, null);
        }

        /// <summary>
        /// Callback method.
        /// </summary>
        /// <param name="asyncResult">IAsyncResult object.</param>
        private void Callback(IAsyncResult asyncResult)
        {
            var task = asyncResult as Task<IEnumerable<Object>>;
            if (task == null) return;
            switch (task.Status)
            {
                case TaskStatus.RanToCompletion:
                    // The task completed execution successfully.
                    foreach (var item in task.Result)
                    {
                        var model = item;
                        Application.Current.Dispatcher.Invoke(DispatcherPriority.DataBind, new Action(() => Items.Add(model)));
                    }
                    break;

                case TaskStatus.Faulted:
                    // The task completed due to an unhandled exception.
                    if (task.Exception != null) task.Exception.Handle(ExceptionHandle);
                    break;
            }
        }

Здесь в методе  CommandExecute(long id) мы создаем экземпляр интерфейса и вызываем его метод. Затем применяем метод расширения  ToAsync(Callback, null), где в качестве первого параметра передаем метод, который выполнится по завершению асинхронной операции, а в качестве второго параметра - объект состояния (сюда можно сохранить объект, который потребуется в методе обратного вызова). В самом методе Callback(IAsyncResult asyncResult) мы проверяем состояние выполненной задачи и если все отлично, добавляем элементы в список, если же овозникла ошибка, то запускаем обработчик. Так как этот метод выполняется в отдельном потоке, то для доступа к коллекции Items приходится использовать объект  Dispatcher, чтобы обновить интерфейс пользователя. Получается неплохо!
А теперь еще одна изюминка этих методов расширения: если вызов асинхронного метода изменить следующим образом:

Code:
        /// <summary>
        /// Get data.
        /// </summary>
        /// <param name="id">Id of object.</param>
        private void CommandExecute(long id)
        {
            IDataAccess dataAccess = new DataAccess();
            dataAccess.GetItems(id).ToAsync(Callback, null, TaskScheduler.FromCurrentSynchronizationContext());
        }

то метод, который должен вызываться при завершении соответствующей асинхронной операции  Callback(IAsyncResult asyncResult) будет вызываться синхронно, то есть в UI-потоке (в случае если сам вызов метода GetItems вызван в UI-потоке)! Это достигается благодаря  TaskScheduler.FromCurrentSynchronizationContext(), который запускает метод обратного вызова в пользовательском потоке. Тем самым необходимость использовать объект Dispatcher отпадает. 

2 комментария:

  1. Анонимный10 июля 2013 г., 15:38

    Спасибо за подсказку, данная статья помогла решить вопрос многопоточной загрузки картинок.

    ОтветитьУдалить
  2. Пожалуйста! Рад, что данная статья вам помогла :)

    ОтветитьУдалить