Отсутствие не захвата Task.Yield заставляет меня использовать Task.Run, зачем следовать этому?
Извините заранее, если этот вопрос основан на мнениях. Отсутствие Task.Yield версии, которая не захватила бы контекст выполнения, уже обсуждалась здесь. По-видимому, эта функция присутствовала в некоторой форме в ранних версиях Async CTP, но была удалена, потому что ее можно было легко использовать.
IMO, такая функция может быть так же легко использована как Task.Run
. Вот что я имею в виду. Представьте себе ожидаемый API SwitchContext.Yield
, который планирует продолжение в ThreadPool, поэтому выполнение всегда будет продолжаться в потоке, отличном от вызывающего потока. Я мог бы использовать его в следующем коде, который запускает некоторую работу, связанную с ЦП, из потока пользовательского интерфейса. Я бы счел это удобным способом продолжения работы, связанной с процессором, в потоке пула:
class Worker
{
static void Log(string format, params object[] args)
{
Debug.WriteLine("{0}: {1}", Thread.CurrentThread.ManagedThreadId, String.Format(format, args));
}
public async Task UIAction()
{
// UI Thread
Log("UIAction");
// start the CPU-bound work
var cts = new CancellationTokenSource(5000);
var workTask = DoWorkAsync(cts.Token);
// possibly await for some IO-bound work
await Task.Delay(1000);
Log("after Task.Delay");
// finally, get the result of the CPU-bound work
int c = await workTask;
Log("Result: {0}", c);
}
async Task<int> DoWorkAsync(CancellationToken ct)
{
// start on the UI thread
Log("DoWorkAsync");
// switch to a pool thread and yield back to the UI thread
await SwitchContext.Yield();
Log("after SwitchContext.Yield");
// continue on a pool thread
int c = 0;
while (!ct.IsCancellationRequested)
{
// do some CPU-bound work on a pool thread: counting cycles :)
c++;
// and use async/await too
await Task.Delay(50);
}
return c;
}
}
Теперь, без SwitchContext.Yield
, DoWorkAsync
будет выглядеть ниже. Он добавляет некоторый дополнительный уровень сложности в виде делегата async и вложенности задачи:
async Task<int> DoWorkAsync(CancellationToken ct)
{
// start on the UI thread
Log("DoWorkAsync");
// Have to use async delegate
// Task.Run uwraps the inner Task<int> task
return await Task.Run(async () =>
{
// continue on a pool thread
Log("after Task.Yield");
int c = 0;
while (!ct.IsCancellationRequested)
{
// do some CPU-bound work on a pool thread: counting cycles :)
c++;
// and use async/await too
await Task.Delay(50);
}
return c;
});
}
Тем не менее, реализация SwitchContext.Yield
может быть довольно простой и (смею сказать) эффективной:
public static class SwitchContext
{
public static Awaiter Yield() { return new Awaiter(); }
public struct Awaiter : System.Runtime.CompilerServices.INotifyCompletion
{
public Awaiter GetAwaiter() { return this; }
public bool IsCompleted { get { return false; } }
public void OnCompleted(Action continuation)
{
ThreadPool.QueueUserWorkItem((state) => ((Action)state)(), continuation);
}
public void GetResult() { }
}
}
Итак, мой вопрос, почему я должен предпочесть вторую версию DoWorkAsync
по сравнению с первой, и почему использование SwitchContext.Yield
считается плохой практикой?
Ответы
Ответ 1
Вам не нужно вставлять Task.Run
в DoWorkAsync
. Рассмотрим эту опцию:
public async Task UIAction()
{
// UI Thread
Log("UIAction");
// start the CPU-bound work
var cts = new CancellationTokenSource(5000);
var workTask = Task.Run(() => DoWorkAsync(cts.Token));
// possibly await for some IO-bound work
await Task.Delay(1000);
Log("after Task.Delay");
// finally, get the result of the CPU-bound work
int c = await workTask;
Log("Result: {0}", c);
}
Это приводит к коду с гораздо более ясными намерениями. DoWorkAsync
является естественным синхронным методом поэтому он имеет синхронную подпись. DoWorkAsync
не знает и не заботится о пользовательском интерфейсе. UIAction
, который заботится о потоке пользовательского интерфейса, отталкивает работу на фоновый поток, используя Task.Run
.
Как правило, попробуйте "нажимать" любые Task.Run
вызовы из ваших методов библиотеки как можно больше.