Ключевые слова async и await, введённые в C# 5.0, значительно упрощают асинхронное программирование. Они также скрывают за собой некоторые сложности, которые, если вы потеряете бдительность, могут добавить проблем в ваш код. Описанные ниже практики пригодятся вам, если вы создаёте асинхронный код для .NET приложений.
Используйте async /await только для тех мест, которые могут длиться «долго»
Здесь всё просто. Создание Task
и других структур для управления асинхронными операциями добавляет некоторые накладные расходы. Если ваша операция действительно продолжительна, например выполнение IO запроса, тогда эти расходы в основном не будут заметны. А в том случае, если ваша операция коротка или займёт несколько циклов процессора, тогда возможно будет лучше выполнять эту операцию синхронно.
В целом, команда, работавшая над .NET Framework, проделала неплохую работу по выбору функциональсти, которая должна быть асинхронной. Так, если метод фреймворка заканчивается на Async
и возвращает задачу, тогда, скорее всего вы должны использовать его асинхронно.
Предпочитайте async/await вместо Task
Написание асинхронного кода, используя async/await
, намного упрощает и сам процесс создания кода, и его чтение, нежели использование задач Task
.
public Task<Data> GetDataAsync() { return MyWebService.FetchDataAsync() .ContinueWith(t => new Data (t.Result)); }
public async Task<Data> GetDataAsync() { var result = await MyWebService.FetchDataAsync(); return new Data (result); }
В терминах производительности, оба метода, представленные выше, имеют небольшие накладные расходы, но они несколько по-разному масштабируются при увеличении количества задач в них:
-
Task
строит цепочку продолжений, которая увеличивается в соответствии с количеством задач, связанных последовательно, и состояние системы управляется через замыкания, найденные компилятором. Async/await
строит машину состояний, которая не использует дополнительных ресурсов при добавлении новых шагов. Однако компилятор может определить больше переменных для сохранение в стеки машины состояний, в зависимости от вашего кода (и компилятора). В статье на MSDN отлично расписаны детали происходящего.
В большинстве сценариев async/await
будет использовать меньше ресурсов и выполняться быстрее, чем задачи Task
.
Используйте уже выполненную пустую статическую задачу для условного кода
Иногда вы хотите запустить задачу только при каком-то условии. К сожалению, await
вызовет NullReferenceException
, если получит null
вместо задачи, а обработка этого сделает ваш код менее читабельным.
public async Task<Data> GetDataAsync(bool getLatestData) { Task<WebData> task = null; if (getLatestData) task = MyWebService.FetchDataAsync(); // здесь выполним другую работу // и не забудем проверить на null WebData result = null; if (task != null) result = await task; return new Data (result); }
Один из способов немного упростить код – использовать пустую задачу, которая уже выполнена. Полученный код будет чище:
public async Task<Data> GetDataAsync(bool getLatestData) { var task = getLatestData ? MyWebService.FetchDataAsync() : Empty<WebData>.Task; // здесь выполним другую работу // task всегда не null return new Data (await task); }
Убедитесь, что задача является статической и создана как завершённая. Например:
public static class Empty<T> { public static Task<T> Task { get { return _task; } } private static readonly Task<T> _task = System.Threading.Tasks.Task.FromResult(default(T)); }
Производительность: предпочитайте кэшировать сами задачи, нежели их данные
Существую некоторые накладные расходы при создании задач. Если вы кэшируете ваши результаты, но потом конвертируете их обратно в задачи, вы, возможно, создаете дополнительные объекты задач.
public Task<byte[]> GetContentsOfUrl(string url) { byte[] bytes; if (_cache.TryGetValue(url, out bytes)) // дополнительная задача создаётся здесь return Task<byte[]>.Factory.StartNew(() => bytes); bytes = MyWebService.GetContentsAsync(url) .ContinueWith(t => { _cache.Add(url, t.Result); return t.Result; ); } // это не потокобезоспасно (не копируйте себе этот код как есть) private static Dictionary<string, byte[]> _cache = new Dictionary<string, byte[]>();
Вместо этого будет лучше копировать в кэш сами задачи. В этом случае использующий их код может ждать уже выполненную задачу. В Task Parallel Library присутствуют оптимизации для того, чтобы код ожидающий выполнения уже завершённой задачи выполнялся быстрее.
public Task<byte[]> GetContentsOfUrl(string url) { Task<byte[]> bytes; if (!_cache.TryGetValue(url, out bytes)) { bytes = MyWebService.GetContentsAsync(url); _cache.Add(url, bytes); } return bytes; } // это не потокобезоспасно (не копируйте себе этот код как есть) private static Dictionary<string, Task<byte[]>> _cache = new Dictionary<string, Task<byte[]>>();
Производительность: понимайте, как await сохраняет состояние
Когда вы используете async/await
, компилятор создаёт машину состояний, которая хранит переменные и стек. Например:
public static async Task FooAsync() { var data = await MyWebService.GetDataAsync(); var otherData = await MyWebService.GetOtherDataAsync(); Console.WriteLine("{0} = "1", data, otherdata); }
Это создаст объект состояния с несколькими переменными. Смотрите, как компилятор сохранит переменные метода:
[StructLayout(LayoutKind.Sequential), CompilerGenerated] private struct <FooAsync>d__0 : <>t__IStateMachine { private int <>1__state; public AsyncTaskMethodBuilder <>t__builder; public Action <>t__MoveNextDelegate; public Data <data>5__1; public OtherData <otherData>5__2; private object <>t__stack; private object <>t__awaiter; public void MoveNext(); [DebuggerHidden] public void <>t__SetMoveNextDelegate(Action param0); }
Замечание 1. Если вы декларируете переменную, она сохранится в объекте, хранящем состояние. Это может привести к тому, что объекты будут оставаться в памяти дольше, чем вы бы могли ожидать.
Замечание 2. Но если вы не станете декларировать переменную, а использовать значение Async
вызова вместе с await
, переменная попадёт во внутренний стек:
public static async Task FooAsync() { var data = MyWebService.GetDataAsync(); var otherData = MyWebService.GetOtherDataAsync(); // промежуточные результаты попадут во внутренний стек и // добавятся дополнительные переключения контекстов между await-ами Console.WriteLine("{0} = "1", await data, await otherdata); }
Вы не должны слишком сильно волноваться по этому поводу до тех пор, пока вы не видите проблем производительности. Если вы всё-таки решили углубиться в оптимизацию, на MSDN есть хорошая статья по этому поводу: Async Performance: Understanding the Costs of Async and Await.
Стабильность: async/await – это не Task.Wait
Машина состояний, генерируемая async/await
– это не то же самое, что Task.ContinueWith/Wait
. В общем случае вы можете заменить реализацию с Task
на await
, но могут возникнуть некоторые проблемы производительности и стабильности. Давайте посмотрим подробнее.
Стабильность: знайте свой контекст синхронизации
Код .NET всегда исполняется в некотором контексте. Этот контекст определяет текущего пользователя и другие значения, требуемые фреймворком. В некоторых контекстах выполнения, код работает в контексте синхронизации, который управляет выполнением задач и другой асинхронной работы.
По-умолчанию, после await
код продолжит работать в контексте, в котором он был запущен. Это удобно, потому что в основном вы захотите, чтобы контекст безопасности был восстановлен, и вы хотите, чтобы ваш код после await имел доступ к объектам Windows UI, если он уже имел доступ к ним при старте. Заметим, что Task.Factory.StartNew
– не осуществляет восстановление контекста.
Некоторые контексты синхронизации не поддерживают повторный вход в них и являются однопоточными. Это означает, что только одна единица работы может выполняться в этом контексте одновременно. Примером этого может быть поток Windows UI или контекст ASP.NET.
В таких однопоточных контекстах синхронизации довольно легко получить deadlock. Если вы создадите задачу в однопоточном контексте, и потом будете ждать в этом же контексте, ваш код, который ждёт, будет блокировать выполнение фоновой задачи.
public ActionResult ActionAsync() { // DEADLOCK: это блокирует асинхронную задачу // которая ждёт, когда она сможет выполняться в этом контексте var data = GetDataAsync().Result; return View(data); } private async Task<string> GetDataAsync() { // простой вызов асинхронного метода var result = await MyWebService.GetDataAsync(); return result.ToString(); }
Стабильность: не используйте Wait
, чтобы дождаться окончания задачи прямо здесь
Как основное правило – если вы создаёте асинхронный код, будьте осторожны c использованием Wait
. (c await
всё несколько лучше.)
Не используйте Wait
для задач в однопоточных контекстах синхронизации, таких как:
- Потоки UI
- Контекст ASP.NET
Хорошая новость заключается в том, что фреймворк позволяет вам возвращать Task
в определённых случаях, и сам фреймворк будет ожидать выполнения задачи. Доверье ему этот процесс:
public async Task<ActionResult> ActionAsync() { // этот метод использует async/await и возвращает Task var data = await GetDataAsync(); return View(data); }
Если вы создаёте асинхронные библиотеки, ваши пользователи должны будут писать асинхронный код. Раньше это было проблемой, так как написание асинхронного кода было утомительным и уязвимым для ошибок, но с появлением async/await
большая часть сложности теперь обрабатывается компилятором. А ваш код получает большую надёжность, и вы теперь с меньше вероятностью будете вынуждены бороться с нюансами ThreadPool
.
Стабильность: рассмотрите использование ConfigureAwait
, если вы создаёте библиотеку
Если вы обязаны ожидать выполнения задачи в одном из этих контекстов, вы можете использовать ConfigureAwait
, чтобы сказать системе, что она не должна выполнять фоновую задачу в вашем контексте. Недостатком этого является то, что фоновая задача не будет иметь доступа к тому же самому контексту синхронизации, так что вы потеряете доступ к Windows UI или HttpContext
(хотя ваш контекст безопасности всё равно будет у вас).
Если вы создаёте «библиотечную» функцию, которая возвращает Task
, вы, скорее всего, не знаете, как она будет вызываться. Так что, возможно, будет безопаснее добавить ConfigureAwait(false)
к вашей задаче перед тем как её вернуть.
private async Task<string> GetDataAsync() { // ConfigureAwait(false) говорит системе, чтобы она // позволила оставшемуся коду выполняться в любом контексте var result = await MyWebService.GetDataAsync().ConfigureAwait(false); return result.ToString(); }
Стабильность: понимайте, как ведут себя исключения
Когда смотришь на асинхронный код, тяжело иногда сказать, что же случается с исключениями. Будет ли оно передано вызывающей функции, или тому коду, который ждёт выполнения задачи?
Правила в этом случае довольно прямолинейны, но всё равно иногда трудно ответить на вопрос, просто глядя на код.
Некоторые примеры:
- Исключения, вызванные из самого async/await метода, будут отправлены коду, ожидающему выполнения задачи (awaiter).
public async Task<Data> GetContentsOfUrl(string url) { // это исключение будет вызвано на коде, ожидающем // выполнения этой задачи if (url == null) throw new ArgumentNullException(); var data = await MyWebService.GetContentsOfUrl(); return data.DoStuffToIt(); }
- Исключения, вызванные из делегата задачи
Task
, тоже будут отправлены коду, ожидающему выполнения задачи (awaiter).
public Task<Data> GetContentsOfUrl(string url) { return Task<Data>.Factory.StartNew(() => { // это исключение будет вызвано на коде, ожидающем // выполнения этой задачи if (url == null) throw new ArgumentNullException(); var data = await MyWebService.GetContentsOfUrl(); return data.DoStuffToIt(); } }
- Исключения, вызванные во время создания Task, будут отправлены коду, который вызывал этот метод (caller) (что, в общем, очевидно):
public Task<Data> GetContentsOfUrl(string url) { // это исключение будет вызвано на вызывающем коде if (url == null) throw new ArgumentNullException(); return Task<Data>.Factory.StartNew(() => { var data = await MyWebService.GetContentsOfUrl(); return data.DoStuffToIt(); } }
Последний пример является одной из причин, почему я предпочитаю async/await
вместо создания цепочек задач посредством Task
.
Дополнительные ссылки (на английском)
- MSDN: Async/Await FAQ
- Об оптимизации await “быстрый путь”
- MSDN: Await, and UI, and deadlocks! Oh my!
- MSDN: Async Performance: Understanding the Costs of Async and Await
ссылка на оригинал статьи http://habrahabr.ru/post/162353/
Добавить комментарий