2013-12-02 5 views
21

Если мне нужно отложить выполнение кода до тех пор, после того, как будущая итерации цикла UI поток сообщений, я мог бы сделать так что-то вроде этого:«ждет Task.Yield()» и его альтернативы

await Task.Factory.StartNew(
    () => { 
     MessageBox.Show("Hello!"); 
    }, 
    CancellationToken.None, 
    TaskCreationOptions.None, 
    TaskScheduler.FromCurrentSynchronizationContext()); 

Это было быть похожим на await Task.Yield(); MessageBox.Show("Hello!");, кроме того, у меня будет возможность отменить задачу, если захочу.

В случае контекста синхронизации по умолчанию я мог бы использовать await Task.Run для продолжения потока пула.

На самом деле мне нравятся Task.Factory.StartNew и Task.Run более Task.Yield, поскольку они оба явно определяют область действия кода продолжения.

Итак, в каких ситуациях await Task.Yield() на самом деле полезен?

+3

Я использовал только 'Task.Yield' в модульных тестов и [ для работы над неясной проблемой ASP.NET, где метод 'async' * не должен * завершаться синхронно] (http://stackoverflow.com/q/16653308/263693). –

+2

Связанный: [Task.Yield - реальные обычаи?] (Http://stackoverflow.com/q/23431595/1768303) – Noseratio

+0

Я понимаю, что ваш вопрос не задавал этого вопроса, но это отчасти связано. Вызов 'MessageBox.Show()' без передачи аргумента '' IWin32Window owner' (https://msdn.microsoft.com/en-us/library/cked7698%28v=vs.110%29.aspx) может привести к в случае, если этот код выполняется, когда ваше окно не имеет фокуса, появляется сообщение «popping under» вашего окна.Это особенно смущает, если сделать это в графическом интерфейсе. Кроме того, если вы передаете 'IWin32Window' в' MessageBox.Show() ', вам нужно сделать это в потоке пользовательского интерфейса. Итак, в этом случае вы ** не должны ** использовать 'Task.Run()' и ** должны ** передавать 'TaskScheduler' в' StartNew() '. – binki

ответ

0

Task.Yield не является альтернативой Task.Factory.StartNew или Task.Run. Они совершенно разные. Когда вы awaitTask.Yield, вы разрешаете выполнять другой код в текущем потоке без блокировки потока. Думайте об этом, как ожидая Task.Delay, за исключением Task.Yield ждет завершения задач, а не ждет определенного времени.

Примечание: Не используйте Task.Yield в потоке пользовательского интерфейса и предполагайте, что пользовательский интерфейс всегда будет реагировать. Это не всегда так.

+7

Я считаю, что у меня есть хорошее представление о том, как работают «Task.Yield» и другие пользовательские ожидания. В этом свете ** я в основном не согласен со всем, кроме вашего последнего предложения. ** – Noseratio

+0

«кроме« Task.Yield »ждет, пока задачи не будут завершены, а не ждут определенного времени». - он не ждет другого задачи для завершения. Он просто планирует «готовое к запуску» продолжение и переключается на следующее, существующее «готовое к запуску» продолжение, которое ждет в очереди (что может быть само собой). I.e., это просто заставляет вашу задачу перестать быть синхронной в этот момент. – binki

3

Task.Yield() отлично подходит для «пробивания отверстия» в другой синхронной части метода async.

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

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

using System; 
using System.Threading; 
using System.Threading.Tasks; 

namespace TaskYieldExample 
{ 
    class Program 
    { 
     private static CancellationTokenSource CancellationTokenSource; 

     static void Main(string[] args) 
     { 
      SelfCancellingAsync(); 
      SelfCancellingAsync(); 
      SelfCancellingAsync(); 

      Console.ReadLine(); 
     } 

     private static async void SelfCancellingAsync() 
     { 
      Console.WriteLine("SelfCancellingAsync starting."); 

      var cts = new CancellationTokenSource(); 
      var oldCts = Interlocked.Exchange(ref CancellationTokenSource, cts); 

      if (oldCts != null) 
      { 
       oldCts.Cancel(); 
      } 

      // Allow quick cancellation. 
      await Task.Yield(); 

      if (cts.IsCancellationRequested) 
      { 
       return; 
      } 

      // Do the "meaty" work. 
      Console.WriteLine("Performing intensive work."); 

      var answer = await Task 
       .Delay(TimeSpan.FromSeconds(1)) 
       .ContinueWith(_ => 42, TaskContinuationOptions.ExecuteSynchronously); 

      if (cts.IsCancellationRequested) 
      { 
       return; 
      } 

      // Do something with the result. 
      Console.WriteLine("SelfCancellingAsync completed. Answer: {0}.", answer); 
     } 
    } 
} 

Цель здесь, чтобы код, который выполняется синхронно на том же SynchronizationContext сразу после, не ждали вызова возврата метода асинхронной (когда она попадает его первый await), чтобы изменить состояние, которое влияет на выполнение асинхронного метода. Это дросселирует так же, как и результат, достигнутый Task.Delay (я говорю об ненулевом периоде задержки здесь), но без фактический, потенциально заметная задержка, которая может быть нежелательной в некоторых ситуациях.

+0

Спасибо за ваши мысли, хотя я делаю то же самое без 'Task.Yield' или helper' Task.Delay', [здесь моя версия] (http://stackoverflow.com/a/20320896/1768303) и-restart. – Noseratio

+0

Кстати, у вас могут возникнуть проблемы с обработкой исключений из вашей «мясистой» работы из-за 'async void' подписи' SelfCancellingAsync'. – Noseratio

+1

Я специально * не * обрабатываю любые исключения ради краткости - это задача того, кто хочет использовать шаблон. Пример должен был быть «void», потому что его «Задача» предполагает, что его следует ожидать, чего здесь нет. Именно поэтому я возвращаюсь из метода, когда обнаруживается отмена, а не исключение (как правило, это предпочтительный способ обмена сообщениями об аннулировании вызывающего абонента). –

6

Рассмотрите случай, когда вы хотите, чтобы ваша задача async возвращала значение.

Существующий синхронный метод:

public int DoSomething() 
{ 
    return SomeMethodThatReturnsAnInt(); 
} 

Чтобы асинхр, добавьте ключевое слово асинхронной и изменить тип возвращаемого значения:

public async Task<int> DoSomething() 

Чтобы использовать Task.Factory.StartNew(), изменить тело в одну строку метода к:

// start new task 
var task = Task<int>.Factory.StartNew(
    () => { 
     return SomeMethodThatReturnsAnInt(); 
    }, 
    CancellationToken.None, 
    TaskCreationOptions.None, 
    TaskScheduler.FromCurrentSynchronizationContext()); 

// await task, return control to calling method 
await task; 

// return task result 
return task.Result; 

VS. добавив одну строку, если вы используете await Task.Yield()

// this returns control to the calling method 
await Task.Yield(); 

// otherwise synchronous method scheduled for async execution by the 
// TaskScheduler of the calling thread 
return SomeMethodThatReturnsAnInt(); 

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

+1

Чтобы быть справедливым, это еще одна строка: 'return waitait Task.Factory.StartNew (() => SomeMethodThatReturnsAnInt(), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());'. Тем не менее, я вижу смысл, +1. Проблема с этим - несколько неинтуитивное изменение потока управления, обсуждаемое здесь (http://stackoverflow.com/q/18779393/1768303). – Noseratio

+0

ну, вам также понадобится 'await task;' строка – Moho

+1

Повторяя это, если мне нужно превратить 'SomeMethodThatReturnsAnInt' в' async', я мог бы просто сделать: 'public Task DoSomething() {return Task.FromResult (SomeMethodThatReturnsAnInt (SomeMethodThatReturnsAnInt))); } '. Или, для ['async' семантики распространения исключений] (http://stackoverflow.com/a/22395161/1768303):' public async Задача DoSomething() {return waitait.FromResult (SomeMethodThatReturnsAnInt()); } '. Ясно, что 'wait Task.Yield()' будет избыточным и нежелательным здесь. Извините за отмену моего голосования. – Noseratio

1

Одна из ситуаций, когда Task.Yield() на самом деле полезна, когда вы находитесь await recursively-called synchronously-completed Tasks. Потому что csharp's async/await“releases Zalgo”, выполняя непрерывные операции синхронно, когда это возможно, стек в сценарии полностью синхронной рекурсии может стать достаточно большим, чтобы ваш процесс погиб. Я думаю, что это также частично связано с тем, что хвостовые звонки не могут быть поддержаны из-за Task косвенности. await Task.Yield() планирует продолжение, которое будет выполняться планировщиком, а не встроенным, что позволяет избежать роста в стеке, и эта проблема будет работать.

Кроме того, Task.Yield() может быть использован для сокращения синхронной части метода. Если вызывающий абонент должен получить ваш метод Task до того, как ваш метод выполнит какое-либо действие, вы можете использовать Task.Yield() для принудительного возврата Task раньше, чем это могло бы произойти в противном случае. Например, в следующем локальном сценарии метод, метод async может получить ссылку на свой собственный Task безопасно (если вы работаете в этом на одном-параллельности SynchronizationContext, например, в WinForms или через nito’s AsyncContext.Run()):

using Nito.AsyncEx; 
using System; 
using System.Threading.Tasks; 

class Program 
{ 
    // Use a single-threaded SynchronizationContext similar to winforms/WPF 
    static void Main(string[] args) => AsyncContext.Run(() => RunAsync()); 

    static async Task RunAsync() 
    { 
     Task<Task> task = null; 
     task = getOwnTaskAsync(); 
     var foundTask = await task; 
     Console.WriteLine($"{task?.Id} == {foundTask?.Id}: {task == foundTask}"); 

     async Task<Task> getOwnTaskAsync() 
     { 
      // Cause this method to return and let the 「task」 local be assigned. 
      await Task.Yield(); 
      return task; 
     } 
    } 
} 

выход:

3 == 3: True 

Я сожалею, что я не могу придумать никаких реальных сценариев, где возможность принудительно оборвалась синхронную часть в async метод является лучшим способом, чтобы сделать что-то. Знать, что вы можете сделать трюк, как я только что показал, может быть полезным иногда, но он также более опасен. Часто вы можете передавать данные лучше, более читабельным и более потокобезопасным способом. Например, вы можете передать локальный метод ссылку на свой собственный Task используя TaskCompletionSource вместо:

using System; 
using System.Threading.Tasks; 

class Program 
{ 
    // Fully free-threaded! Works in more environments! 
    static void Main(string[] args) => RunAsync().Wait(); 

    static async Task RunAsync() 
    { 
     var ownTaskSource = new TaskCompletionSource<Task>(); 
     var task = getOwnTaskAsync(ownTaskSource.Task); 
     ownTaskSource.SetResult(task); 
     var foundTask = await task; 
     Console.WriteLine($"{task?.Id} == {foundTask?.Id}: {task == foundTask}"); 

     async Task<Task> getOwnTaskAsync(
      Task<Task> ownTaskTask) 
     { 
      // This might be clearer. 
      return await ownTaskTask; 
     } 
    } 
} 

выход:

2 == 2: True 
+0

Рекурсия ретрансляции для них действительно хорошо подходит, хотя ее стоимость зависит от конкретного контекста синхронизации. – Noseratio

+0

@noseratio True. Тот факт, что у него есть накладные расходы, можно немного смягчить, пропустив счетчик и используя его только каждые 30 рекурсий или около того. Однако я не уверен, что «SynchronizationContext» должен был бы с ним поработать. Если вы делаете что-то вроде этого, надеюсь, что это не в потоке GUI, если в графическом приложении.Но если вы повторяетесь с API-интерфейсами задач, вы, возможно, не знаете наверняка, что Задачи, которые вы ожидаете, не являются полными, поэтому, возможно, это необходимо сделать. Вы всегда можете сделать это условным для задачи «Task.IsCompleted» и счетчика. – binki

Смежные вопросы