WinRT, Реалізація асинхронних методів

WinRT --- Реалізація асинхронних методів

Раніше я показав, як написати метод із суфіксом Async, який викликає один або кілька інших асинхронних методів. Код, наданий програмістом для цього методу, виконується в потоці інтерфейсу користувача, хоча викликані з нього асинхронні методи можуть виконуватися у вторинних потоках.

Іноді додатку потрібно зробити масштабну обчислювальну роботу, здатну паралізувати виконання потоку інтерфейсу користувача. Якщо розбити цю роботу на безліч дрібних фрагментів, з'являється можливість використовувати для виконання DispatcherTimer або подію CompositionTarget.Rendering. Обробники подій виконуються в потоці інтерфейсу користувача, але навантаження розподіляється таким чином, що потік інтерфейсу нормально реагує на дії користувача.

Також можна виконати роботу у вторинному потоці. Одне з рішень полягає у використанні класу ThreadPool з простору імен Windows.System.Threading, але клас Task більш універсальний, тому я тут представлю саме це рішення.

Найпростіший метод Task.Run() отримує аргумент типу Action (метод без аргументів і без значення, що повертається) і виконує його в потоці, отриманому з пулу. Як правило, аргумент визначається лямбда-функцією.

Припустимо, є метод (з парою аргументів), виконання якого займає багато часу:

Запускати цей метод безпосередньо в потоці інтерфейсу користувача небажано, але його можна помістити в тіло лямбда-функції, що передається Task.Run(), для застосування await:

Оскільки Task.Run() виконує BigJob() у вторинному потоці, метод не може містити код звернення до об'єктівінтерфейсу користувача. А вірніше, якщо він повинен містити такий код, то для цього має використовуватись метод RunAsync() класу CoreDispatcher. Якщо з цим викликом RunAsync() повинен використовуватися оператор await, то метод BigJob() має бути оголошений із ключовим словом async і повертати об'єкт Task). А ось інший метод, що також займає багато часу, але повертає значення:

Цей метод теж було б небажано запускати в потоці інтерфейсу користувача, але можна безпечно запустити методом Task.Run():

Так як метод у тілі лямбда-функції, що передається Task.Run(), повертає double (повертається значення CalculateMagicNumber), то значення Task.Run(), що повертається, буде Task . Оператор await повертає значення double, обчислене методом CalculateMagicNumber.

Також метод CalculateMagicNumberAsync можна визначити так:

Цей метод можна викликати з потоку інтерфейсу користувача:

А можна зробити все в одному методі:

Якщо обчислення вимагають виклику інших асинхронних методів, цим викликам має передувати слово await, а лямбда-функція повинна оголошуватися з ключовим словом async:

Остання форма - за якої все зібрано в одному методі, - є найпростішою для реалізації скасування та сповіщень про хід виконання. У багатьох визначених вами асинхронних методах буде присутній будь-який цикл:

Такі цикли добре підходять для реалізації як скасування, так і сповіщень про хід виконання, але будьте обережні. Перевірка скасування або сповіщень не повинна виконуватись ні тисячі разів на секунду, ні через п'ять секунд. Щосекундно або кілька разів на секунду - найкраща частота. Наприклад, у цикли, що виконуються тисячі та мільйони разів, можна включити додаткову логіку, якаперевіряє скасування або оповіщення про хід виконання тільки в тому випадку, якщо змінна циклу повністю ділиться на 100.

Для реалізації скасування цього методу слід додати параметр типуCancellationToken і в зручній точці викликати для нього метод ThrowIfCancellationRequested():

Зверніть увагу: параметр cancellationToken також передається у другому аргументі Task.Run(). Це дозволяє скасувати завдання ще до його запуску. Тепер під час виклику методу CalculateMagicNumberAsync() необхідно передати об'єкт CancellationToken в останньому аргументі. Щоб отримати цей об'єкт, необхідно визначити поле для об'єкта типу CancellationTokenSource:

Цей об'єкт повинен бути визначений як поле, тому що до нього необхідно звертатися з методу, що ініціює відміну - швидше за все, на вимогу користувача:

До виклику CalculateMagicNumberAsync() необхідно створити новий об'єкт CancellationTokenSource і передати його властивість Token методу в блоці try:

Якщо викликати метод Cancel() об'єкта CancellationTokenSource, за наступного виклику методу ThrowIfCancellationRequested для об'єкта CancellationToken в асинхронному методі буде ініційовано виключення типу OperationCanceledException, яке перехоплюється кодом, що викликає асинхронний метод. Інші можливі винятки (швидше за все, пов'язані з файловим введенням/виводом або зверненнями по Інтернету) перехоплюються другим блоком catch.

Щоб асинхронний метод видавав оповіщення про хід виконання операції, слід додати ще один параметр. Цей параметр відноситься до типуIProgress, де T - тип, який використовуватиметься для передачі інформації про прогрес. Зазвичай як T використовується тип double, але ви оцінюватимете прогрес за шкалою від 0 до 1 або від 0 до 100 - вирішуйтесамі. У другому випадку замість double можна використати int. Я навіть бачив приклад, у якому T використовувався тип bool, а значення true позначало завершення операції!

Потім у якомусь зручному місці (можливо, у тій точці, де перевіряється скасування операції) оновлюється інформація про прогрес:

У цьому коді змінна циклу перетворюється на double; її значення лежать у діапазоні від 0 до 100 і є відсотком завершення операції (що робить їх дуже зручними для завдання властивості Value елемента ProgressBar). У деяких випадках на початку методу виводиться окреме повідомлення про нульовий прогрес, а в кінці – про досягнення максимального прогресу.

Також знадобиться метод, який отримує вибраний вами тип як параметр і оновлює інформацію про хід виконання:

Цей метод викликає в потоці інтерфейсу користувача. При виклику CalculateMagicNumberAsync() (який, як ви пам'ятаєте, знаходиться в блоці try) створюється об'єкт типу Progress, якому в останньому аргументі передається певний метод зворотного виклику:

Метод зворотного виклику не обов'язково оформляти у вигляді окремої функції, можна обмежитися простим лямбда-виразом:

Розглянемо практичний приклад.

Наступний тестовий проект WordFreq читає текстовий файл (скажімо, електронну книгу зі знаменитого веб-сайту «Project Gutenberg») та обчислює кількість повторень слів – наприклад, щоб ви могли дізнатися, скільки разів у тексті книги Германа Мелвілла «Мобі Дік» зустрічається слово «whale »(Кит). Власне, програма WordFreq жорстко запрограмована для книги «Мобі Дік», хоча процедура підрахунку слів у методі GetWordFrequenciesAsync(), звичайно, універсальна.

Метод GetWordFrequenciesAsync() отримує аргумент класу .NET Stream, тому що яхотів використати об'єкт .NET StreamReader для рядкового читання файлу. Також методу передаються аргументи CancellationToken та IProgress.

З значенням, що повертається, справа складніше. Метод використовує об'єкт .NET Dictionary для зберігання лічильника входжень кожного унікального слова у файлі. Відповідно ключ Dictionary відноситься до типу string, а значення типу int. Наприкінці методу функція LINQ - OrderByDescending() сортує словник за значеннями (тобто спочатку йдуть слова з найбільшою частотою входження). Результат є колекцією об'єктів типу:

Колекція, фактично повертається OrderByDescending(), є об'єктом узагальненого типу IOrderedEnumerable:

Це означає, що значення методу GetWordFrequenciesAsync(), що повертається, має тип:

А ось як виглядає сам метод:

Метод не містить обробки винятків. Якщо в конструкторі StreamReader або під час виклику ReadLineAsync ініціюється виняток, він має бути оброблений кодом, що викликає цей метод.

Файл XAML програми містить дві кнопки для запуску та скасування (остання спочатку заблокована), індикатор ProgressBar для відстеження прогресу, поле TextBlock для виведення інформації про помилки та панель StackPanel у ScrollViewer для списку слів та лічильників:

Файл відокремленого коду містить метод GetWordFrequenciesAsync(), а також кілька коротких методів для скасування та сповіщень про прогрес:

Єдина частина коду, яку ми ще не розглянули, - обробник Click кнопки "Почати". Передбачається, що він може багаторазово викликатися під час виконання програми, але при цьому не розрахований на повторний вхід (тобто не запускатиметься вдруге, доки не завершиться попередній запуск). Більшість логіки у методі пов'язані зініціалізацією StackPanel, ініціалізацією ProgressBar, зняттям та встановленням блокування кнопок. Зверніть увагу: всі звернення до файлів, а також виклик GetWordFrequenciesAsync() укладено в блок try:

Але тут виникає ще одна проблема: після того, як асинхронний метод поверне керування, програма повинна перемістити дані на панель StackPanel. Завдання розв'язується блоком начому в кінці методу. Цикл вимагає інтенсивної взаємодії з об'єктами інтерфейсу користувача (створення елемента TextBlock і додавання його на StackPanel) і просто не може виконуватися в іншому потоці. Навіть якщо обмежити список словами, що зустрічаються в «Мобі Діку» як мінімум двічі (як я зробив у своїй програмі), він міститиме майже 10 000 пунктів.

Такий цикл здатний паралізувати інтерфейс користувача, не дозволяючи йому відреагувати на дії користувача, і навіть заблокує появу даних на екрані. Рішення - хоч і не ідеальне - засноване на використанні наступної команди:

Цей виклик з await фактично дозволяє іншому коду виконатися в потоці інтерфейсу користувача, а потім повертає управління після завершення цього коду. Зокрема, «іншим» виконуваним кодом може стати код, реалізований у класі StackPanel, який розміщує нащадків TextBlock, і обробка дій користувача прокручування StackPanel в ScrollViewer.

Без цього виклику Task.Yield() список слів на екрані не з'явиться близько 5 секунд після того, як ProgressBar повідомить про досягнення максимального прогресу. Безумовно, через повторні виклики Task.Yield() виконання циклу займе значно більше часу (як ви побачите самі по затримці перед зняттям блокування з кнопки Start), але результати з'являться майже негайно. Ви також зможете прокрутити список до ньогозавершення і побачите, що слово "whale" у "Мобі Діке" зустрічається 963 рази:

winrt

В іншому, правильнішому вирішенні елемент StackPanel взагалі не використовується. Існують елементи керування, призначені спеціально для виведення списків. Зокрема, можна використовувати панель VirtualizingStackPanel, яка не створює елементи списку доти, доки вони не потраплять в область відображення в результаті прокручування.

Завдяки Windows 8, .NET і C# використовувати асинхронні методи стало простіше, ніж будь-коли, проте від розробника все ще потрібна уважність та ретельне тестування. Наприклад, на машині, яку я використовував під час написання статті, виконання методу GetWordFrequenciesAsync() займало до чотирьох секунд. Але коли я видалив перевірку скасування та сповіщень про прогрес, метод почав виконуватися менш ніж за секунду. Не знаю, як вам, а мені здається, що в односекундних асинхронних методах без скасування та сповіщень про прогрес можна обійтися.

Всі ці складності обумовлені ще й тим, що ми в якомусь сенсі намагаємося зробити неможливе: змусити комп'ютери виконувати багато роботи, але при цьому вдавати, ніби вони нічого не роблять. Програми Windows 8 мають виглядати так, ніби вони піднімають тяжкості без найменших зусиль, а для цього програмісту доведеться ґрунтовно попрацювати.

Для високотехнологічних виробництв потрібно створення системи чистих приміщень, у якій контролюватиметься забруднення мікрочастинками довкілля. Обладнання для чистих приміщень враховує концентрацію пилу, мікроорганізмів та частинок, що дозволяє забезпечити успішне виробництво високоточної електроніки.