Россия, Тамбов, ТГТУ, 2009 |
Ключевые концепции компонентов WinRT
Основы асинхронности в WinRT-компонентах
В WinRT-компонентах есть три основных требования, следуя которым можно сделать любой данный метод асинхронным.
Во-первых, присоедините Async к имени метода. Это простое действие, которое, с технической точки зрения, ничего не дает, позволяет четко понять тому, кто вызывает этот метод, что работать с ним следует не так, как с асинхронным.
Во-вторых, возвращаемое значение метода должно быть одним из следующих интерфейсов (http://msdn.microsoft.com/library/windows/apps/br211924.aspx), показанных в таблице ниже, каждый из которых представляет определенноую комбинацию из асинхронных способов поведения, а именно, того, предоставляет ли метод результат и может ли метод сообщать о прогрессе хода операции.
Интерфейс (в Windows.Foundation) | Вариант использования |
---|---|
IAsyncAction | Используется для асинхронного метода, который не возвращает результатов (никакие аргументы не отправляются обработчику завершения) и не сообщает о ходе выполнения операции. |
IAsyncActionWithProgress<TProgress> | Используется для асинхронного метода, который не возвращает результатов, но сообщает о ходе выполнения операции, где <TProgress> это тип данных аргумента, который передается в обработчик прогресса операции. |
IAsyncOperation<TResult> | Используется для асинхронного метода, который возвращает результаты типа <TResult>, но не сообщает о ходе выполнения операции. |
IAsyncOperationWithProgress<TResult, TProgress=""> | Используется для асинхронного метода, который возвращает результат типа <TResult> и сообщает о ходе выолнения операции с помощью аргумента типа <TProgress> обработчику прогресса. |
Выбрав тип асинхронного метода, который мы создаем, теперь мы можем исполнять код метода в другом потоке. Здесь можно использовать потоки напрямую, использовать пул потоков, предоставляемый API Windows.System.Threading (http://msdn.microsoft.com/library/windows/apps/windows.system.threading.aspx), однако, имеются и более высокоуровневые конструкции и в C#/VB и в C++, которые значительно упрощают работу.
Асинхронные методы в C# и Visual Basic
В C# и Visual Basic имеется класс System.Threading.Tasks.Task (http://msdn.microsoft.com/library/system.threading.tasks.task.aspx) для данной цели. Объект Task, создается с помощью одного из статических методов Task.Run. Мы передаем этому механизму анонимную функцию (она называется делегатом в .NET, определяется с помощью лямбда-выражения =>), которая содержит код для выполнения. Для того, чтобы потом конвертировать объект Task в подходящий асинхронный интерфейс WinRT, мы вызываем методы расширения задачи AsAsyncAction or AsAsyncOperation. Вот как это выглядит:
public IAsyncOperation<string> SomeMethodAsync(int id) { var task = Task.Run<string> ( () => // () => в C# это то же, что function () в JS { return "Here is a string."; }); return task.AsAsyncOperation(); }
Внутри кода задачи производятся любые асинхронные операции (для чего мы используем ключевое слово C# await, как описано в вышеупомянутых сообщениях из блога), делегат должен быть маркирован с помощью async:
public IAsyncOperation<string> SomeMethodAsync(int id) { var task = Task.Run<string> (async () => { var idString = await GetMyStringAsync(id); //await делает асинхронный вызов выглядящим как синхронный return idString; }); return task.AsAsyncOperation(); }
Обратите внимание на то, что Task.Run не поддерживает сообщение о ходе выполнения операции, и методы расширения AsAsyncAction и AsAsyncOperation не поддерживают отмену. В подобных случаях вам нужно использовать класс System.Runtime.InteropServices.WindowsRuntime.AsyncInfo (http://msdn.microsoft.com/ru-RU/library/windows/apps/br206594.aspx) и один из его методов Run, который подходит под нужное асинхронное поведение. Вызов Task.AsAsyncOperation в конце не нужен здесь, так как AsyncInfo.Run уже предоставляет соответствующий интерфейс:
public IAsyncOperation<string> SomeMethodAsync(int id) { return AsyncInfo.Run<string> (async (token) => { var idString = await GetMyStringAsync(id); token.ThrowIfCancellationRequested(); return idString; }); }
В этом коде AsyncInfo.Run предоставляет делегата с аргументом типа System.Threading.CancellationToken (http://msdn.microsoft.com/library/windows/apps/system.threading.cancellationtoken.aspx). Для поддержки возможности отмены оерации, вы должны периодически вызывать метод маркера отмены ThrowIfCancellationRequested. Это позволяет распознать ситуацию, когда механизм, вызывавший асинхронный метод, отменил его (например, вызовом метода promise-объекта cancel). Так как отмена выполнения операции обычно инициируется пользователем, нет необходимости вызывать ThrowIfCancellationRequested его слишком часто. Если вызывать его каждые 50 миллисекунд или около того, приложение будет отлично реагировать на действия пользователя.
С другой стороны, если метод наподобие GetMyStringAsync принимает маркер отмены CancellationToken, вы можете просто передать ему маркер. Одно из преимуществ модели CancellationToken заключается в том, что она хорошо пригодна для компоновки: если вы получаете маркер в вашем собственном асинхронном вызове, вы можете передать его в любое количество других функций, которые вы вызвали, и которые так же поддерживают маркер. Если произошла отмена действия, запрос автоматически распространится на все подобные операции.
Обратите внимание на то, что WinRT-методы могут принимать маркеры отмены из-за перегрузки AsTask. Вместо этого:
await SomeWinRTMethodAsync();
Вы можете использовать это:
await SomeWinRTMethodAsync().AsTask(token);
Во всяком случае, учитывая эти примеры, вот асинхронная версия CountFromZero, которая не поддерживает отмену:
public static IAsyncOperation<double> CountFromZeroAsync(double max, double increment) { var task = Task.Run<double> (() => { double sum = 0; for (double x = 0; x < max; x += increment) { sum += x; } return sum; }); return task.AsAsyncOperation(); }
Интерфейс IAsyncOperation, возвращаемый этим методом, как и все асинхронные интерфейсты в Windows.Foundation, проецируется в JavaScript как promise-объект, таким образом, мы можем использовать обычный код для вызова методов и получения результатов (asyncVars это объект для хранения переменных):
asyncVars.startCS = new Date(); var promiseCS = PixelCruncherCS.Tests.countFromZeroAsync(max, increment); promiseCS.done(function (sum) { asyncVars.timeCS = new Date() - asyncVars.startCS; asyncVars.sumCS = sum; });
Используя подобный код в упражнении "Image Manipulation" вы можете начать асинхронную операцию счета (использовав кнопку Counting Perf (Async)) и немедленно после этого открыть изображение и выполнить конверсию в оттенки серого в то же самое время.
Асинхронные методы в C++
Для реализации асинхронных методов в C++, нам нужно прийти к тому же конечному результату, что и в C# - к методу, который возвращает один из интерфейсов IAsync* и исполняет собственный внутренний код в другом потоке.
Первая часть задачи весьма очевидна. Нам лишь нужно объявить метод с использованием типов C++ (здесь это показано в C++ коде; объявление класса в Grayscale.h выглядит точно так же):
using namespace Windows::Foundation; IAsyncOperation<double> ^ Tests::CountFromZeroAsync(double max, double increment)
Аналог класса AsyncInfo в C++ - это класс task (http://msdn.microsoft.com/library/windows/apps/hh750113.aspx), его можно найти в том, что называется Среда выполнения с параллелизмом для C++ (Parallel Patterns Library for C++, PPL) или Concurrency Runtime, ее пространство имен – concurrency (http://msdn.microsoft.com/library/windows/apps/dd492819.aspx) (используйте #include <ppltasks.h> и using namespace concurrency; вы можете так поступить в коде C++). Функция, которая создает задачу, называется create_async. Все, что нам нужно – это обернуть наш код в такую функцию, как показано ниже:
IAsyncOperation<double> ^ Tests::CountFromZeroAsync(double max, double increment) { return create_async([max, increment]() { double sum = 0; for (double x = 0; x < max; x += increment) { sum += x; } return sum; }); }
Как и в C#, здесь есть дополнительные структуры, предназначенные для организации вложенных асинхронных операций, поддерживающие отмену операции и сообщения о ходе выполнения операции. Подробности вы можете найти в материале документации "Асинхронное программирование на языке C++" (http://msdn.microsoft.com/library/windows/apps/hh780559.aspx) и "Параллелизм задач" (http://msdn.microsoft.com/library/windows/apps/dd492427.aspx).
Врезка: Объединение отложенных результатов
Есть одна особенность упражнения "Image Manipulation", которая использует преимущества того, что управление всеми асинхронными операциями реализуется с помощью promise-объектов. В приложении мы отображает горизонтальный индикатор выполнения перед началом всех асинхронных операций по кнопке Counting Perf (Async):
function testPerfAsync() { showProgress("progressAsync", true); //... }
Мы хотим, чтобы этот элемент управления оставался видимым во время выполнения любых асинхронных операций. Это легко можно сделать с помощью WinJS.Promise.join. Что здесь интересно отметить, этот то, что мы уже можем вызывать методы then или done у этих отдельных promise-объектов, что просто означает, что ы соединили различные обработчики для этих отдельных операций. Обработчики, которые мы передаем join, лишь привязаны к исполнению всех запрошенных отложенных операций:
promiseJS.done(function (sum) { // Вывод данных для рабочего процесса JS } promiseCS.done(function (sum) { // Вывод данных для компонента C# }) promiseCPP.done(function (sum) { // Вывод данных для компонента C++ }); WinJS.Promise.join([promiseJS, promiseCS, promiseCPP]).done(function () { // Скрыть индикатор выполнения когда все операции будут выполнены showProgress("progressAsync", false); });
В этом коде мы можем видеть, насколько все упрощается благодаря заключению механизма сообщений рабочего процесса внутрь promise-объекта. Не сделав этго, мы нуждались бы в управлении одним флагом для указания на то, получены ли отложенные результаты (устанавливая его в true внутри join), и другим флагом для того, чтобы показать, что получен результат от рабочего процесса (устанавливая его в true внутри обработчика сообщений рабочего процесса). Внутри join нам понадобилось бы проверять, завершена ли операция рабочего процесса, прежде чем скрыть индикатор выполнения. Обработчик сообщения рабочего процесса дела бы то же самое, проверяя, завершены ли операции в join. Подобными взаимоотношениями можно управлять в маленьких масштабах, но с множеством параллельных асинхронных операций все станет слишком запутанным – и именно в этом причина создания promise-объекта в первом случае!