softhelp.ru! | Программирование


Сравнение с крупномодульным параллелизмом C#


До появления C# 5.0 асинхронное программирование было затруднено не только из-за отсутствия языковой поддержки, но и потому, что асинхронная функциональность в .NET Framework была доступна через неудобные шаблоны ЕАР и АРМ, а не посредством методов, возвращающих задачи.

Популярным обходным путем был крупномодульный параллелизм (для этого даже был предусмотрен тип по имени BackgroundWorker). Мы можем продемонстрировать крупномодульную асинхронность на исходном синхронном примере с GetPrimesCount, модифицировав обработчик событий кнопки следующим образом:

_button.Click += (sender, args) =>
{

_button.IsEnabled = false;
Task.Run (() =>Go());
};

В любом случае конечный результат заключается в том, что целый граф синхронных вызовов (Go плюс GetPrimesCount) выполняется в рабочем потоке.

Реализация отмены и сообщения о ходе работ создает больше возможностей для ошибок, связанных с нарушением безопасности потоков, как и любой дополнительный код в методе. Например, предположим, что верхний предел для цикла не закодирован жестко, а поступает из вызова метода:

for (int i = 1; i < GetUpperBound() ; i++)

Далее представим, что GetUpperBound () читает значение из конфигурационного файла, который загружается с диска при первом вызове. Весь этот код теперь выполняется в рабочем потоке — код, который, скорее всего, не является безопасным к потокам. В этом заключается опасность запуска рабочих потоков на высоких уровнях в графе вызовов.
Написание асинхронных функций

Благодаря любой асинхронной функции, возвращаемый тип void можно заменить типом Task, чтобы сделать сам метод пригодным для асинхронного выполнения (и применения await). Никаких других изменений не понадобится:

async Task PrintAnswerToLife() // Можно возвращать Task вместо void

{

await Task.Delay (1474);

int answer = 21 * 2;

Console.WriteLine (answer);

}

Обратите внимание, что мы не возвращаем явно задачу в теле метода. Компилятор произведет задачу, которая будет отправлять сигнал о завершении (или о возникновении необработанного исключения). Это упрощает создание цепочек асинхронных вызовов:

async Task Go()

{

await PrintAnswerToLife () ;

Console.WriteLine ("Done");

}

И поскольку метод Go объявлен с возвращаемым типом Task, сам Go допускает ожидание посредством await.

Компилятор расширяет асинхронные функции, возвращающие задачи, в код, использующий класс TaskCompletionSource для создания задачи, которая затем отправляет сигнал о завершении и отказе.

Компилятор в действительности обращается к TaskCompletionSource косвенно, через типы Async*MethodBuilder из пространства имен d'*! System. CompilerServices. Эти типы обрабатывают крайние случаи, такие как помещение задачи в состояние отмены при возникновении OperationCanceledException.

Базовый принцип проектирования с примененного ем асинхронных функций в С#.

1 Напишите синхронную версию методов.

2 Замените вызовы синхронных методов вызовами асинхронных методов и примените к ним await.

3 За исключением методов “верхнего уровня” (обычно обработчиков событий для элементов управления пользовательского интерфейса), обновите возвращаемые типы асинхронных методов на Task или Task<TResult>, чтобы они поддерживали await.

Способность компилятора производить задачи для асинхронных функций означает, что явно создавать экземпляр TaskCompletionSource почти всегда потребуется только в методах нижнего уровня, которые инициируют параллелизм с интенсивным вводом-выводом. (Для методов, которые инициируют параллелизм с интенсивными вычислениями, создается задача с помощью Task.Run.)

Параллелизм



Вызов асинхронного метода без его ожидания позволяет код, который следует выполнять параллельно. Отметим наличие кнопки, обработчик события которой вызывал метод следующим образом -

_button.Click += (sender, args) => Go();

Хотя Go и является асинхронным методом, мы не можем применить к нему await, и это действительно то, что содействует параллелизму, необходимому для поддержки отзывчивого пользовательского интерфейса.

Параллелизм, созданный подобным образом, происходит независимо от того, инициированы ли операции в потоке пользовательского интерфейса, хотя существует отличие в том, как он происходит. В обоих случаях мы получаем тот же самый “истинный” параллелизм в операциях нижнего уровня, которые его инициируют (таких как Task.Delay или код, переданный Task.Run). Методы, находящиеся выше этого в стеке вызовов, будут по-настоящему параллельными, только если операция была инициирована без контекста синхронизации; в противном случае они окажутся псевдопараллельными (и упростят обеспечение безопасности потоков), в соответствие с чем единственным местом, где может произойти вытеснение, является оператор await. Это позволяет, к примеру, определить разделяемое поле _х и инкрементировать его в GetAnswerToLife без блокировки:

async Task<int> GetAnswerToLife ()

{

_х--;

await Task.Delay (1474);

return 21 * 2;

}

(Тем не менее, мы не можем предположить, что _х имеет одно и то же значение до и после await.)