Task.Yield - реальные обычаи?

Я читал о Task.Yield. И как разработчик Javascript я могу сказать, что это задание точно то же самое, что и setTimeout(function (){...},0); с точки зрения разрешения основного отдельного потока на другие материал aka:

"не принимают всю власть, освобождаются от времени - так что другие будут тоже есть..."

В js он работает особенно в длинных циклах. (не заставляйте браузер замораживать...)

Но я увидел этот пример здесь:

public static async Task < int > FindSeriesSum(int i1)
{
    int sum = 0;
    for (int i = 0; i < i1; i++)
    {
        sum += i;
        if (i % 1000 == 0) ( after a bulk , release power to main thread)
            await Task.Yield();
    }

    return sum;
}

Как программист JS я могу понять, что они здесь сделали.

НО, как программист на С#, я спрашиваю себя: почему бы не открыть для него задачу?

 public static async Task < int > FindSeriesSum(int i1)
    {
         //do something....
         return await MyLongCalculationTask();
         //do something
    }

Вопрос

С помощью Js я не могу открыть задачу (да, я знаю, что могу на самом деле с веб-работниками). Но с С# я могу.

Если So - зачем даже время от времени отпускать, когда я могу вообще его отпустить?

Изменить

Добавление ссылок:

От здесь: enter image description here

Из здесь (еще одна книга):

enter image description here

Ответ 1

Когда вы видите:

await Task.Yield();

вы можете думать об этом так:

await Task.Factory.StartNew( 
    () => {}, 
    CancellationToken.None, 
    TaskCreationOptions.None, 
    SynchronizationContext.Current != null?
        TaskScheduler.FromCurrentSynchronizationContext(): 
        TaskScheduler.Current);

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

Когда точно и в каком потоке это произойдет, все зависит от контекста синхронизации потока вызывающего.

Для потока пользовательского интерфейса продолжение будет происходить после некоторой будущей итерации цикла сообщений, выполняемой Application.Run (WinForms) или Dispatcher.Run (WPF). Внутренне это сводится к API Win32 PostMessage, который отправляет пользовательское сообщение в очередь сообщений потока пользовательского интерфейса. Обратный вызов продолжения await вызывается, когда это сообщение накачивается и обрабатывается. Вы полностью не контролируете, когда именно это произойдет.

Кроме того, у Windows есть свои приоритеты для перекачки сообщений: INFO: Приоритеты Message Window. Наиболее важная часть:

В соответствии с этой схемой приоритезация может считаться трехуровневой. Все отправленные сообщения имеют более высокий приоритет, чем сообщения ввода пользователя, потому что они находятся в разных очередях. И все сообщения ввода пользователя более высокий приоритет, чем сообщения WM_PAINT и WM_TIMER.

Итак, если вы используете await Task.Yield(), чтобы перейти к циклу сообщений, пытаясь сохранить отзывчивость пользовательского интерфейса, вы рискуете помешать циклу сообщений потока пользовательского интерфейса. Некоторые ожидающие сообщения ввода пользователя, а также WM_PAINT и WM_TIMER имеют более низкий приоритет, чем отправленное сообщение продолжения. Таким образом, если вы выполняете await Task.Yield() в узком цикле, вы все равно можете заблокировать пользовательский интерфейс.

Вот как это отличается от аналогии JavaScript setTimer, которую вы упомянули в вопросе. Обратный вызов setTimer будет вызван после того, как все сообщения пользовательского ввода будут обработаны насосом сообщений браузера.

Итак, await Task.Yield() не подходит для выполнения фоновой работы с потоком пользовательского интерфейса. На самом деле вам очень редко нужно запускать фоновый процесс в потоке пользовательского интерфейса, но иногда вы это делаете, например. подсветка синтаксиса редактора, проверка орфографии и т.д. В этом случае используйте инфраструктуру бездействующего фрейма.

Например, с помощью WPF вы можете сделать await Dispatcher.Yield(DispatcherPriority.ApplicationIdle):

async Task DoUIThreadWorkAsync(CancellationToken token)
{
    var i = 0;

    while (true)
    {
        token.ThrowIfCancellationRequested();

        await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);

        // do the UI-related work item
        this.TextBlock.Text = "iteration " + i++;
    }
}

Для WinForms вы можете использовать событие Application.Idle:

// await IdleYield();

public static Task IdleYield()
{
    var idleTcs = new TaskCompletionSource<bool>();
    // subscribe to Application.Idle
    EventHandler handler = null;
    handler = (s, e) =>
    {
        Application.Idle -= handler;
        idleTcs.SetResult(true);
    };
    Application.Idle += handler;
    return idleTcs.Task;
}

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

Для потока без UI без контекста синхронизации await Task.Yield() просто переключает продолжение на случайный поток пула. Нет никакой гарантии, что это будет другой поток из текущего потока, он гарантированно будет асинхронным продолжением. Если ThreadPool голодает, он может планировать продолжение в тот же поток.

В ASP.NET выполнение await Task.Yield() не имеет смысла вообще, за исключением обходного пути, упомянутого в @StephenCleary answer, В противном случае это повредит производительности веб-приложения с помощью избыточного переключателя потоков.

Так что, await Task.Yield() полезно? IMO, не так много. Его можно использовать как ярлык для продолжения продолжения через SynchronizationContext.Post или ThreadPool.QueueUserWorkItem, если вам действительно нужно наложить асинхронность на часть вашего метода.

Что касается книг, которые вы цитировали, на мой взгляд, эти подходы к использованию Task.Yield неверны. Я объяснил, почему они ошибаются в потоке пользовательского интерфейса, выше. Для потока пула, отличного от UI, просто нет "других задач в потоке для выполнения", если только вы не запускаете настраиваемый насос задач, например Stephen Toub AsyncPump.

Обновлено для ответа на комментарий:

... как это может быть операция асинхронной работы и оставаться в одном потоке?..

В качестве простого примера: приложение WinForms:

async void Form_Load(object s, object e) 
{ 
    await Task.Yield(); 
    MessageBox.Show("Async message!");
}

Form_Load вернется к вызывающему абоненту (код оболочки WinFroms, который запустил событие Load), а затем окно сообщения будет отображаться асинхронно, после некоторой будущей итерации цикла сообщения, выполняемой Application.Run(). Обратный вызов продолжения помещается в очередь с помощью WinFormsSynchronizationContext.Post, который внутренне помещает личное сообщение Windows в цикл сообщений потока пользовательского интерфейса. Обратный вызов будет выполняться, когда это сообщение накачивается, все еще в том же потоке.

В консольном приложении вы можете запустить аналогичный цикл сериализации с AsyncPump, упомянутым выше.

Ответ 3

Нет, это не совсем так, как с помощью setTimeout, чтобы вернуть управление пользовательскому интерфейсу. В Javascript, который всегда позволял бы обновлять пользовательский интерфейс, поскольку setTimeout всегда имеет минимальную паузу в несколько миллисекунд, а ожидающая работу пользовательского интерфейса имеет приоритет над таймерами, но await Task.Yield(); этого не делает.

Нет гарантии, что выход даст любую работу в основном потоке, напротив, код, называемый доходностью, будет часто приоритетным по сравнению с работой пользовательского интерфейса.

"Контекст синхронизации, присутствующий в потоке пользовательского интерфейса в большинстве пользовательских интерфейсов средах часто уделяется приоритетное внимание работе, выложенной в контекст выше чем ввод и рендеринг. По этой причине не полагайтесь на ожидание Task.Yield(); чтобы пользовательский интерфейс был отзывчивым".

Ссылка: MSDN: метод Task.Yield

Ответ 4

Прежде всего, позвольте мне уточнить: Yield не совсем то же самое, что setTimeout(function (){...},0);. JS выполняется в среде с одним потоком, так что это единственный способ разрешить другие действия. Вид совместная многозадачность..net выполняется в среде с упреждающей многозадачностью с явным многопотоком.

Теперь вернемся к Thread.Yield. Как я сказал .net живет в упреждающем мире, но это немного сложнее, чем это. С# await/async создают интересную смесь из режима многозадачности, управляемого государственными машинами. Поэтому, если вы опустите Yield из своего кода, он просто заблокирует поток и его. Если вы делаете это обычной задачей и просто вызываете start (или поток), тогда он будет просто делать это параллельно и позже блокировать вызов потока при вызове task.Result. Что происходит, когда вы делаете await Task.Yield();, более сложно. Логически он разблокирует вызывающий код (аналогичный JS), и выполнение продолжается. Что он на самом деле делает - он выбирает другой поток и продолжает выполнение в нем в превентивной среде с вызывающим потоком. Таким образом, он вызывает вызов до первого Task.Yield, а затем он сам по себе. Последующие вызовы Task.Yield, по-видимому, ничего не делают.

Простая демонстрация:

class MainClass
{
    //Just to reduce amont of log itmes
    static HashSet<Tuple<string, int>> cache = new HashSet<Tuple<string, int>>();
    public static void LogThread(string msg, bool clear=false) {
        if (clear)
            cache.Clear ();
        var val = Tuple.Create(msg, Thread.CurrentThread.ManagedThreadId);
        if (cache.Add (val))
            Console.WriteLine ("{0}\t:{1}", val.Item1, val.Item2);
    }

    public static async Task<int> FindSeriesSum(int i1)
    {
        LogThread ("Task enter");
        int sum = 0;
        for (int i = 0; i < i1; i++)
        {
            sum += i;
            if (i % 1000 == 0) {
                LogThread ("Before yield");
                await Task.Yield ();
                LogThread ("After yield");
            }
        }
        LogThread ("Task done");
        return sum;
    }

    public static void Main (string[] args)
    {
        LogThread ("Before task");
        var task = FindSeriesSum(1000000);
        LogThread ("While task", true);
        Console.WriteLine ("Sum = {0}", task.Result);
        LogThread ("After task");
    }
}

Вот результаты:

Before task     :1
Task enter      :1
Before yield    :1
After yield     :5
Before yield    :5
While task      :1
Before yield    :5
After yield     :5
Task done       :5
Sum = 1783293664
After task      :1
  • Результат, полученный на моно 4.5 в Mac OS X, результаты могут отличаться для других настроек

Если вы переместите Task.Yield поверх метода, он будет с помощью async с самого начала и не будет блокировать вызывающий поток.

Заключение: Task.Yield может сделать возможным синхронизацию и асинхронный код. Несколько более или менее реалистичный сценарий: у вас есть тяжелая вычислительная операция и локальный кэш и задача CalcThing. В этом методе вы проверяете, находится ли элемент в кеше, если да - возвращать элемент, если он не существует Yield и приступить к его вычислению. На самом деле выборка из вашей книги довольно бессмысленна, потому что ничего полезного здесь не достигается. Их замечание относительно интерактивности GUI просто плохое и неправильное (поток пользовательского интерфейса будет заблокирован до первого вызова Yield, вы никогда не должны этого делать, MSDN ясно (и правильно): "не полагайтесь на Task.Yield(), чтобы поддерживать пользовательский интерфейс".

Ответ 5

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

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

Ответ 6

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

Пример:

  CancellationTokenSource cts;
  void Start()
  {
        cts = new CancellationTokenSource();

        // run async operation
        var task = Task.Run(() => SomeWork(cts.Token), cts.Token);
        // wait for completion
        // after the completion handle the result/ cancellation/ errors
    }

    async Task<int> SomeWork(CancellationToken cancellationToken)
    {
        int result = 0;

        bool loopAgain = true;
        while (loopAgain)
        {
            // do something ... means a substantial work or a micro batch here - not processing a single byte

            loopAgain = /* check for loop end && */  cancellationToken.IsCancellationRequested;
            if (loopAgain) {
                // reschedule  the task to the threadpool and free this thread for other waiting tasks
                await Task.Yield();
            }
        }
        cancellationToken.ThrowIfCancellationRequested();
        return result;
    }

    void Cancel()
    {
        // request cancelation
        cts.Cancel();
    }