C# Language
Асинхронный-Await
Поиск…
Вступление
В C # метод, объявленный async
, не будет блокироваться в синхронном процессе, если вы используете операции на основе ввода-вывода (например, веб-доступ, работа с файлами, ...). Результат таких асинхронных меток может ожидаться с помощью ключевого слова await
.
замечания
Метод async
может возвращать void
, Task
или Task<T>
.
Тип возвращаемого Task
будет ждать завершения метода, и результат будет void
. Task<T>
вернет значение из типа T
после завершения метода.
методы async
должны возвращать Task
или Task<T>
, в отличие от void
, практически во всех случаях. async void
методы не могут быть await
, что приводит к множеству проблем. Единственный сценарий, в котором async
должен возвращать void
относится к обработчику событий.
async
/ await
работает, преобразуя ваш async
метод в async
автомат. Он делает это, создавая структуру за кулисами, которая хранит текущее состояние и любой контекст (например, локальные переменные) и предоставляет метод MoveNext()
для продвижения состояний (и запуска любого связанного кода) всякий раз, когда ожидается ожидаемое завершение.
Простые последовательные звонки
public async Task<JobResult> GetDataFromWebAsync()
{
var nextJob = await _database.GetNextJobAsync();
var response = await _httpClient.GetAsync(nextJob.Uri);
var pageContents = await response.Content.ReadAsStringAsync();
return await _database.SaveJobResultAsync(pageContents);
}
Главное , чтобы отметить здесь , что в то время как каждый await
-ED метод вызывается асинхронно - и за все время , что называют управления привели обратно в систему - поток внутри метода является линейным и не требует какого - либо специального лечения из - за асинхронность. Если какой-либо из методов, называемых fail, исключение будет обработано «как ожидалось», что в этом случае означает, что выполнение метода будет прервано, и исключение будет расти вверх по стеку.
Попробуйте / Поймать / Наконец
Начиная с C # 6.0 ключевое слово await
теперь можно использовать в блоке catch
и finally
.
try {
var client = new AsyncClient();
await client.DoSomething();
} catch (MyException ex) {
await client.LogExceptionAsync();
throw;
} finally {
await client.CloseAsync();
}
До C # 6.0 вам нужно сделать что-то в следующих строках: Обратите внимание, что 6.0 также очистил нулевые проверки с помощью оператора Null Propagating .
AsynClient client;
MyException caughtException;
try {
client = new AsyncClient();
await client.DoSomething();
} catch (MyException ex) {
caughtException = ex;
}
if (client != null) {
if (caughtException != null) {
await client.LogExceptionAsync();
}
await client.CloseAsync();
if (caughtException != null) throw caughtException;
}
Обратите внимание, что если вы ожидаете задачи, не созданные async
(например, задача, созданная Task.Run
), некоторые отладчики могут ломаться над исключениями, Task.Run
этой задачей, даже если она, по-видимому, обрабатывается окружением try / catch. Это происходит потому, что отладчик считает, что он не обрабатывается в отношении кода пользователя. В Visual Studio есть опция «Только мой код» , которую можно отключить, чтобы предотвратить отладчик в таких ситуациях.
Настройка Web.config для установки 4.5 для правильного асинхронного поведения.
Web.config system.web.httpRuntime должен быть нацелен на 4.5, чтобы гарантировать, что поток будет обрабатывать контекст запроса, прежде чем возобновлять ваш метод async.
<httpRuntime targetFramework="4.5" />
Async и ждут неопределенного поведения на ASP.NET до 4.5. Async / await возобновится на произвольном потоке, который может не иметь контекста запроса. Приложения под нагрузкой случайным образом терпят неудачу с нулевыми ссылочными исключениями, доступ к HttpContext после ожидания. Использование HttpContext.Current в WebApi опасно из-за async
Параллельные вызовы
Одновременно можно ожидать нескольких вызовов, сначала вызвав ожидаемые задачи, а затем ожидая их.
public async Task RunConcurrentTasks()
{
var firstTask = DoSomethingAsync();
var secondTask = DoSomethingElseAsync();
await firstTask;
await secondTask;
}
Кроме того, Task.WhenAll
можно использовать для группировки нескольких задач в одну Task
, которая завершается, когда все переданные задачи завершены.
public async Task RunConcurrentTasks()
{
var firstTask = DoSomethingAsync();
var secondTask = DoSomethingElseAsync();
await Task.WhenAll(firstTask, secondTask);
}
Вы также можете сделать это внутри цикла, например:
List<Task> tasks = new List<Task>();
while (something) {
// do stuff
Task someAsyncTask = someAsyncMethod();
tasks.Add(someAsyncTask);
}
await Task.WhenAll(tasks);
Чтобы получить результаты из задачи после ожидания нескольких задач с помощью Task.WhenAll, просто подождите снова. Поскольку задача уже завершена, она вернет результат обратно
var task1 = SomeOpAsync();
var task2 = SomeOtherOpAsync();
await Task.WhenAll(task1, task2);
var result = await task2;
Кроме того, Task.WhenAny
может использоваться для одновременного выполнения нескольких задач, таких как Task.WhenAll
выше, с той разницей, что этот метод будет завершен, когда будет завершена любая из поставленных задач.
public async Task RunConcurrentTasksWhenAny()
{
var firstTask = TaskOperation("#firstTask executed");
var secondTask = TaskOperation("#secondTask executed");
var thirdTask = TaskOperation("#thirdTask executed");
await Task.WhenAny(firstTask, secondTask, thirdTask);
}
Task
возвращаемая RunConcurrentTasksWhenAny
, завершится, когда завершатся все из firstTask
, secondTask
или thirdTask
.
Оператор ожидания и ключевое слово async
await
оператора и ключевое слово async
объединяются:
Асинхронный метод, в котором используется await, должен быть изменен с помощью ключевого слова async .
Противоположность не всегда верна: вы можете пометить метод как async
не используя await
в своем теле.
await
самом деле заключается в том, чтобы приостановить выполнение кода до тех пор, пока ожидаемая задача не завершится; любая задача может быть ожидаемой.
Примечание: вы не можете ждать метода async, который ничего не возвращает (void).
На самом деле слово «suspends» немного вводит в заблуждение, потому что не только выполнение останавливается, но поток может стать бесплатным для выполнения других операций. Под капотом await
реализуется с помощью магии компилятора: он разбивает метод на две части - до и после await
. Последняя часть выполняется, когда ожидаемая задача завершается.
Если мы проигнорируем некоторые важные детали, компилятор примерно сделает это за вас:
public async Task<TResult> DoIt()
{
// do something and acquire someTask of type Task<TSomeResult>
var awaitedResult = await someTask;
// ... do something more and produce result of type TResult
return result;
}
будет выглядеть так:
public Task<TResult> DoIt()
{
// ...
return someTask.ContinueWith(task => {
var result = ((Task<TSomeResult>)task).Result;
return DoIt_Continuation(result);
});
}
private TResult DoIt_Continuation(TSomeResult awaitedResult)
{
// ...
}
Любой обычный метод можно превратить в async следующим образом:
await Task.Run(() => YourSyncMethod());
Это может быть полезно, когда вам нужно выполнить длинный запуск в потоке пользовательского интерфейса без замораживания пользовательского интерфейса.
Но здесь есть очень важное замечание: асинхронный не всегда означает одновременный (параллельный или даже многопоточный). Даже в одном потоке async
await
до сих пор позволяет использовать асинхронный код. Например, см. Этот настраиваемый планировщик задач . Такой «сумасшедший» планировщик задач может просто превращать задачи в функции, которые вызывают в обработке цикла сообщений.
Нам нужно спросить себя: какой поток выполнит продолжение нашего метода DoIt_Continuation
?
По умолчанию оператор await
выполняет выполнение продолжения с текущим контекстом синхронизации . Это означает, что по умолчанию для продолжений WinForms и WPF в потоке пользовательского интерфейса. Если по какой-то причине вам нужно изменить это поведение, используйте метод Task.ConfigureAwait()
:
await Task.Run(() => YourSyncMethod()).ConfigureAwait(continueOnCapturedContext: false);
Возвращение задачи без ожидания
Методы , которые выполняют асинхронные операции не нужно использовать await
, если:
- Внутри метода есть только один асинхронный вызов
- Асинхронный вызов находится в конце метода
- Исключение исключения / обработки, которое может произойти в Задаче, не обязательно
Рассмотрим этот метод, который возвращает Task
:
public async Task<User> GetUserAsync(int id)
{
var lookupKey = "Users" + id;
return await dataStore.GetByKeyAsync(lookupKey);
}
Если GetByKeyAsync
имеет ту же подпись, что и GetUserAsync
(возвращает Task<User>
), метод может быть упрощен:
public Task<User> GetUserAsync(int id)
{
var lookupKey = "Users" + id;
return dataStore.GetByKeyAsync(lookupKey);
}
В этом случае метод не должен быть помечен как async
, даже если он выполняет предварительную асинхронную операцию. Задача, возвращаемая GetByKeyAsync
, передается непосредственно вызывающему методу, где он будет await
.
Важно : возвращая Task
вместо ожидания, изменяет поведение исключения для метода, поскольку оно не будет генерировать исключение внутри метода, который запускает задачу, но в ожидающем ее методе.
public Task SaveAsync()
{
try {
return dataStore.SaveChangesAsync();
}
catch(Exception ex)
{
// this will never be called
logger.LogException(ex);
}
}
// Some other code calling SaveAsync()
// If exception happens, it will be thrown here, not inside SaveAsync()
await SaveAsync();
Это улучшит производительность, так как это спасет компилятор от генерации дополнительного асинхронного конечного автомата.
Блокирование асинхронного кода может привести к блокировкам
Плохая практика блокировать асинхронные вызовы, так как это может вызвать взаимоблокировки в средах, которые имеют контекст синхронизации. Лучшей практикой является использование async / await «полностью вниз». Например, следующий код Windows Forms вызывает тупик:
private async Task<bool> TryThis()
{
Trace.TraceInformation("Starting TryThis");
await Task.Run(() =>
{
Trace.TraceInformation("In TryThis task");
for (int i = 0; i < 100; i++)
{
// This runs successfully - the loop runs to completion
Trace.TraceInformation("For loop " + i);
System.Threading.Thread.Sleep(10);
}
});
// This never happens due to the deadlock
Trace.TraceInformation("About to return");
return true;
}
// Button click event handler
private void button1_Click(object sender, EventArgs e)
{
// .Result causes this to block on the asynchronous call
bool result = TryThis().Result;
// Never actually gets here
Trace.TraceInformation("Done with result");
}
По сути, после завершения асинхронного вызова он ожидает, что контекст синхронизации станет доступным. Однако обработчик событий «держится» в контексте синхронизации, пока он ждет завершения TryThis()
, что вызывает циклическое ожидание.
Чтобы исправить это, код должен быть изменен на
private async void button1_Click(object sender, EventArgs e)
{
bool result = await TryThis();
Trace.TraceInformation("Done with result");
}
Примечание. Обработчики событий - это единственное место, где следует использовать async void
(потому что вы не можете ждать метода async void
).
Async / await будет только улучшать производительность, если он позволяет машине выполнять дополнительную работу
Рассмотрим следующий код:
public async Task MethodA()
{
await MethodB();
// Do other work
}
public async Task MethodB()
{
await MethodC();
// Do other work
}
public async Task MethodC()
{
// Or await some other async work
await Task.Delay(100);
}
Это не будет лучше, чем
public void MethodA()
{
MethodB();
// Do other work
}
public void MethodB()
{
MethodC();
// Do other work
}
public void MethodC()
{
Thread.Sleep(100);
}
Основная цель async / await - позволить машине выполнять дополнительную работу - например, разрешить вызывающему потоку выполнять другую работу, пока он ждет результата от какой-либо операции ввода-вывода. В этом случае вызывающему потоку никогда не разрешается выполнять больше работы, чем это было бы в противном случае, так что нет никакого увеличения производительности при простом вызове MethodA()
, MethodB()
и MethodC()
синхронно.