Асинхронное программированиеAsynchronous programming

Для решения задач, связанных с вводом-выводом (например, запрос данных из сети, доступ к базе данных или чтение и запись в файловой системе), желательно использовать асинхронное программирование.If you have any I/O-bound needs (such as requesting data from a network, accessing a database, or reading and writing to a file system), you'll want to utilize asynchronous programming. Если у вас есть код, ограниченный ресурсами процессора, например выполняющий сложные вычисления, то это также подходящий сценарий для асинхронного программирования.You could also have CPU-bound code, such as performing an expensive calculation, which is also a good scenario for writing async code.

В C# есть модель асинхронного программирования, реализованная на уровне языка, которая позволяет легко писать асинхронный код, не прибегая к обратным вызовам или библиотекам, которые поддерживают асинхронность.C# has a language-level asynchronous programming model, which allows for easily writing asynchronous code without having to juggle callbacks or conform to a library that supports asynchrony. Она строится на принципах асинхронной модели на основе задач (TAP).It follows what is known as the Task-based Asynchronous Pattern (TAP).

Обзор асинхронной моделиOverview of the asynchronous model

В основе асинхронного программирования лежат объекты Task и Task<T>, которые моделируют асинхронные операции.The core of async programming is the Task and Task<T> objects, which model asynchronous operations. Они поддерживаются ключевыми словами async и await.They are supported by the async and await keywords. В большинстве случаев модель достаточно проста.The model is fairly simple in most cases:

  • В коде, ограниченном производительностью ввода-вывода, выполняйте await для операции, которая возвращает Task или Task<T>, внутри метода async.For I/O-bound code, you await an operation that returns a Task or Task<T> inside of an async method.
  • В коде, ограниченном ресурсами процессора, выполняйте await для операции, которая запускается в фоновом потоке методом Task.Run.For CPU-bound code, you await an operation that is started on a background thread with the Task.Run method.

Именно с помощью ключевого слова await творится вся магия.The await keyword is where the magic happens. Оно передает управление вызывающему объекту метода, который выполнил await, позволяя, таким образом, пользовательскому интерфейсу или службе отвечать на запросы.It yields control to the caller of the method that performed await, and it ultimately allows a UI to be responsive or a service to be elastic. Хотя существуют и другие способы реализации асинхронного кода, кроме async и await, в этой статье рассматриваются только конструкции уровня языка.While there are ways to approach async code other than async and await, this article focuses on the language-level constructs.

Пример кода, ограниченного производительностью ввода-вывода: скачивание данных из веб-службыI/O-bound example: Download data from a web service

Предположим, вам нужно скачать некоторые данные из веб-службы по нажатию кнопки, не блокируя поток пользовательского интерфейса.You may need to download some data from a web service when a button is pressed but don't want to block the UI thread. Это можно сделать так:It can be accomplished like this:

private readonly HttpClient _httpClient = new HttpClient();

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 _httpClient.GetStringAsync(URL);
    DoSomethingWithData(stringData);
};

В этом коде намерение (скачивание данных в асинхронном режиме) выражается без запутанных операций с объектами Task.The code expresses the intent (downloading data asynchronously) without getting bogged down in interacting with Task objects.

Пример кода, ограниченного ресурсами процессора: выполнение вычислений для игрыCPU-bound example: Perform a calculation for a game

Предположим, вы разрабатываете игру для мобильных устройств, в которой при нажатии кнопки может наноситься урон множеству противников на экране.Say you're writing a mobile game where pressing a button can inflict damage on many enemies on the screen. Расчет урона может потреблять много ресурсов. Если производить его в потоке пользовательского интерфейса, то на это время игра может приостанавливаться!Performing the damage calculation can be expensive, and doing it on the UI thread would make the game appear to pause as the calculation is performed!

Оптимальный способ — запустить фоновый поток, который выполняет задачу с помощью Task.Run, а затем ожидать ее результат с помощью await.The best way to handle this is to start a background thread, which does the work using Task.Run, and await its result using await. Это обеспечит плавность работы пользовательского интерфейса в процессе вычисления.This allows the UI to feel smooth as the work is being done.

private DamageResult CalculateDamageDone()
{
    // Code omitted:
    //
    // Does an expensive calculation and returns
    // the result of that calculation.
}

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);
};

В этом коде четко выражается назначение события нажатия кнопки, фоновым потоком не требуется управлять вручную, и он выполняется без блокировки.This code clearly expresses the intent of the button's click event, it doesn't require managing a background thread manually, and it does so in a non-blocking way.

Что происходит на внутреннем уровнеWhat happens under the covers

Выполнение асинхронных операций связано со множеством моментов.There are many moving pieces where asynchronous operations are concerned. Если вы хотите знать внутренние принципы работы объектов Task и Task<T>, прочтите статью Подробный обзор асинхронного программирования.If you're curious about what's happening underneath the covers of Task and Task<T>, see the Async in-depth article for more information.

С точки зрения C#, компилятор преобразовывает код в конечный автомат, который контролирует такие моменты, как передача выполнения при достижении await и возобновление выполнения после завершения фонового задания.On the C# side of things, the compiler transforms your code into a state machine that keeps track of things like yielding execution when an await is reached and resuming execution when a background job has finished.

Если вас интересует теория, это реализация модели асинхронности на основе обещаний.For the theoretically inclined, this is an implementation of the Promise Model of asynchrony.

Ключевые моменты для пониманияKey pieces to understand

  • Асинхронный код можно использовать как при ограниченной производительности ввода-вывода, так и при ограниченных ресурсах процессора, но по-разному в каждом случае.Async code can be used for both I/O-bound and CPU-bound code, but differently for each scenario.
  • В асинхронном коде используются конструкции Task<T> и Task, которые служат для моделирования задач, выполняемых в фоновом режиме.Async code uses Task<T> and Task, which are constructs used to model work being done in the background.
  • Ключевое слово async делает метод асинхронным, что позволяет использовать в его теле ключевое слово await.The async keyword turns a method into an async method, which allows you to use the await keyword in its body.
  • Когда применяется ключевое слово await, оно приостанавливает выполнение вызывающего метода и передает управление обратно вызывающему объекту, пока не будет завершена ожидаемая задача.When the await keyword is applied, it suspends the calling method and yields control back to its caller until the awaited task is complete.
  • await можно использовать только внутри асинхронного метода.await can only be used inside an async method.

Различия задач, ограниченных ресурсами процессора и производительностью ввода-выводаRecognize CPU-bound and I/O-bound work

В первых двух примерах этого руководства было показано, как можно использовать async и await для выполнения задач, ограниченных производительностью ввода-вывода и ресурсами процессора.The first two examples of this guide showed how you could use async and await for I/O-bound and CPU-bound work. Крайне важно уметь идентифицировать такие задачи, так как они могут существенно повлиять на производительность кода и привести к неправильному использованию некоторых конструкций.It's key that you can identify when a job you need to do is I/O-bound or CPU-bound because it can greatly affect the performance of your code and could potentially lead to misusing certain constructs.

Перед написанием любого кода нужно ответить на два вопроса.Here are two questions you should ask before you write any code:

  1. Будет ли код "ожидать" чего-либо, например данных из базы данных?Will your code be "waiting" for something, such as data from a database?

    Если ответ утвердительный, то ваша задача ограничена производительностью ввода-вывода.If your answer is "yes", then your work is I/O-bound.

  2. Будет ли код выполнять сложные вычисления?Will your code be performing an expensive computation?

    Если ответ утвердительный, то задача ограничена ресурсами процессора.If you answered "yes", then your work is CPU-bound.

Если ваша задача ограничена производительностью ввода-вывода, используйте async и await без Task.Run.If the work you have is I/O-bound, use async and await without Task.Run. Библиотеку параллельных задач использовать не следует.You should not use the Task Parallel Library. Причина этого указана в статье Подробный обзор асинхронного программирования.The reason for this is outlined in Async in Depth.

Если ваша задача ограничена ресурсами процессора и вам важна скорость реагирования, используйте async и await, но перенесите выполнение задачи в дополнительный поток, у которого есть Task.Run.If the work you have is CPU-bound and you care about responsiveness, use async and await, but spawn off the work on another thread with Task.Run. Если к задаче применим параллелизм, рассмотрите возможность использования библиотеки параллельных задач.If the work is appropriate for concurrency and parallelism, also consider using the Task Parallel Library.

Кроме того, всегда следует оценивать выполнение кода.Additionally, you should always measure the execution of your code. Например, затраты на выполнение задачи, ограниченной ресурсами процессора, могут оказаться не столь высокими, как накладные расходы, связанные с переключениями контекста при многопоточности.For example, you may find yourself in a situation where your CPU-bound work is not costly enough compared with the overhead of context switches when multithreading. Каждый вариант имеет свои недостатки, поэтому следует выбрать наиболее компромиссный вариант в вашей ситуации.Every choice has its tradeoff, and you should pick the correct tradeoff for your situation.

Дополнительные примерыMore examples

В приведенных ниже примерах демонстрируются различные способы написания асинхронного кода на C#.The following examples demonstrate various ways you can write async code in C#. Они охватывают несколько сценариев, с которыми вы можете столкнуться.They cover a few different scenarios you may come across.

Извлечение данных из сетиExtract data from a network

Этот фрагмент кода скачивает код HTML с главной страницы сайта https://dotnetfoundation.org и подсчитывает в нем число вхождений строки ".NET".This snippet downloads the HTML from the homepage at https://dotnetfoundation.org and counts the number of times the string ".NET" occurs in the HTML. С помощью ASP.NET он определяет метод контроллера веб-API, который выполняет эту задачу и возвращает число.It uses ASP.NET to define a Web API controller method, which performs this task and returns the number.

Примечание

Если вы планируете проанализировать HTML в рабочем коде, не используйте регулярные выражения.If you plan on doing HTML parsing in production code, don't use regular expressions. Используйте библиотеку анализа.Use a parsing library instead.

private readonly HttpClient _httpClient = new HttpClient();

[HttpGet, Route("DotNetCount")]
public async Task<int> GetDotNetCount()
{
    // Suspends GetDotNetCount() to allow the caller (the web server)
    // to accept another request, rather than blocking on this one.
    var html = await _httpClient.GetStringAsync("https://dotnetfoundation.org");

    return Regex.Matches(html, @"\.NET").Count;
}

Вот аналогичный код для универсального приложения для Windows, который выполняет ту же задачу при нажатии кнопки:Here's the same scenario written for a Universal Windows App, which performs the same task when a Button is pressed:

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.
    // This is important to do here, before the "await" call, so that 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 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;
}

Ожидание выполнения нескольких задачWait for multiple tasks to complete

Может возникнуть ситуация, когда несколько фрагментов данных должны извлекаться одновременно.You may find yourself in a situation where you need to retrieve multiple pieces of data concurrently. API-интерфейс Task содержит два метода, Task.WhenAll и Task.WhenAny, которые позволяют писать асинхронный код, выполняющий неблокирующее ожидание нескольких фоновых заданий.The Task API contains two methods, Task.WhenAll and Task.WhenAny, that allow you to write asynchronous code that performs a non-blocking wait on multiple background jobs.

В этом примере показано, как можно получить данные User для набора userId.This example shows how you might grab User data for a set of userIds.

public 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.
}

public 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:Here's another way to write this more succinctly, using LINQ:

public 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.
}

public static async Task<User[]> GetUsersAsync(IEnumerable<int> userIds)
{
    var getUserTasks = userIds.Select(id => GetUserAsync(id));
    return await Task.WhenAll(getUserTasks);
}

Хотя размер кода будет меньше, соблюдайте осторожность при использовании LINQ в сочетании с асинхронным кодом.Although it's less code, use caution when mixing LINQ with asynchronous code. Так как в LINQ используется отложенное выполнение, асинхронные вызовы будут выполняться не немедленно, как в цикле foreach, если только вы не производите принудительную итерацию созданной последовательности с помощью вызова .ToList() или .ToArray().Because LINQ uses deferred (lazy) execution, async calls won't happen immediately as they do in a foreach loop unless you force the generated sequence to iterate with a call to .ToList() or .ToArray().

Важные сведения и советыImportant info and advice

При создании асинхронного кода необходимо учитывать ряд моментов, которые позволят избежать непредвиденного поведения.With async programming, there are some details to keep in mind that can prevent unexpected behavior.

  • В методах async должно присутствовать ключевое слово await . В противном случае результат не будет получен.async methods need to have an await keyword in their body or they will never yield!

    Это важно помнить.This is important to keep in mind. Если в теле метода async не используется ключевое слово await, компилятор C# выдаст предупреждение, но код скомпилируется и будет выполняться, как обычный метод.If await is not used in the body of an async method, the C# compiler generates a warning, but the code compiles and runs as if it were a normal method. Это крайне неэффективно, так как созданный компилятором C# конечный автомат для асинхронного метода не будет выполнять никакой работы.This is incredibly inefficient, as the state machine generated by the C# compiler for the async method is not accomplishing anything.

  • К имени каждого создаваемого асинхронного метода следует добавлять суффикс Async.Add "Async" as the suffix of every async method name you write.

    Это соглашение применяется в .NET для удобной дифференциации синхронных и асинхронных методов.This is the convention used in .NET to more easily differentiate synchronous and asynchronous methods. Это не всегда применимо к некоторым методам, которые не вызываются в коде явным образом (например, к обработчикам событий или методам веб-контроллеров).Certain methods that aren't explicitly called by your code (such as event handlers or web controller methods) don't necessarily apply. Так как они не вызываются в коде явно, требования к их именованию не так строги.Because they are not explicitly called by your code, being explicit about their naming isn't as important.

  • async void следует использовать только для обработчиков событий.async void should only be used for event handlers.

    async void — это единственный способ обеспечить работу асинхронных обработчиков событий, так как у событий нет типов возвращаемых значений (поэтому они не могут использовать Task и Task<T>).async void is the only way to allow asynchronous event handlers to work because events do not have return types (thus cannot make use of Task and Task<T>). Любые иные способы применения async void не предусмотрены моделью TAP и могут создавать указанные ниже проблемы.Any other use of async void does not follow the TAP model and can be challenging to use, such as:

    • Исключения, вызываемые в методе async void, невозможно перехватывать вне этого метода.Exceptions thrown in an async void method can't be caught outside of that method.
    • Методы async void очень трудно тестировать.async void methods are difficult to test.
    • Методы async void могут иметь негативные побочные эффекты, если вызывающий объект не ожидает, что они будут асинхронными.async void methods can cause bad side effects if the caller isn't expecting them to be async.
  • Будьте осторожны при использовании асинхронных лямбда-выражений в выражениях LINQTread carefully when using async lambdas in LINQ expressions

    Для лямбда-выражений в LINQ применяется отложенное выполнение. Это означает, что код может выполняться в произвольный момент, когда вы этого не ожидаете.Lambda expressions in LINQ use deferred execution, meaning code could end up executing at a time when you're not expecting it to. Неправильное использование блокирующих задач при этом может привести к взаимоблокировке.The introduction of blocking tasks into this can easily result in a deadlock if not written correctly. Кроме того, вложение такого асинхронного кода может усложнить анализ выполнения кода.Additionally, the nesting of asynchronous code like this can also make it more difficult to reason about the execution of the code. Асинхронное выполнение и LINQ — эффективные средства, но использовать их следует с максимальной осторожностью и ясным пониманием того, что вы делаете.Async and LINQ are powerful but should be used together as carefully and clearly as possible.

  • При написании кода ожидание задач следует реализовывать без блокированияWrite code that awaits Tasks in a non-blocking manner

    Блокирование текущего потока для ожидания завершения Task может привести к взаимоблокировкам и блокированию потоков контекста, что потребует более сложной обработки ошибок.Blocking the current thread as a means to wait for a Task to complete can result in deadlocks and blocked context threads and can require more complex error-handling. В приведенной ниже таблице даются рекомендации по реализации ожидания задач без блокировки.The following table provides guidance on how to deal with waiting for tasks in a non-blocking way:

    Рекомендуемый способUse this... Нерекомендуемый способInstead of this... ЗадачаWhen wishing to do this...
    await Task.Wait или Task.ResultTask.Wait or Task.Result Получение результата фоновой задачиRetrieving the result of a background task
    await Task.WhenAny Task.WaitAny Ожидание завершения выполнения любой задачиWaiting for any task to complete
    await Task.WhenAll Task.WaitAll Ожидание завершения выполнения всех задачWaiting for all tasks to complete
    await Task.Delay Thread.Sleep Ожидание в течение заданного времениWaiting for a period of time
  • Рекомендуется использовать ValueTask во всех возможных случаяхConsider using ValueTask where possible

    В некоторых случаях возврат объекта Task из асинхронных методов может вызывать сложности.Returning a Task object from async methods can introduce performance bottlenecks in certain paths. Task — это тип ссылки, поэтому его применение означает распределение объекта.Task is a reference type, so using it means allocating an object. В случаях, когда метод с модификатором async возвращает кэшированный результат или завершается синхронно, лишние распределения могут вызывать серьезные потери времени при выполнении фрагментов кода, зависящих от производительности.In cases where a method declared with the async modifier returns a cached result or completes synchronously, the extra allocations can become a significant time cost in performance critical sections of code. Эта проблема встает серьезно, если распределения происходят в коротких циклах.It can become costly if those allocations occur in tight loops. Дополнительные сведения см. в разделе Обобщенные асинхронные типы возвращаемых значений.For more information, see generalized async return types.

  • Рекомендуется использовать ConfigureAwait(false)Consider using ConfigureAwait(false)

    Часто возникает вопрос: "Когда же нужно использовать метод Task.ConfigureAwait(Boolean)?"A common question is, "when should I use the Task.ConfigureAwait(Boolean) method?". Этот метод позволяет экземпляру Task настроить ожидающий объект.The method allows for a Task instance to configure its awaiter. Это важный элемент, неправильная настройка которого может привести к снижению производительности и даже к взаимоблокировкам.This is an important consideration and setting it incorrectly could potentially have performance implications and even deadlocks. Дополнительные сведения о ConfigureAwait см. в статье с вопросами и ответами по ConfigureAwait.For more information on ConfigureAwait, see the ConfigureAwait FAQ.

  • Пишите код с менее строгим отслеживанием состоянияWrite less stateful code

    Старайтесь, чтобы выполнение кода не зависело от состояния глобальных объектов или выполнения определенных методов.Don't depend on the state of global objects or the execution of certain methods. Оно должно зависеть только от возвращаемых методами значений.Instead, depend only on the return values of methods. Почему?Why?

    • Код будет проще анализировать.Code will be easier to reason about.
    • Код будет проще тестировать.Code will be easier to test.
    • Гораздо проще будет сочетать асинхронный и синхронный код.Mixing async and synchronous code is far simpler.
    • Как правило, можно полностью избежать состояний гонки.Race conditions can typically be avoided altogether.
    • Зависимость от возвращаемых значений упрощает согласование асинхронного кода.Depending on return values makes coordinating async code simple.
    • Дополнительным преимуществом является то, что такой код хорошо работает с внедрением зависимостей.(Bonus) it works really well with dependency injection.

Следует стремиться к достижению полной или почти полной ссылочной прозрачности в коде.A recommended goal is to achieve complete or near-complete Referential Transparency in your code. Результатом будет предсказуемость базы кода, а также ее пригодность для тестирования и обслуживания.Doing so will result in a predictable, testable, and maintainable codebase.

Другие источникиOther resources