Сценарии асинхронного программирования
Если код реализует сценарии с привязкой ввода-вывода для поддержки запросов к сетевым данным, доступа к базе данных или операций чтения и записи файловой системы, асинхронное программирование является лучшим подходом. Вы также можете написать асинхронный код для сценариев, связанных с ЦП, таких как дорогостоящие вычисления.
C# имеет асинхронную модель программирования на уровне языка, которая позволяет легко писать асинхронный код без необходимости переключать обратные вызовы или соответствовать библиотеке, поддерживающей асинхронность. Модель следует асинхронному шаблону на основе задач, известному как (TAP).
Изучение асинхронной модели программирования
Объекты Task
и Task<T>
представляют ядро асинхронного программирования. Эти объекты используются для моделирования асинхронных операций, поддерживая ключевые слова async
и await
. В большинстве случаев модель довольно проста для сценариев с привязкой ввода-вывода и ЦП. Внутри метода async
:
-
код с привязкой ввода-вывода запускает операцию, представленную объектом
Task
илиTask<T>
в методеasync
. - ЦП-зависимый код запускает операцию в фоновом потоке с помощью метода Task.Run.
В обоих случаях активная Task
представляет асинхронную операцию, которая может быть не завершена.
Именно с помощью ключевого слова await
творится вся магия. Он передает управление вызывающему метод, содержащий выражение await
, и в конечном счете позволяет пользовательскому интерфейсу быть отзывчивым, а службе — гибкой. Хотя существуют способы подходить к асинхронному коду помимо использования выражений async
и await
, в этой статье основное внимание уделяется языковым конструкциям.
Примечание.
В некоторых примерах, представленных в этой статье, используется класс System.Net.Http.HttpClient для скачивания данных из веб-службы. В примере кода объект s_httpClient
является статическим полем класса Program
типа:
private static readonly HttpClient s_httpClient = new();
Для получения дополнительной информации, например, полный пример кода находится в конце этой статьи.
Ознакомьтесь с основными понятиями
При реализации асинхронного программирования в коде C# компилятор преобразует программу в компьютер состояния. Эта конструкция отслеживает различные операции и состояние в коде, например выполнение, когда код достигает выражения await
, а также возобновляет выполнение после завершения фонового задания.
С точки зрения теории компьютерного анализа асинхронное программирование является реализацией модели Promise асинхронного.
В модели асинхронного программирования существует несколько ключевых понятий, которые необходимо понять:
- Вы можете использовать асинхронный код для привязанного к вводу-выводу и привязанного к ЦП кода, но реализация отличается.
- Асинхронный код использует объекты
Task<T>
иTask
в качестве конструкций для моделирования работы в фоновом режиме. - Ключевое слово
async
объявляет метод асинхронным методом, что позволяет использовать ключевое словоawait
в тексте метода. - При применении ключевого слова
await
код приостанавливает вызывающий метод и возвращает управление вызывающей стороне до завершения задачи. - Выражение
await
можно использовать только в асинхронном методе.
Пример задачи, зависящей от ввода-вывода: загрузка данных из веб-сервиса
В этом примере, когда пользователь выбирает кнопку, приложение скачивает данные из веб-службы. Вы не хотите блокировать поток пользовательского интерфейса для приложения во время загрузки. Следующий код выполняет эту задачу:
s_downloadButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI as the request
// from the web service is happening.
//
// The UI thread is now free to perform other work.
var stringData = await s_httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};
В этом коде намерение (скачивание данных в асинхронном режиме) выражается без запутанных операций с объектами Task
.
Пример, привязанный к ЦП: выполнение вычисления игры
В следующем примере мобильная игра наносит урон нескольким агентам на экране в ответ на нажатие кнопки. Выполнение вычисления ущерба может быть дорогостоящим. При выполнении вычисления в потоке пользовательского интерфейса могут возникнуть проблемы с отображением и взаимодействием пользовательского интерфейса во время вычисления.
Лучший способ обрабатывать задачу — запустить фоновый поток для завершения работы с помощью метода Task.Run
. Операция даёт результаты посредством использования выражения await
. Операция возобновляется после завершения задачи. Такой подход позволяет пользовательскому интерфейсу работать плавно, пока работа завершается в фоновом режиме.
static DamageResult CalculateDamageDone()
{
return new DamageResult()
{
// Code omitted:
//
// Does an expensive calculation and returns
// the result of that calculation.
};
}
s_calculateButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI while CalculateDamageDone()
// performs its work. The UI thread is free to perform other work.
var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};
Код четко выражает намерение события кнопки Clicked
. Для этого не требуется управлять фоновым потоком вручную, и задача завершается без блокировки.
Распознавание сценариев с привязкой ЦП и операций ввода-вывода
В предыдущих примерах показано, как использовать модификатор async
и выражение await
для ввода-вывода и процессорных задач. Пример для каждого сценария показывает, как код отличается в зависимости от того, где связана операция. Чтобы подготовиться к реализации, необходимо понять, как определить, когда операция привязана к операции ввода-вывода или ЦП. Выбор реализации может значительно повлиять на производительность кода и потенциально привести к неправильному применению конструкций.
Перед написанием любого кода необходимо задать два основных вопроса:
Вопрос | Сценарий | Реализация |
---|---|---|
Следует ли код ожидать результата или действия, например данных из базы данных? | ограничение по вводу-выводу | Используйте модификатор async и выражение await без метода Task.Run . Избегайте использования параллельной библиотеки задач. |
Должен ли код выполнять дорогостоящие вычисления? | с привязкой к ЦП | Используйте модификатор async и выражение await , но отключите работу над другим потоком с помощью метода Task.Run . Этот подход устраняет проблемы с скоростью реагирования ЦП. Если к задаче применим параллелизм, рассмотрите возможность использования библиотеки параллельных задач. |
Всегда измеряйте выполнение кода. Возможно, вы обнаружите, что работа, связанная с ЦП, недостаточно затратна по сравнению с затратами на переключения контекста при многопоточной работе. Каждый выбор имеет компромиссы. Выберите правильный компромисс для вашей ситуации.
Изучение других примеров
Примеры в этом разделе демонстрируют несколько способов написания асинхронного кода в C#. Они охватывают несколько сценариев, которые могут возникнуть.
Извлечение данных из сети
Следующий код загружает HTML из заданного URL-адреса и подсчитывает количество случаев, когда строка .NET возникает в HTML. Код использует ASP.NET для определения метода контроллера веб-API, который выполняет задачу и возвращает количество.
Примечание.
Если вы планируете проанализировать HTML в рабочем коде, не используйте регулярные выражения. Используйте библиотеку парсинга.
[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCount(string URL)
{
// Suspends GetDotNetCount() to allow the caller (the web server)
// to accept another request, rather than blocking on this one.
var html = await s_httpClient.GetStringAsync(URL);
return Regex.Matches(html, @"\.NET").Count;
}
Вы можете написать аналогичный код для универсального приложения Windows и выполнить задачу подсчета после нажатия кнопки:
private readonly HttpClient _httpClient = new HttpClient();
private async void OnSeeTheDotNetsButtonClick(object sender, RoutedEventArgs e)
{
// Capture the task handle here so we can await the background task later.
var getDotNetFoundationHtmlTask = _httpClient.GetStringAsync("https://dotnetfoundation.org");
// Any other work on the UI thread can be done here, such as enabling a Progress Bar.
// It's important to do the extra work here before the "await" call,
// so the user sees the progress bar before execution of this method is yielded.
NetworkProgressBar.IsEnabled = true;
NetworkProgressBar.Visibility = Visibility.Visible;
// The await operator suspends OnSeeTheDotNetsButtonClick(), returning control to its caller.
// This action is what allows the app to be responsive and not block the UI thread.
var html = await getDotNetFoundationHtmlTask;
int count = Regex.Matches(html, @"\.NET").Count;
DotNetCountLabel.Text = $"Number of .NETs on dotnetfoundation.org: {count}";
NetworkProgressBar.IsEnabled = false;
NetworkProgressBar.Visibility = Visibility.Collapsed;
}
Ожидание выполнения нескольких задач
В некоторых сценариях код должен одновременно извлекать несколько фрагментов данных. API-интерфейсы Task
предоставляют методы, которые позволяют вам писать асинхронный код для неблокирующего ожидания выполнения нескольких фоновых задач.
- метод Task.WhenAll
- метод Task.WhenAny
В следующем примере показано, как можно получить данные объекта User
для набора объектов userId
.
private static async Task<User> GetUserAsync(int userId)
{
// Code omitted:
//
// Given a user Id {userId}, retrieves a User object corresponding
// to the entry in the database with {userId} as its Id.
return await Task.FromResult(new User() { id = userId });
}
private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
var getUserTasks = new List<Task<User>>();
foreach (int userId in userIds)
{
getUserTasks.Add(GetUserAsync(userId));
}
return await Task.WhenAll(getUserTasks);
}
Этот код можно писать более кратко с помощью LINQ:
private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
return await Task.WhenAll(getUserTasks);
}
Хотя вы пишете меньше кода с помощью LINQ, следует соблюдать осторожность при сочетании LINQ с асинхронным кодом. LINQ использует отложенное (или ленивое) выполнение. Асинхронные вызовы не происходят немедленно, как они выполняются в цикле foreach
, если только не принудительно выполнить итерацию созданной последовательности с вызовом метода .ToList()
или .ToArray()
. В этом примере используется метод Enumerable.ToArray для выполнения запроса сразу и хранения результатов в массиве. Этот подход заставляет оператор id => GetUserAsync(id)
выполнить и инициировать задачу.
Ознакомьтесь с рекомендациями по асинхронным программированию
При асинхронном программировании следует учитывать несколько деталей, которые могут предотвратить непредвиденное поведение.
Используйте await внутри тела метода async()
При использовании модификатора async
необходимо включить в текст метода один или несколько выражений await
. Если компилятор не сталкивается с выражением await
, метод не сможет получить. Хотя компилятор создает предупреждение, код по-прежнему компилируется, а компилятор запускает метод. Компьютер состояния, созданный компилятором C# для асинхронного метода, не выполняет ничего, поэтому весь процесс является очень неэффективным.
Добавление суффикса Async в асинхронные имена методов
Соглашение о стиле .NET заключается в добавлении суффикса Async ко всем именам асинхронных методов. Этот подход помогает более легко различать синхронные и асинхронные методы. Некоторые методы, которые не вызываются явным образом в коде (например, обработчики событий или методы веб-контроллера), не обязательно применяются в этом сценарии. Так как эти элементы не вызываются явным образом в коде, использование явного именования не так важно.
Возвращает значение async void только из обработчиков событий
Обработчики событий должны объявлять типы возвращаемых значений как void
и не могут использовать или возвращать объекты Task
и Task<T>
, в отличие от других методов. При написании асинхронных обработчиков событий необходимо использовать модификатор async
в методе, возвращающем void
, для обработчиков. Другие реализации методов, возвращающих async void
, не соответствуют модели TAP и могут создавать трудности.
- Исключения, выбрасываемые в методе
async void
, не могут быть пойманы за пределами этого метода. -
async void
методы сложно тестировать - методы
async void
могут вызвать негативные побочные эффекты, если вызывающий не ожидает, что они будут асинхронными.
Будьте осторожны с асинхронными лямбда-выражениями в LINQ
При внедрении асинхронных лямбда-выражений в выражениях LINQ важно проявлять осторожность. Лямбда-выражения в LINQ используют отложенное выполнение, что означает, что код может выполняться в непредвиденное время. Введение блокирующих задач в этот сценарий может легко привести к взаимоблокировке, если код не написан правильно. Кроме того, вложенность асинхронного кода также может усложнить понимание исполнения этого кода. Асинхронные операции и LINQ являются мощными, но эти методы следует использовать вместе настолько тщательно и ясно, насколько это возможно.
Выполнение задач в неблокирующем режиме
Если вашей программе нужен результат задачи, напишите код, реализующий выражение await
в неблокирующем режиме. Блокировка текущего потока в качестве средства для синхронного ожидания завершения элемента Task
может привести к взаимоблокировкам и заблокированным потокам контекста. Этот подход программирования может требовать более сложной обработки ошибок. В следующей таблице приведены рекомендации по доступу к результатам задач без их блокировки.
Сценарий задачи | Текущий код | Замените "await" |
---|---|---|
получение результата фоновой задачи |
Task.Wait или Task.Result |
await |
Продолжить после выполнения любой задачи | Task.WaitAny |
await Task.WhenAny |
Продолжить, когда все задачи завершены | Task.WaitAll |
await Task.WhenAll |
Продолжить через некоторое время | Thread.Sleep |
await Task.Delay |
Рекомендуется использовать тип ValueTask
Когда асинхронный метод возвращает объект Task
, узкие места производительности могут возникать в определенных сценариях. Так как Task
является ссылочным типом, объект Task
выделяется из кучи. Если метод, объявленный с помощью модификатора async
, возвращает кэшированный результат или завершается синхронно, дополнительные выделения могут приводить к значительным временным затратам в критически важных секциях кода. Этот сценарий может стать дорогостоящим, когда выделение происходит в жестких циклах. Дополнительные сведения см. в разделе Обобщенные асинхронные типы возвращаемых значений.
Понимание того, когда следует устанавливать ConfigureAwait(false)
Разработчики часто спрашивают, когда следует использовать булевый Task.ConfigureAwait(Boolean). Этот API позволяет Task
экземпляру настроить контекст для конечного компьютера, реализующего любое выражение await
. Если булево значение не задано правильно, производительность может снизиться или могут произойти взаимоблокировки. Дополнительные сведения см. в разделе Вопросы и ответы о ConfigureAwait.
Пишите менее состоятельный код
Избегайте написания кода, зависящее от состояния глобальных объектов или выполнения определенных методов. Вместо этого, полагайтесь только на возвращаемые методами значения. Существует множество преимуществ написания кода, который является менее использующим состояние.
- Легче анализировать код
- Проще тестировать код
- Более простое сочетание асинхронного и синхронного кода
- Возможность избежать условий гонки в коде
- Легко координировать асинхронный код, зависящий от возвращаемых значений
- (Бонус) Хорошо совместимо с внедрением зависимостей в код.
Следует стремиться к достижению полной или почти полной ссылочной прозрачности в коде. Такой подход приводит к прогнозируемой, тестируемой и поддерживаемой базе кода.
Просмотрите полный пример
Следующий код представляет полный пример, который доступен в файле примера Program.cs.
using System.Text.RegularExpressions;
using System.Windows;
using Microsoft.AspNetCore.Mvc;
class Button
{
public Func<object, object, Task>? Clicked
{
get;
internal set;
}
}
class DamageResult
{
public int Damage
{
get { return 0; }
}
}
class User
{
public bool isEnabled
{
get;
set;
}
public int id
{
get;
set;
}
}
public class Program
{
private static readonly Button s_downloadButton = new();
private static readonly Button s_calculateButton = new();
private static readonly HttpClient s_httpClient = new();
private static readonly IEnumerable<string> s_urlList = new string[]
{
"https://learn.microsoft.com",
"https://learn.microsoft.com/aspnet/core",
"https://learn.microsoft.com/azure",
"https://learn.microsoft.com/azure/devops",
"https://learn.microsoft.com/dotnet",
"https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio",
"https://learn.microsoft.com/education",
"https://learn.microsoft.com/shows/net-core-101/what-is-net",
"https://learn.microsoft.com/enterprise-mobility-security",
"https://learn.microsoft.com/gaming",
"https://learn.microsoft.com/graph",
"https://learn.microsoft.com/microsoft-365",
"https://learn.microsoft.com/office",
"https://learn.microsoft.com/powershell",
"https://learn.microsoft.com/sql",
"https://learn.microsoft.com/surface",
"https://dotnetfoundation.org",
"https://learn.microsoft.com/visualstudio",
"https://learn.microsoft.com/windows",
"https://learn.microsoft.com/maui"
};
private static void Calculate()
{
// <PerformGameCalculation>
static DamageResult CalculateDamageDone()
{
return new DamageResult()
{
// Code omitted:
//
// Does an expensive calculation and returns
// the result of that calculation.
};
}
s_calculateButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI while CalculateDamageDone()
// performs its work. The UI thread is free to perform other work.
var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};
// </PerformGameCalculation>
}
private static void DisplayDamage(DamageResult damage)
{
Console.WriteLine(damage.Damage);
}
private static void Download(string URL)
{
// <UnblockingDownload>
s_downloadButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI as the request
// from the web service is happening.
//
// The UI thread is now free to perform other work.
var stringData = await s_httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};
// </UnblockingDownload>
}
private static void DoSomethingWithData(object stringData)
{
Console.WriteLine($"Displaying data: {stringData}");
}
// <GetUsersForDataset>
private static async Task<User> GetUserAsync(int userId)
{
// Code omitted:
//
// Given a user Id {userId}, retrieves a User object corresponding
// to the entry in the database with {userId} as its Id.
return await Task.FromResult(new User() { id = userId });
}
private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
var getUserTasks = new List<Task<User>>();
foreach (int userId in userIds)
{
getUserTasks.Add(GetUserAsync(userId));
}
return await Task.WhenAll(getUserTasks);
}
// </GetUsersForDataset>
// <GetUsersForDatasetByLINQ>
private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
return await Task.WhenAll(getUserTasks);
}
// </GetUsersForDatasetByLINQ>
// <ExtractDataFromNetwork>
[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCount(string URL)
{
// Suspends GetDotNetCount() to allow the caller (the web server)
// to accept another request, rather than blocking on this one.
var html = await s_httpClient.GetStringAsync(URL);
return Regex.Matches(html, @"\.NET").Count;
}
// </ExtractDataFromNetwork>
static async Task Main()
{
Console.WriteLine("Application started.");
Console.WriteLine("Counting '.NET' phrase in websites...");
int total = 0;
foreach (string url in s_urlList)
{
var result = await GetDotNetCount(url);
Console.WriteLine($"{url}: {result}");
total += result;
}
Console.WriteLine("Total: " + total);
Console.WriteLine("Retrieving User objects with list of IDs...");
IEnumerable<int> ids = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
var users = await GetUsersAsync(ids);
foreach (User? user in users)
{
Console.WriteLine($"{user.id}: isEnabled={user.isEnabled}");
}
Console.WriteLine("Application ending.");
}
}
// Example output:
//
// Application started.
// Counting '.NET' phrase in websites...
// https://learn.microsoft.com: 0
// https://learn.microsoft.com/aspnet/core: 57
// https://learn.microsoft.com/azure: 1
// https://learn.microsoft.com/azure/devops: 2
// https://learn.microsoft.com/dotnet: 83
// https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio: 31
// https://learn.microsoft.com/education: 0
// https://learn.microsoft.com/shows/net-core-101/what-is-net: 42
// https://learn.microsoft.com/enterprise-mobility-security: 0
// https://learn.microsoft.com/gaming: 0
// https://learn.microsoft.com/graph: 0
// https://learn.microsoft.com/microsoft-365: 0
// https://learn.microsoft.com/office: 0
// https://learn.microsoft.com/powershell: 0
// https://learn.microsoft.com/sql: 0
// https://learn.microsoft.com/surface: 0
// https://dotnetfoundation.org: 16
// https://learn.microsoft.com/visualstudio: 0
// https://learn.microsoft.com/windows: 0
// https://learn.microsoft.com/maui: 6
// Total: 238
// Retrieving User objects with list of IDs...
// 1: isEnabled= False
// 2: isEnabled= False
// 3: isEnabled= False
// 4: isEnabled= False
// 5: isEnabled= False
// 6: isEnabled= False
// 7: isEnabled= False
// 8: isEnabled= False
// 9: isEnabled= False
// 0: isEnabled= False
// Application ending.