it-roy-ru.com

Когда лучше всего использовать Task.Result вместо ожидания Task

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

var user = await _userRepo.GetByUsername(User.Identity.Name);

//Some minor work that doesn't rely on the user object

user = await _userRepo.UpdateLastAccessed(user, DateTime.Now);

return user;

Сейчас становится: 

var userTask = _userRepo.GetByUsername(User.Identity.Name);

//Some work that doesn't rely on the user object

user = await _userRepo.UpdateLastAccessed(userTask.Result, DateTime.Now);

return user;

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

6
George Harnwell

Давайте сделаем все возможное, чтобы не хоронить лиду здесь:

Так, например: [некоторый правильный код] становится [некоторый неправильный код]

НИКОГДА, НИКОГДА, НЕ ДЕЛАЙТЕ ЭТОГО.

Ваш инстинкт того, что вы можете реструктурировать поток управления для повышения производительности, превосходен и корректен. Использование Result для этого НЕПРАВИЛЬНО НЕПРАВИЛЬНО.

Правильный способ переписать ваш код

var userTask = _userRepo.GetByUsername(User.Identity.Name);    
//Some work that doesn't rely on the user object    
user = await _userRepo.UpdateLastAccessed(await userTask, DateTime.Now);    
return user;

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

Люди, кажется, думают, что await имеет семантику совместного вызова; Это не. Скорее, await - это операция extract над заданием comonad; это оператор в tasks, а не call expression. Обычно вы видите это при вызовах методов просто потому, что это обычный шаблон для абстрагирования асинхронной операции как метода. Возвращаемое задание - это то, что ожидается, а не вызов .

Тем не менее, из того, что я видел в публикации, подразумевается, что результат следует использовать редко, и предпочтение отдается ожиданию, но я не понимаю, почему я хотел бы дождаться получения моего пользовательского объекта, если я могу выполнять какую-то другую независимую логику в в то же время?

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

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

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

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

Теперь предположим, что Foo вместо этого извлекает Result из task1. Что просходит? Foo синхронно ожидает завершения task1, ожидая, когда текущий поток станет доступным, чего не происходит, потому что мы находимся в синхронном ожидании. Вызов Result приводит к взаимоблокировке потока с самой, если задача каким-то образом аффинитизируется текущему потоку . Теперь вы можете создавать взаимоблокировки без блокировок и только одного потока! Не делай этого.

21
Eric Lippert

В вашем случае вы можете использовать:

user = await _userRepo.UpdateLastAccessed(await userTask, DateTime.Now);

или, может быть, более четко:

var user = await _userRepo.GetByUsername(User.Identity.Name);
//Some work that doesn't rely on the user object
user = await _userRepo.UpdateLastAccessed(user, DateTime.Now);

только время, когда вы должны прикоснуться к .Result, это когда вы знаете, что задача выполнена. Это может быть полезно в некоторых сценариях, когда вы пытаетесь избежать создания конечного автомата async и думаете, что есть хороший шанс, что задача была выполнена синхронно (возможно, с использованием локальной функции для случая async), или если вы используете обратные вызовы вместо async/await, и вы внутри обратного вызова .

В качестве примера избегания конечного автомата:

ValueTask<int> FetchAndProcess(SomeArgs args) {
    async ValueTask<int> Awaited(ValueTask<int> task) => SomeOtherProcessing(await task);
    var task = GetAsyncData(args);
    if (!task.IsCompletedSuccessfully) return Awaited(task);
    return new ValueTask<int>(SomeOtherProcessing(task.Result));
}

Дело в том, что если GetAsyncData возвращает синхронно завершенный результат, мы полностью избегаем всех механизмов async.

4
Marc Gravell

Async await не означает, что ваш код будет выполняться в нескольких потоках.

Однако это сократит время, в течение которого ваш поток будет бездействовать, пока процессы не завершатся, и завершится раньше. 

Всякий раз, когда поток, как правило, должен был бы ждать в ожидании чего-то для завершения, например, ждать загрузки веб-страницы, завершения запроса к базе данных, завершения записи на диск, поток async-await не будет ждать в режиме ожидания, пока данные не будут записаны./fetched, но оглядывается, может ли он вместо этого делать другие вещи, и возвращается позже после завершения ожидаемой задачи.

Это было описано с помощью аналогии с поварами в это интервью с Эриком Липпертом . Поиск где-то посередине для асинхронного ожидания.

Эрик Липперт сравнивает асинхронное ожидание с одним (!) Поваром, который должен приготовить завтрак. После того, как он начнет поджаривать хлеб, он мог ждать до тех пор, пока хлеб не будет поджарен, прежде чем ставить чайник на чай, ждать, пока вода закипит, прежде чем положить чайные листья в чайник и т.д.

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

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

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

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

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

Может случиться так, что другой поток продолжит обрабатывать операторы, которые идут после ожидания (вы можете увидеть это в отладчике, проверив идентификатор потока). Однако этот другой поток имеет context исходного потока, поэтому он может действовать так, как если бы он был исходным потоком. Нет необходимости в мьютексах, семафорах, IsInvokeRequired (в winforms) и т.д. Для вас кажется, что существует один поток.

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

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

Вы можете нанять дополнительного повара, используя Task.Run

async Task<TimeSpan> CalculateSunSet()
{
    // start fetching sunset data. however don't wait for the result yet
    // you've got better things to do:
    Task<SunsetData> taskFetchData = FetchSunsetData();

    // because you are not awaiting your thread will do the following:
    Location location = FetchLocation();

    // now you need the sunset data, start awaiting for the Task:
    SunsetData sunsetData = await taskFetchData;

    // some big calculations are needed, that take 33 seconds,
    // you want to keep your caller responsive, so start a Task
    // this Task will be run by a different thread:
    ask<DateTime> taskBigCalculations = Taks.Run( () => BigCalculations(sunsetData, location);

    // again no await: you are still free to do other things
    ...
    // before returning you need the result of the big calculations.
    // wait until big calculations are finished, keep caller responsive:
    DateTime result = await taskBigCalculations;
    return result;
}
3
Harald Coppoolse

Вы рассматривали эту версию?

var userTask = _userRepo.GetByUsername(User.Identity.Name);

//Some work that doesn't rely on the user object

user = await _userRepo.UpdateLastAccessed(await userTask, DateTime.Now);

return user;

Это выполнит «работу», пока пользователь извлекается, но у него также есть все преимущества await, которые описаны в Ожидать выполненную задачу, такую ​​же, как задача. Результат?


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

var userTask = _userRepo.GetByUsername(User.Identity.Name);

//Some work that doesn't rely on the user object

user = await userTask;
user = await _userRepo.UpdateLastAccessed(user, DateTime.Now);

return user;
0
NineBerry