Вы находитесь на странице: 1из 17

Tasks and Parallelism: The New Wave

of Multithreading
By Miguel Castro

Начиная с самого начала .NET разработчики смогли использовать многопоточность при разработке
приложений. На самом деле нам дана более чем одна модель программирования для размещения
практически любых требований, которые могут возникнуть. Есть класс Thread, пул потоков, шаблон Async и
класс Background Worker. Ну, как будто этого было недостаточно, и теперь у нас есть еще несколько
паттернов, которые приносят с собой еще один жанр - параллельное программирование.

Я начну с объяснения того, что подразумевается под параллельным программированием, потому что на
самом деле это не синоним многопоточного программирования. Параллельное программирование
охватывает более широкий спектр и относится к способности одновременного выполнения нескольких задач.
Параллельно не обязательно означает многопоточность. Это может означать грид-вычисления на многих
машинах. На самом деле даже термин многопоточный часто неправильно понимается. Потоки на CPU
немного отличаются от потоков, управляемых CLR, с использованием пула потоков. В этом случае потоки
более чем вероятно даже не поистине одновременны, но более схожи с временным разрезом, так как более
вероятно, что количество активных потоков превысит количество ядер на машине. Даже в сегодняшнем дне и
возрасте CPU (или ядро) способен делать только одну вещь в любой момент времени. Разработчикам .NET
мы предоставляем роскошь богатого набора инструментов, который позволяет нашим приложениям давать
своим пользователям представление о том, что многие вещи происходят одновременно. Этот вид важнее,
чем предполагаемый обман. Способность приложения демонстрировать пользователю, что многие вещи
происходят одновременно, часто важнее, чем его истинная мера скорости. Фактически, при многопоточности
часто бывает, что накладные расходы на управление потоками фактически влияют на производительность
выполнения кода отрицательным образом.

Способность приложения демонстрировать пользователю, что многие вещи


происходят одновременно, часто важнее, чем его истинная мера скорости.

Разработчики часто игнорируют реакцию среды вокруг приложения. С развитием мобильной разработки
разработчики больше не могут игнорировать ее. Подумайте о возможности перехода от выбора, который
вызывает длительный процесс для выполнения на вашем мобильном телефоне, или даже полностью
отменить приложение. Пользователи смартфонов стали воспринимать это как обычное поведение в каждом
мобильном приложении, которое мы используем, но правда в том, что это должно быть нормальное
поведение в каждом приложении, которое мы разрабатываем; Мобильный или нет.

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

.NET Parallelism
Большинство параллельных расширений в .NET Framework были выпущены с .NET 4, хотя я все еще нахожу их
сильно недоиспользуемыми. Visual Studio Async CTP (обновление SP1) позже представила ключевые слова
async и ожидания, и, конечно же, они превратились в .NET Framework 4.5. Я расскажу вам об этом, но это будет
бесполезно, если вы не будете довольны концепцией задач. Таким образом, части структуры, которые я
расскажу в этой статье, включают:

The Task Parallel Library


Ключевые слова async и await

Как и старые модели программирования, вы должны использовать новые с осторожностью. Многопоточное


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

В мобильном программировании вы должны держать параллелизм в авангарде своих мыслей. Зачем?


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

The Task Parallel Library


Возможно, самым большим дополнением к миру параллельного программирования в .NET является
параллельная библиотека задач. Эта библиотека включает содержимое пространства имен,
System.Threading.Tasks. Двумя основными классами здесь являются класс Task и Parallel. Эти два класса
составляют основу API, которую вы должны использовать в будущем для выполнения многопоточного
программирования, в дальнейшем называемого параллельным программированием. Я начну с фокусировки
на классе Task.

Task

Класс Task является центральным элементом параллельной библиотеки задач. Задача представляет собой
операцию, которая выполняется или запускается. Используя класс Task, вы получаете выгоду от
современного уровня API, который прост в использовании и обеспечивает исключительную гибкость. Другим
преимуществом параллельной библиотеки задач является то, что когда он включает многопоточность, он
использует пул потоков. Пул потоков управляет использованием потоков для максимальной пропускной
способности и масштабируемости. Задачи необязательно должны выполняться в отдельном потоке, но
использование класса Task, как я объясню в этой статье, будет иметь многопоточность. Вы можете
установить задачи для выполнения в текущем (или конкретном) потоке.

Простое выполнение задачи

В простейшей форме задача, определенная классом Task, содержит фрагмент кода, который определяет
выполняемую операцию.
Task task = new Task(() =>
{
Console.WriteLine("Task on thread {0}
started.",
Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(3000);
Console.WriteLine("Task on thread {0}
finished.",
Thread.CurrentThread.ManagedThreadId);
});

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

task.Start();
Console.WriteLine("This is the main thread.");

Так как код, определенный в задании, выполняется в другом потоке, любой код, следующий за исполнением
метода Start, будет продолжать выполняться немедленно. В этом конкретном примере, поскольку
асинхронный код просто засыпает свой поток в течение 3 секунд, перед текстом будет отображаться текст
«Это основной поток», «Задача по потоку« x »завершена».

Возвращаемое значение

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

Task<int> task = new Task<int>(() =>


{
Thread.Sleep(3000);
return 2 + 3;
});

Как иллюстрирует этот фрагмент, задача выполняет простую математическую операцию и возвращает ее
результат. Тип результата определяется в общем аргументе. Я могу использовать несколько методов для
изучения этих результатов. Самый простой способ - использовать свойство Result класса Task.

task.Start();
Console.WriteLine("This is the main thread.");
int sum = task.Result;
Console.WriteLine("The result from the task
is {0}.", sum);

Свойство Result возвращает значение, возвращенное кодом, определенным в задаче. Тип переменной Result -
это тип, определяемый общим аргументом, используемым при объявлении экземпляра Task. Важно отметить,
что ссылка на свойство Result будет блокировать вызывающий поток до завершения задачи. Таким образом,
в этом случае задача запускает 3-секундный цикл сна. В основном потоке вы сразу увидите сообщение,
которое следует методу Start, но сообщение результата не будет показываться до завершения задачи. Это
означает, что вы можете выполнить любой код, который вы хотите, между моментом запуска задачи и
временем, когда вы ссылаетесь на свойство Result, но когда вы доберетесь до последнего, вы будете ждать
завершения задачи и поток будет заблокирован пока вы ждете. Это важно помнить, когда вы вызываете
поток, является потоком пользовательского интерфейса.

Другой способ проверки возвращаемых значений из задачи иллюстрирует интерфейс класса Task. Метод
ContinueWith позволяет мне определить конструкцию кода обратного вызова, которая будет автоматически
выполняться, когда моя задача будет завершена.
task.ContinueWith(taskInfo =>
{
Console.WriteLine("The result from the
task is {0}.",
taskInfo.Result);
});

Аргументом для этого метода является тип Action с его общим аргументом, относящимся к типу задачи.
Таким образом, в этом случае сигнатурой метода ContinueWith является Action <Task <int >>. Если вы хорошо
разбираетесь в лямбда-выражениях (извините, у вас нет времени на такой учебник), вы это знаете, это
означает, что переменная лямбда имеет тип Task <int> и что ее значение будет представлять мою текущую
задачу. Из-за этого я могу получить доступ к переменной Result из фрагмента кода. Этот код обратного
вызова также называется «кодом продолжения задачи».

В методе ContinueWith вверх вы можете определить как класс Task, так и код c, а также код обратного вызова.
Затем используйте метод Start для выполнения. В этом случае что-нибудь после запуска будет
продолжаться. Всякий раз, когда задача завершается в фоновом потоке, код обратного вызова будет
выполнен. Важно отметить, что код обратного вызова выполняется в другом фоновом потоке, поэтому
применяются все правила маршалинга UI. Я объясню причину этого ниже.

Advertisement

Fluent API

Я сказал, что ContinueWith продемонстрировал API-интерфейс класса Task, так что первое, что именно
представляет собой API-интерфейс Flux? Проще говоря, и определил Мартин Фаулер, это
объектно-ориентированный API с акцентом на удобочитаемость. То, что делает API-интерфейс Flux визуально
уникальным, - это использование цепочки методов.

Прежде чем продемонстрировать это, позвольте мне сначала объяснить, что метод ContinueWith фактически
возвращает сам объект Task. Фактически, если вы добавите общий аргумент в ContinueWith <T>, он вернет
экземпляр Task <T>. Затем эта задача может также вызвать собственный метод продолжения и т. д. Именно
по этой причине код, определенный в методе ContinueWith, выполняется в другом фоновом потоке; Это сама
по себе другая задача. Отключение метода Start в начальной задаче начнет процесс. Первая задача будет
выполняться в фоновом потоке, и когда она будет завершена, ее обратный вызов, как это определено в его
методе ContinueWith, будет выполняться в другом фоновом потоке, а когда он будет завершен, другой поток
выполнит любой код, который может быть определен в другом методе ContinueWith , В листинге 1 показан код,
который я только что описал.

Вместо того, чтобы объявлять все это в отдельных строках кода с использованием разных переменных
задачи, я могу использовать API-интерфейс flant, чтобы сделать это более элегантным способом. Вы можете
связать методы ContinueWith один за другим, каждый из которых возвращает свой собственный экземпляр
класса Task или Task <T>. Но поскольку последняя задача, тип которой должен быть начальным типом задачи,
определенным в начале (и может быть не так), следующий синтаксис не будет работать:
// Does not work
Task<int> task = new Task<int>(() =>
{
Thread.Sleep(3000);
return 2 + 3;
}).ContinueWith<string>(taskInfo =>
{
Thread.Sleep(3000);
return "Miguel";
}).ContinueWith(taskInfo =>
{
Console.WriteLine("The result from the
second task is {0}.", taskInfo.Result);
});

Как вы можете видеть, последний метод ContinueWith возвращает экземпляр Task, а начальная переменная
определена типа Task <int>. Если вы попробуете просто изменить начальный тип объявления из Task <int> t o
Task, когда вы вызываете метод Start, вы получите сообщение об ошибке, в котором вы не можете выполнить
команду Start on ContinueWith. Мне нужна возможность объявить и списать за один шаг или одно
утверждение. Вот где на помощь приходит Task Factory.

Task Factory

Я могу легко изменить свой код, чтобы использовать фабрику задач. Я начинаю с создания экземпляра
класса TaskFactory <T>, где generic - это тип, который мне нужно вернуть. Это похоже на создание экземпляра
Task <T>. Используя заводский экземпляр, я могу определить новую задачу и выполнить ее, вызвав метод
StartNew. Аргумент метода StartNew - это тот же тип действия, что и конструктор класса Task, и через его
лямбда-выражение, затем может использоваться, чтобы содержать конструкцию кода, которая будет
выполняться асинхронно. Вы можете выполнить цепочку обратного вызова, используя ContinueWith точно так
же, как показано выше.

Чтобы сохранить еще больше кода, класс Task <T> содержит статическое свойство Factory, которое
возвращает экземпляр TaskFactory <T>.

Task<int>.Factory.StartNew(() =>
{
Thread.Sleep(3000);
return 2 + 3;
}).ContinueWith<string>(taskInfo =>
{
Thread.Sleep(3000);
return "Miguel";
}).ContinueWith(taskInfo =>
{
Console.WriteLine("The result from the
second task is {0}.", taskInfo.Result);
});

Приведенный выше код объявляет и снимает цепочку задач. Какой метод кода вы выбрали, зависит от вас. На
фабрике задач приятно иметь, если вы хотите воспользоваться API-интерфейсом, но много раз я
предпочитаю объявлять свои задачи отдельно от их запуска, поэтому я обычно использую Task <T> с
инструкцией ContinueWith.

При определении задачи вам может потребоваться использовать переменные, определенные за пределами
задачи. Для этого требуются обычные методы блокировки в случае, когда эти же переменные изменяются
параллельно во время выполнения задачи. Другой выбор - отправить состояние в задание. Это обеспечивает
лучшую инкапсуляцию и элегантность кода. Вы определяете переменную состояния в выражении лямбда,
которая определяет асинхронное выполнение конструкции кода. Вы получаете одну переменную состояния,
поэтому несколько значений должны быть обернуты в один класс или массив.
Task<int> task = new Task<int>((value) =>
{
int num = (int)value;
Thread.Sleep(3000);
return 2 + num;
}, 4);

Как вы можете видеть, значение переменной состояния предлагается как второй аргумент конструктора
класса Task.
Вы можете использовать обратный вызов для доступа к переменной состояния с использованием
переменной AsyncState экземпляра задачи, используемой в его выражении лямбда.

task.ContinueWith(taskInfo =>
{
Console.WriteLine("The state passed to the
task is {0}.", taskInfo.AsyncState);
Console.WriteLine("The result from the
task is {0}.", taskInfo.Result);
});

Passing Methods

При определении задач вам не нужно встраивать код для асинхронного выполнения. На самом деле, в этом
случае код - это код, и этот код может быть просто методом, от которого вы отложите фактическую обработку.

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

static int PerformMath(int amt1, int amt2)


{
return amt1 + amt2;
}

static void ShowResults(int result)


{
Console.WriteLine("Final result is {0}
and the thread is {1}.",
result,
Thread.CurrentThread.ManagedThreadId);
}

Теперь я могу настроить задачу с обратным вызовом, чтобы вызвать эти два метода. В этом примере я
буду использовать фабрику задач.
Task<int>.Factory.StartNew(() =>
PerformMath(5, 7)).ContinueWith(taskInfo =>
ShowResults(taskInfo.Result));

Отладка выполняемых задач

Каждый экземпляр Task содержит свойство ID, которое разработчики могут использовать для целей ведения
журнала (или любого другого) или для проверки активных задач с помощью окна инструмента
«Параллельные задачи» (рис. 1). Вы можете получить доступ к этому окну из меню Debug в Visual Studio 2012.
Это похоже на окно инструмента Threads и фактически может использоваться в сочетании с ним, но помните,
что есть моменты, когда задача может не выполняться в отдельном потоке, но Все еще необходимо
отслеживать во время отладки.
Figure 1: Parallel Tasks tool window.

Ожидание

После завершения асинхронной задачи основной поток может продолжать работать над чем-то другим. Тем
не менее, точка может наступить, когда основной поток завершил выполнение всей обработки, которую он
может выполнять параллельно, и что будет дальше, только если выполнение будет завершено. Я могу
обеспечить это с помощью метода Wait.

Task task = new Task(() =>


{
Thread.Sleep(3000);
});

task.Start();
task.Wait();
Console.WriteLine("This is the main thread.");

В приведенном выше коде вывод на консоль не будет выполняться до завершения задачи. Если я опускаю
вызов Wait, вывод на консоль произойдет сразу после запуска задачи.

Класс Task также имеет статический метод WaitAll, который принимает множество задач. Это позволило бы
мне остановить текущий поток, пока не завершится набор задач.

Advertisement

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

Отмена задачи

В какой-то момент вы можете решить, что вам нужно отменить длительную задачу из вызывающего потока.
Когда вы думаете об этом, на самом деле довольно опасно просто убивать запущенный процесс без
какого-либо надлежащего ведения домашнего хозяйства. Нам научили правильно закрывать наши ПК, а не
просто тянуть за штепсельную вилку, а задачи убийства довольно схожи. По этой причине параллельная
библиотека задач позволяет вам «запросить отмену», переведя ее на код реализации задачи, чтобы
проверить это.

Я должен сделать один шаг в этом процессе, прежде чем я смогу даже определить задачу. Прежде всего,
нужно определить экземпляр класса CancelationTokenSource. С помощью этого объекта я могу позже
запросить отмену задачи. Задача получает сообщение, потому что я собираюсь дать ему токен отмены,
предоставленный свойством Token объекта CancelationTokenSource. Я делаю это через второй аргумент
конструктора класса Task, когда я определяю задачу.
Позвольте мне показать вам пример того, что я имею в виду:

CancellationTokenSource tokenSource =
new CancellationTokenSource();
CancellationToken token = tokenSource.Token;

Task task = new Task(() =>


{
Console.WriteLine("Task on thread
{0} started.",
Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(5000);
if (!token.IsCancellationRequested)
Console.WriteLine("Task on thread
{0} finished.",
Thread.CurrentThread.ManagedThreadId);
else
Console.WriteLine("Cancelation
requested.");
}, token);

task.Start();
Thread.Sleep(1000);
Console.WriteLine("This is the main thread.");
tokenSource.Cancel();

Как вы можете видеть, когда задача объявлена, первым аргументом конструктора является, конечно же,
конструкция кода, а вторая - токен отмены. Впоследствии, когда я запускаю задачу, я выводю ее на консоль, а
затем запрашиваю отмену. Поскольку моя задача засыпает в течение 5 секунд, и запрос на отмену
происходит вскоре после ее запуска, когда код задачи проверяет IsKancellationRequested, он обнаруживает,
что это правда, и предпочитает не продолжать обработку.

Важно понимать, что задача должна решить, что делать, а точнее, что не делать, если она встречает запрос
об аннулировании.

Также обратите внимание на один второй сон сразу после запуска задачи. Я поместил это здесь по очень
интересной причине - тот, который я случайно обнаружил, потому что для него нет документации (по крайней
мере, того, что я нашел). Если я запустил задачу и немедленно выдал запрос об аннулировании, я столкнулся
с тем, что у задачи никогда не было возможности начать ее обработку. Процесс поворота потока достаточно
тяжелый, что задача задерживает миллисекунды за запросом отмены, но достаточно. В результате, если
задача вот-вот начнет свою обработку и уже запрошена для отмены, она просто отменяет себя. Это хорошее
поведение, потому что помните, что причина, по которой мы начинаем отменять «запросы», - это безопасная
обработка кода, но если этот код даже не начал выполняться, то совершенно безопасно просто его не
запускать. Я поставил задержку там только для целей этой демонстрации. Я оставил бы это в реальном
приложении.

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

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

Разработчики используют класс Parallel для сценария, называемого параллелизмом данных. Это сценарий,
когда одна и та же операция выполняется параллельно на разных элементах. Наиболее распространенным
примером этого являются элементы в коллекции или массиве, которым необходимо действовать. Используя
класс Parallel, вы можете параллельно выполнять операцию над членами коллекции или любой итерацией.

Хотя я могу, конечно, закодировать это вручную, используя класс Task внутри цикла for for for each, я могу
проще и быстрее закодировать его с помощью класса Parallel. У этого класса есть два метода, которые
позволят мне работать с циклами: For и ForEach. Как разработчики C # вы должны быть в состоянии выяснить,
для чего они предназначены по именам, но я покажу вам, как их использовать.

For

Метод For будет выполнять те же самые итерации, которые вы обычно делаете с помощью цикла for, но
каждая итерация будет выполняться в новой задаче. Это может означать другую тему. Это определяет
реализацию итерационного кода. Класс Parallel продолжает отслеживать код, выполняемый в используемых
им потоках. Если это длинная обработка, это увеличивает вероятность того, что больше потоков будет
запущено, чтобы помочь с оставшимися итерациями. Но помните, что по мере завершения каждой итерации
поток возвращается в пул потоков и может быть повторно использован. Это означает, что если итерация
цикла довольно мгновенная, она может повторно использовать поток из предыдущей итерации. Фактически,
даже основной поток, который начал итерацию, будет участвовать в итерациях.

Я начну с примера последовательного цикла:

for (int i = 0; i < 10; i++)


{
Console.WriteLine("Sequential iteration on
index {0} running on thread {1}.",
i, Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(200);
}

Теперь я буду выполнять тот же цикл, используя параллельную обработку:

Parallel.For(0, 10, i =>


{
Console.WriteLine("Sequential iteration on
index {0} running on thread {1}.",
i, Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(200);
});

Синтаксис довольно прост. Первые два аргумента - значения «от» и «до», а переменная в выражении лямбда
- это счетчик.

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

Advertisement
Параллельный пример не будет визуально демонстрировать какую-либо задержку, прежде чем вы увидите
вывод на консоль. Это связано с тем, что каждая итерация будет выполняться в другом потоке. Потоки могут
быть повторно использованы, так как задержка составляет всего 200 миллисекунд, так что, возможно, вы
увидите 3 или 4 потока, повторно используемых во время итераций. Фактически, я тестировал один и тот же
пример без каких-либо задержек и так быстро, что каждая итерация происходит в одном потоке. Это означает,
что каждая итерация обрабатывается так быстро, она возвращает поток в пул потоков как раз вовремя, чтобы
следующая итерация забирала его.

ForEach

Метод ForEach - это a для каждого цикла, что метод For для цикла for. Он позволяет выполнять параллельную
сборку или массив.

Вот пример обычной, последовательной итерации сбора:

foreach (string item in bands)


{
Console.WriteLine("Sequential iteration on
item '{0}' running on
thread {1}.",
item,
Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(200);
}

И это тот же цикл, используя Parallel.ForEach:

Parallel.ForEach(bands, item =>


{
Console.WriteLine("Sequential iteration on
item '{0}' running on
thread {1}.",
item,
Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(200);
});

Подобно методу For, первым аргументом является сбор, через который я хочу выполнить итерацию, а
переменная в выражении лямбда - это значение итерации. Кстати, в моем примере с полным кодом была
коллекция некоторых из моих любимых рок-групп, поэтому имя переменной коллекции.

Invoke

Класс Parallel имеет один дополнительный метод (Invoke), который можно использовать для быстрого
удаления асинхронной задачи без излишеств. Никакие излишества также не означают, что вы не получаете
возможности обратного вызова или отмены или каких-либо дополнительных функций, которые вы могли бы
получить при использовании класса Task.

Чтобы использовать метод I nvoke, я просто передаю ему пустое лямбда-выражение с кодом, который я хочу
выполнить асинхронно:

Parallel.Invoke(() =>
{
Console.WriteLine("This code is running on
thread {0}.",
Thread.CurrentThread.ManagedThreadId);
});
Важно отметить, что, хотя класс Parallel управляет возможным нерестом нескольких потоков для обработки
его итераций наиболее эффективным образом, он сам является синхронным. Это означает, что хотя код на
каждой итерации предназначен для асинхронного запуска, то только после завершения всех итераций
выполняется оператор после использования класса Parallel. Подумайте об этом как о большом синхронном
процессе, который, хотя он работает, отключается от нескольких асинхронных.

Все эти функции упростят и элегантны для написания параллельного кода. (Ух ты, я только что начал новую
фразу?) С бурей мобильных разработок, влияющих на нашу отрасль, знание того, как правильно писать такой
код, важнее, чем когда-либо. На мобильном устройстве крайне важно, чтобы приложение не блокировало
устройство в любой момент времени. С этим требованием нам нужно еще немного помочь в обеспечении
того, чтобы мы писали неблокирующий код, и что мы делаем это достаточно легко, чтобы иметь возможность
делать это достаточно часто. Таким образом, мы переходим к ключевым словам async и ожидаем.

Использование “Async”
.NET Framework содержит много вещей, которые мы называем синтаксическим сахаром. Этот термин
означает, что для выполнения чего-то с библиотекой базового класса .NET Framework, которая требует
специфического шаблона, обычно сложного, мы, как разработчики, даем роскошь простого синтаксиса кода.
Команде компилятора в Microsoft не нужно было добавлять новые функции и ключевые слова в компилятор
IL. Они просто модифицировали компиляторы языка, чтобы превратить простой синтаксис в сложный, когда он
генерирует IL во время компиляции.

Итераторы - прекрасный пример этого. Вместо того, чтобы учиться и повторять шаблон IEnumerable /
IEnumerator, который позволяет писать пользовательские итераторы, вы можете просто использовать
оператор yield, а компилятор языка определит, какой код действительно должен выглядеть и
превратить его в это. Другим примером является оператор блокировки. IL не распознает оператор
блокировки больше, чем распознает оператор yield. Вместо этого компилятор языка превращает
оператор блокировки в использование класса Monitor. Если вы еще не видели этот класс, класс M
onitor представляет собой другой способ обработки синхронизации и предоставляет больше
функциональности, чем оператор блокировки. Оператор блокировки представляет собой простой код
для быстрого доступа к одному из наиболее часто используемых шаблонов, доступных через класс
Monitor.

Асинхронные и ожидающие ключевые слова - это ИМХО, конечное представление синтаксического сахара,
потому что синтаксис, который они позволяют разработчикам, значительно проще, чем код, который
действительно необходим на уровне ИЛ для выполнения той же функциональности.

Microsoft представила async и ждет с Async CTP для .NET 4, и теперь они полностью квалифицированы
членами .NET 4.5.

Microsoft создала эти ключевые слова для предотвращения блокировки текущего потока и во многих случаях
допускает параллельное выполнение кода. Существуют вариации в отношении того, как это может быть
закодировано, а также вариации в отношении поведения каждого случая, но более полезный включает в себя
истинную многопоточность и гарантирует отсутствие блокировки пользовательского интерфейса во время
вызова. Я продемонстрирую это использование async и жду вас. Это тот, который был охвачен каркасом с
добавлением множества новых методов, разбросанных по различным подсистемам.

Advertisement
Вы можете вспомнить, что .NET Framework заполнена методами, родительский класс которых также содержит
соответствующие версии «Begin» и «End». Это классический асинхронный шаблон делегирования на работе,
и он хорошо зарекомендовал себя в течение очень долгого времени. Фактически, моя первая статья для
журнала CODE была названа «Async-Up Your Objects», и она показала, как включить этот шаблон в свои
собственные классы. Хотя он работал хорошо и был мощным шаблоном, асинхронный шаблон
делегирования был утомительным для записи иногда и требовал немного кода, особенно если вам нужны
обратные вызовы. Ну, все прогрессирует с новыми версиями фреймворка, и это не исключение. Возможно, вы
столкнулись с методами, имена которых заканчиваются словом Async. Эти методы представляют собой
новый способ выполнения асинхронных вызовов и демонстрируют гораздо более чистый шаблон
кодирования, благодаря ключевым словам async и ожидания.

Позвольте мне начать с краткого описания того, как работают эти два ключевых слова. Основное ключевое
слово здесь ждет, и оно может быть помещено перед всем, что возвращает экземпляр Task или Task <T>. Вы
можете использовать только ключевое слово ожидания в методе с ключевым словом async перед ним.
Ключевое слово await просто гарантирует, что текущий код не будет продолжаться до тех пор, пока значение,
«ожидаемое», будет полностью доступно. Он предлагает параллелизм, обертывая все, что следует за ним в
продолжении задачи. Вот где появляется синтаксическая сахарная магия компилятора. Возьмите следующий
пример кода:
async void button2_Click(object sender,
EventArgs e)
{
Trace.WriteLine(string.Format(
"Button pressed on thread {0}",
Thread.CurrentThread.ManagedThreadId));

Task<int> getLengthTask =
GetStringLengthAsync("Miguel A. Castro");

Thread.Sleep(1000);

Trace.WriteLine(string.Format(
"Continuing to run within button event
on thread {0}.",
Thread.CurrentThread.ManagedThreadId));

int length = await getLengthTask;

Trace.WriteLine(string.Format(
"The length of the string is {0}.",
length));

Thread.CurrentThread.ManagedThreadId));
}

Здесь я звоню на метод GetStringLengthAsync, который очень просто получает длину строки (да, я знаю, вы
можете сделать это с помощью string.Length - юмор меня здесь). Теперь вот где это может немного запутать и
даже ввести в заблуждение. Когда и где (какой поток) фактический код в GetStringLengthAsync выполняется,
зависит от того, как этот метод написан. Взгляните на эту первую версию:
async Task<int> GetStringLengthAsync(string text)
{
Trace.WriteLine(string.Format(
"Task on thread {0} started.",
Thread.CurrentThread.ManagedThreadId));
// something long running here
Trace.WriteLine(string.Format(
"Task on thread {0} finished.",
Thread.CurrentThread.ManagedThreadId));

return text.Length;
}

Здесь вы видите, что метод возвращает задачу, но фактически не запускает ее внутри; Или даже установить
один. Это потому, что я объявил этот метод с помощью ключевого слова async. Компилятор превращает его в
реальную задачу. Что вводит в заблуждение, это неправильное представление о том, что это будет
выполняться в другом потоке, чего не будет. В этом примере метод GetStringLengthAsync будет выполнять
свой код при его вызове и в том же потоке, что и вызывающий. Ожидание переменной getLengthTask в
вызывающей программе говорит компилятору обернуть все, что следует за ним в продолжении задачи.
Шаблон, хотя и настроен правильно, не дает вам какой-либо очевидной выгоды, потому что, как я уже сказал,
как вы пишете «ожидаемый» код, это определяет истинный параллелизм. Таким образом, вы можете сказать,
что async / await обеспечивает возможность параллелизма, но не обязательно многопоточность.

Новые методы, которые вы увидите, разбросаны по всей структуре, которые заканчиваются словом Async,
записываются как потенциально длительные задачи, поэтому они включают многопоточность. Так что теперь
я тоже перепишу перепишу мой код.

Task<int> GetStringLengthAsync(string text)


{
Task<int> task = Task.Run<int>(() =>
{
Trace.WriteLine(string.Format(
"StringLength task on
thread {0} started.",
Thread.CurrentThread.ManagedThreadId));
Thread.Sleep(5000);
Trace.WriteLine(string.Format(
"StringLength task on
thread {0} finished.",
Thread.CurrentThread.ManagedThreadId));

return text.Length;
});

return task;
}

Обратите внимание, что эта версия также возвращает задачу, которая, согласно моему ранее заявленному
правилу, может быть «ожидаемой». Просто вызов этого метода (вызывающий код не изменяется) начнет эту
задачу, потому что метод явно создает и выполняет задание. Task.Run - это просто ярлык для
Task.Factory.StartNew с некоторыми настройками по умолчанию. Обратите внимание, что этот метод не
отмечен ключевым словом async, потому что я не позволяю компилятору обернуть код в задаче, так как я явно
сделал это сам. Явное использование задачи также позволяет компилировать этот код с использованием
.NET 4 без Async CTP. Task.Run отключит свой код, используя поток из пула потоков, чтобы код вызова
продолжал работать. В этом случае, поскольку мой метод спящий в течение пяти секунд, в окне вывода будет
отображаться текст «Продолжение запуска в событии кнопки ...» сразу после того, как текст «Задача
StringLength запущена ...» (благодаря одной секунде задержки в вызывающем абоненте) , Но перед текстом
«StringLength task finished ...». Кроме того, поток I Ds для вызываемого метода будет отличаться от метода
вызывающего. Теперь я сказал, что вызывающий метод будет продолжать выполнять код даже после того,
как он вызовет метод GetStringLengthAsync, и он будет, вплоть до момента, когда он достигнет ожиданий от
переменной, возвращаемой вызовом. Моя вторая задержка обеспечила, что к моменту, когда я дойду до этой
строки, вызываемый метод все еще работает, но вызывающий абонент просто ждет, пока он не получит
результаты задачи, а затем продолжит, чтобы вернуть длину строки. Это связано с тем, что, как было
установлено в моем описании шаблона ранее, код, следующий за ожиданием, переносится в продолжение
задачи, поэтому он по сути является обратным вызовом. Тем временем, управление вернется к вызывающей
стороне кнопки Click event (U I), и вы не получите блокировки U I-thread. Теперь, хотя компилятор обертывает
код, следующий за ключевым словом ожидания, в продолжении задачи, этот код будет выполняться в
контексте синхронизации, под которым запускается метод вызова. Я указываю это, потому что раньше, когда
я проинструктировал вас о методе ContinueWith класса Task, я сказал, что код работает в еще одном фоновом
потоке, но, включив дополнительные функции Task, может быть определен конкретный контекст
синхронизации, и это то, что делает компилятор , Запуск в контексте U I означает, что совершенно безопасно
обновлять U I. Я бы ожидал такого поведения, если я собираюсь написать код, например, пример
вызывающего метода (кнопка Click event), где нет никаких указаний на то, что «Задача» имеет место.

Путаница, которую вы получите (как я, поверьте), когда вы читаете об этих функциях по всему Интернету,
заключается в том, что простое включение ключевых слов async / await в методе сделает этот метод
асинхронным, и это просто неверно. Помните, что только «Задача» или «Задача» <T> могут быть
«ожидаемыми» (на самом деле может быть и пустота), и как написан метод, который будет определять эту
задачу, - это то, что сделает этот процесс действительно параллельным.

Пример места, где этот шаблон используется в структуре, - это методы async, предоставляемые классом
HttpClient. Они используются для вызова REST и возврата строки данных. В зависимости от того, что
происходит за фактическими вызовами службы и где находится служба, получение возвращаемых строковых
данных по вызову REST может занять несколько секунд, поэтому вы не хотите блокировать вызывающий
поток во время этого процесса. Вызов метода GetStringAsync в классе HttpClient позволяет сделать вызов
REST еще одним потоком, пока вы можете продолжить работу над основным потоком. Теперь, поскольку я
открыл эту возможность червей, позвольте мне продолжить использовать это в качестве примера того, как
правильно сделать такой вызов.

У вас есть два способа вызвать метод, предназначенный для асинхронного запуска:

string getResponse = await client.GetStringAsync(


"http://localhost:1665/api/zipcode/07035");

И:

Task<string> getResponseTask =
client.GetStringAsync(
"http://localhost:1665/api/zipcode/07035");
// continue processing on the main thread
string getResponse2 = await getResponseTask;

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

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

В обоих приведенных выше примерах потребуются методы, в которых эти вызовы содержат ключевое слово
async в своем объявлении. Поскольку класс Task доступен в .NET 4, тогда как ключевые слова async и
ожидания присутствуют только в том случае, если был установлен Async CTP, методы Async могут быть
вызваны обычным способом и результирующее значение, полученное с помощью свойства Result.
string getResponse =
client.GetStringAsync(
"http://localhost:1665/api/zipcode/07035")
.Result;

Помните, что при доступе к свойству Result текущий поток будет блокироваться, в отличие от использования
ключевого слова await, где поток будет ждать завершения задачи, но не блокирует какой-либо
пользовательский интерфейс.

Вывод
Я мог бы долгое время заниматься этой темой. Один класс Task заполнен перегрузками для обсуждения, но я
хотел сконцентрироваться на шаблонах, которые играют в современном параллельном программировании.
Многие разработчики обсуждают эту тему, и каждый, кажется, выражает вещи по-другому. Наличие
нескольких точек зрения на потенциально запутанной, но важной функции может помочь вам понять ее; И
будьте уверены, что параллелизм движется к авангарду моделей программирования.

Sidebars:

Background vs. Foreground Threads


A thread is a thread is a thread. In the CLR-managed threads, what makes a thread a so-called “background thread” is the
fact that an application gives it no priority upon closing time. When an application is closed, it checks to see if there are
any other foreground threads running besides the primary one and if so, it will pause its shutdown until they finish their
work. Background threads will simply be killed. Thread pool threads are automatically background threads. To create a
foreground thread you must use Thread.Start.

References
Task-Based Asynchronous Pattern - Whitepaper

http://www.microsoft.com/en-us/download/details.aspx?id=19957

Visual Studio Asynchronous Programming Home Page

Includes whitepaper, videos, how-to, & Silverlight-based source explorer

http://msdn.microsoft.com/en-US/async

Parallel Programming in the .NET Framework - MSDN posting

http://msdn.microsoft.com/en-us/library/dd460693.aspx

Marshaling to the UI Thread


Marshaling became amazingly easier back in .NET 2.0 and I still find developers who don’t use Synchronization Context.
The key to using this is knowing when to grab it and that is when you are sure you are in the UI thread. A window’s
constructor is a good place for this. You simply store the value of SynchronizationContext.Current in a class variable of
type SynchronizationContext. Among other things, this will represent the execution context of the UI thread. At the point
where you want to execute code on the UI thread and you’re not on that thread (a callback or a ContinueWith perhaps),
you create an instance of the SendOrPostCallback delegate and pass in either a method or a code construct into its action
value. Then you use the sync-context variable’s Send or Post method to execute that code on the UI’s context.

Listing 1: Multiple tasks with ContinueWith


Task<int> task = new Task<int>(() =>
{
Console.WriteLine("Task on thread {0} started.",
Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(3000);
Console.WriteLine("Task on thread {0} finished.",
Thread.CurrentThread.ManagedThreadId);

return 2 + 3;
});

Task<string> task2 = task.ContinueWith<string>(taskInfo =>


{
Console.WriteLine("The result from the task is {0}.",
taskInfo.Result);
Console.WriteLine("Task2 on thread {0} started.",
Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(3000);
Console.WriteLine("Task2 on thread {0} finished.",
Thread.CurrentThread.ManagedThreadId);

return "Miguel";
});

task2.ContinueWith(taskInfo =>
{
Console.WriteLine("The result from the second task is
{0}.", taskInfo.Result);
});

task.Start();
Console.WriteLine("This is the main thread.");

Miguel Castro
Whether playing on the local Radio Shack’s TRS-80 or designing systems for clients around the globe, Miguel
has been writing software since he was 12 years old. He insists on staying heavily involved and up-to-date on
all aspects of software application design and development, and projects that diversity onto the type of
training and consulting he provides to his customers and believes that it’s never just about understanding the
technologies but how technologies work together. In fact, it’s on this concept that Miguel bases his Pluralsight
courses. Miguel has been a Microsoft MVP since 2005 and when he’s not consulting or training, he speaks at
conferences around the world, practices combining on-stage tech and comedy, and never misses a Formula 1
race. But best of all, he’s the proud father of a very tech-savvy 12 year old girl

This article was filed under:

.NET .NET .NET .NET Framework C# Patterns


3.5 4.0 Assemblies

This article was published in:

Вам также может понравиться