Когда я буду использовать Task.Yield ()?

219

Я использую async / await и Taskмного, но никогда не использовал Task.Yield()и, честно говоря, даже со всеми объяснениями, я не понимаю, зачем мне этот метод.

Может кто-нибудь привести хороший пример, где Yield()это требуется?

Крумелер
источник

Ответы:

240

Когда вы используете async/ await, нет никакой гарантии, что метод, который вы вызываете, когда вы делаете, await FooAsync()будет работать асинхронно. Внутренняя реализация может свободно возвращаться, используя полностью синхронный путь.

Если вы создаете API, в котором важно, чтобы вы не блокировали и выполняли некоторый код асинхронно, и есть вероятность, что вызываемый метод будет работать синхронно (эффективно блокируя), использование await Task.Yield()заставит ваш метод быть асинхронным и вернуть контроль в этой точке. Остальная часть кода будет выполняться позже (в этот момент он все еще может выполняться синхронно) в текущем контексте.

Это также может быть полезно, если вы делаете асинхронный метод, который требует некоторой «длительной» инициализации, то есть:

 private async void button_Click(object sender, EventArgs e)
 {
      await Task.Yield(); // Make us async right away

      var data = ExecuteFooOnUIThread(); // This will run on the UI thread at some point later

      await UseDataAsync(data);
 }

Без Task.Yield()вызова метод будет выполняться синхронно вплоть до первого вызова await.

Рид Копси
источник
28
Я чувствую, что здесь что-то неправильно понимаю. Если await Task.Yield()метод заставляет метод быть асинхронным, зачем нам писать «настоящий» асинхронный код? Представьте себе тяжелый метод синхронизации. Чтобы сделать это асинхронным, просто добавьте asyncи await Task.Yield()в начале, и волшебным образом, это будет асинхронно? Это было бы похоже на обертывание всего кода синхронизации Task.Run()и создание ложного асинхронного метода.
Krumelur
14
@ Krumelur Есть большая разница - посмотрите на мой пример. Если вы используете Task.Runдля его реализации, ExecuteFooOnUIThreadбудет работать в пуле потоков, а не поток пользовательского интерфейса. С помощью await Task.Yield()этого вы заставляете его быть асинхронным таким образом, чтобы последующий код все еще выполнялся в текущем контексте (только в более поздний момент времени). Это не то, что вы обычно делаете, но приятно, что есть опция, если она требуется по какой-то странной причине.
Рид Копси
7
Еще один вопрос: если бы он ExecuteFooOnUIThread()работал очень долго, он в какой-то момент блокировал бы поток пользовательского интерфейса на долгое время и сделал бы пользовательский интерфейс невосприимчивым, это правильно?
Krumelur
7
@Krumelur Да, было бы. Только не сразу - это случится позже.
Рид Копси
34
Хотя этот ответ технически верен, утверждение, что «остальная часть кода будет выполнена позже», является слишком абстрактным и может вводить в заблуждение. График выполнения кода после Task.Yield () очень сильно зависит от конкретного SynchronisationContext. И в документации MSDN четко говорится, что «Контекст синхронизации, который присутствует в потоке пользовательского интерфейса в большинстве сред пользовательского интерфейса, часто будет отдавать приоритет работе, размещенной в контексте выше, чем работа ввода и рендеринга. По этой причине не полагайтесь на await Task.Yield () чтобы пользовательский интерфейс оставался отзывчивым. "
Виталий Цвайер
36

Внутренне await Task.Yield()просто помещает в очередь продолжение либо в текущем контексте синхронизации, либо в потоке случайного пула, если он SynchronizationContext.Currentесть null.

Он эффективно реализован как пользовательский ожидающий. Менее эффективный код, производящий идентичный эффект, может быть таким простым:

var tcs = new TaskCompletionSource<bool>();
var sc = SynchronizationContext.Current;
if (sc != null)
    sc.Post(_ => tcs.SetResult(true), null);
else
    ThreadPool.QueueUserWorkItem(_ => tcs.SetResult(true));
await tcs.Task;

Task.Yield()может использоваться как ярлык для некоторых странных изменений потока выполнения. Например:

async Task DoDialogAsync()
{
    var dialog = new Form();

    Func<Task> showAsync = async () => 
    {
        await Task.Yield();
        dialog.ShowDialog();
    }

    var dialogTask = showAsync();
    await Task.Yield();

    // now we're on the dialog's nested message loop started by dialog.ShowDialog 
    MessageBox.Show("The dialog is visible, click OK to close");
    dialog.Close();

    await dialogTask;
    // we're back to the main message loop  
}

Тем не менее, я не могу вспомнить ни одного случая, когда Task.Yield()не может быть заменен с Task.Factory.StartNew/ надлежащим планировщиком задач.

Смотрите также:

noseratio
источник
В вашем примере, в чем разница между тем, что там и var dialogTask = await showAsync();?
Эрик Филипс
@ErikPhilips, var dialogTask = await showAsync()не будет компилироваться, потому что await showAsync()выражение не возвращает a Task(в отличие от этого без await). Тем не менее, если вы это сделаете await showAsync(), выполнение после того, как оно будет возобновлено только после закрытия диалога, вот как это отличается. Это потому, что window.ShowDialogсинхронный API (несмотря на то, что он по-прежнему качает сообщения). В этом коде я хотел продолжить, пока диалог все еще отображается.
noseratio
5

Одним из применений Task.Yield()является предотвращение переполнения стека при выполнении асинхронной рекурсии. Task.Yield()предотвращает синхронное продолжение. Обратите внимание, однако, что это может вызвать исключение OutOfMemory (как отметил Triynko). Бесконечная рекурсия все еще небезопасна, и вам, вероятно, лучше переписать рекурсию как цикл.

private static void Main()
    {
        RecursiveMethod().Wait();
    }

    private static async Task RecursiveMethod()
    {
        await Task.Delay(1);
        //await Task.Yield(); // Uncomment this line to prevent stackoverlfow.
        await RecursiveMethod();
    }
Йоаким М.Х.
источник
4
Это может предотвратить переполнение стека, но в конечном итоге это исчерпает системную память, если вы позволите ему работать достаточно долго. Каждая итерация создаст новую задачу, которая никогда не завершится, поскольку внешняя задача ожидает внутреннюю задачу, которая ожидает еще одну внутреннюю задачу, и так далее. Это не хорошо. В качестве альтернативы, вы можете просто иметь одну внешнюю задачу, которая никогда не завершится, и просто сделать ее циклической, а не рекурсивной. Задача никогда не будет выполнена, но будет только один из них. Внутри цикла, он может выдавать или ждать что угодно.
Трийнко
Я не могу воспроизвести переполнение стека. Кажется, этого await Task.Delay(1)достаточно, чтобы предотвратить это. (Консольное приложение, .NET Core 3.1, C # 8)
Теодор Зулиас
-8

Task.Yield() может использоваться в имитационных реализациях асинхронных методов.

mhsirig
источник
4
Вы должны предоставить некоторые детали.
PJProudhon
3
Для этой цели я бы предпочел использовать Task.CompletedTask - см. Раздел Task.CompletedTask в этом сообщении блога msdn для получения дополнительной информации.
Гжегож Смулько
2
Проблема с использованием Task.CompletedTask или Task.FromResult заключается в том, что вы можете пропустить ошибки, которые появляются только тогда, когда метод выполняется асинхронно.
Иоаким МЗ