Коли я використовую Task.Yield ()?


218

Я використовую async / wait і Taskбагато, але ніколи не користувався Task.Yield()і, якщо чесно, навіть при всіх поясненнях, я не розумію, для чого мені потрібен цей метод.

Чи може хтось дати хороший приклад, де Yield()це потрібно?

Відповіді:


241

Коли ви використовуєте 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.


26
Я відчуваю, що тут щось неправильно трактую. Якщо await Task.Yield()метод змушує бути асинхронним, то чому б ми не турбувались писати "справжній" код асинхронізації? Уявіть важкий метод синхронізації. Щоб зробити це асинхронним, просто додати asyncі await Task.Yield()на початку , і чарівно, це буде асинхронним? Це дуже схоже на Task.Run()введення коду синхронізації та створення підробленого методу асинхронізації.
Крумелур

14
@Krumelur Є велика різниця - подивіться на мій приклад. Якщо ви використовуєте a Task.Runдля його реалізації, він ExecuteFooOnUIThreadбуде працювати в пулі потоків, а не в потоці UI. З await Task.Yield(), ви змушуєте його бути асинхронним таким чином, що наступний код все ще запускається в поточному контексті (саме в більш пізній момент часу). Як правило, це не те, що ти робиш, але приємно, що є варіант, якщо це потрібно з якоїсь дивної причини.
Рід Копсі

7
Ще одне питання: якщо ExecuteFooOnUIThread()дуже довго працювати, воно все-таки на деякий час заблокувало б потік інтерфейсу та зробило інтерфейс безвідповідальним, чи правильно?
Крумельюр

7
@Krumelur Так. Тільки не відразу - це станеться згодом.
Рід Копсі

33
Хоча ця відповідь технічно правильна, твердження про те, що "решта коду буде виконано пізніше" є надто абстрактним і може бути оманливим. Графік виконання коду після Task.Yield () дуже залежить від конкретного SynchronisationContext. І в документації MSDN чітко зазначено, що "Контекст синхронізації, який присутній у потоці користувальницького інтерфейсу в більшості середовищ інтерфейсу, часто надає пріоритет роботі, розміщеній у контексті, вищому, ніж робота вводу та візуалізації. З цієї причини не покладайтеся на очікування 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.StartNeww / належним планувальником завдань.

Дивитися також:


У вашому прикладі, яка різниця між тим, що там, і var dialogTask = await showAsync();?
Ерік Філіпс

@ErikPhilips, var dialogTask = await showAsync()не буде компілюватися, оскільки await showAsync()вираз не повертає a Task(на відміну від нього без await). Однак, якщо це зробити await showAsync(), виконання після його відновлення буде відновлено лише після закриття діалогового вікна. Це тому window.ShowDialog, що це синхронний API (незважаючи на те, що він все ще перекачує повідомлення). У цьому коді я хотів продовжити, поки діалогове вікно ще відображається.
носраціо

5

Одне використання Task.Yield()- запобігти переповненню стека під час асинхронної рекурсії. Task.Yield()перешкоджає синхронному продовженню. Однак зауважте, що це може спричинити виняток OutOfMemory (як зазначив Трайнко). Нескінченна рекурсія все ще не є безпечною, і вам, ймовірно, краще переписати рекурсію у цикл.

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() може використовуватися в макетних реалізаціях методів асинхронізації.


4
Ви повинні надати деякі деталі.
PJProudhon

3
З цією метою я б скоріше скористався Task.CompletedTask - див. Розділ Task.CompletedTask в цій публікації щоденника msdn для отримання додаткових міркувань.
Гжегож Смолько

2
Проблема з використанням Task.CompletedTask або Task.FromResult полягає в тому, що ви можете пропустити помилки, які з’являються лише тоді, коли метод виконує асинхронний характер.
Йоаким MH
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.