Что такое Task.Yield()?

1,00
р.
Я не понимаю что это, как работает и в каких случаях используется. Может кто-нибудь по-русски объяснить?

Ответ
Этот метод возвращает специальное значение, предназначенное для передачи оператору await, и в отрыве от этого оператора не имеющее смысла.
Конструкция же await Task.Yield() делает довольно простую вещь — прерывает текущий метод и сразу же планирует его продолжение в текущем контексте синхронизации.
Используется же эта конструкция для разных целей.
Во-первых, эта конструкция может быть использована для немедленного возврата управления вызывающему коду. Например, при вызове из обработчика события событие будет считаться обработанным:
protected override async void OnClosing(CancelEventArgs e) { e.Cancel = true await Task.Yield() // (какая-то логика) }
Во-вторых, эта конструкция используется для очистки синхронного контекста вызова. Например, так можно "закрыть" текущую транзакцию (ambient transaction):
using (var ts = new TransactionScope()) { // ... Foo() // ... ts.Complete() }
async void Foo() { // ... тут мы находимся в контексте транзакции if (Transaction.Current != null) await Task.Yield() // ... а тут его уже нет! }
В-третьих, эта конструкция может очистить стек вызовов. Это может быть полезным, если программа падает с переполнением стека при обработке кучи вложенных продолжений.
Например, рассмотрим упрощенную реализацию AsyncLock:
class AsyncLock { private Task unlockedTask = Task.CompletedTask
public async Task Lock() { var tcs = new TaskCompletionSource()
await Interlocked.Exchange(ref unlockedTask, tcs.Task)
return () => tcs.SetResult(null) } }
Здесь поступающие запросы на получение блокировки выстраиваются в неявную очередь на продолжениях. Казалось бы, что может пойти не так?
private static async Task Foo() { var _lock = new AsyncLock() var unlock = await _lock.Lock()
for (var i = 0 i < 100000 i++) Bar(_lock)
unlock() }
private static async void Bar(AsyncLock _lock) { var unlock = await _lock.Lock() // do something sync unlock() }
Здесь продолжение метода Bar вызывается в тот момент, когда другой метод Bar выполняет вызов unlock(). Получается косвенная рекурсия между методом Bar и делегатом unlock, которая быстро сжирает стек и ведет к его переполнению.
Добавление же вызова Task.Yield() перенесет исполнение в "чистый" фрейм стека, и ошибка исчезнет:
class AsyncLock { private Task unlockedTask = Task.CompletedTask
public async Task Lock() { var tcs = new TaskCompletionSource()
var prevTask = Interlocked.Exchange(ref unlockedTask, tcs.Task)
if (!prevTask.IsCompleted) { await prevTask await Task.Yield() }
return () => tcs.SetResult(null) } }
Кстати, альтернативный способ починить код выше — использование флага RunContinuationsAsynchronously:
class AsyncLock { private Task unlockedTask = Task.CompletedTask
public async Task Lock() { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously)
await Interlocked.Exchange(ref unlockedTask, tcs.Task)
return () => tcs.SetResult(null) } }
В-четвертых, при использовании в UI-потоке эта конструкция позволяет обработать накопившиеся события ввода-вывода, что полезно при длительных обновлениях интерфейса.
Например, при добавлении миллиона строк в таблицу программа не будет реагировать на действия пользователя, пока все строки не будут добавлены. Но если, к примеру, после добавления каждой тысячи строк вставлять вызов await Task.Yield() - программа сможет обрабатывать действия пользователя и не будет выглядеть зависшей.
В WinForms для тех же целей можно было использовать метод Application.DoEvents() - но его избыточное использование приводило к переполнению стека. await Task.Yield() - это универсальный способ, который можно использовать как в WinForms, так и в WPF.