什么时候使用Task.Yield()?

我经常使用async/await和Task,但从未使用过Task.Yield (),老实说,即使有所有的解释,我也不明白为什么我需要这个方法。

谁能给出一个需要Yield()的好例子吗?

97562 次浏览

当你使用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为止。

在内部,如果SynchronizationContext.Currentnullawait Task.Yield()只是在当前同步上下文或随机池线程上对延续进行排队。

它是有效地实现作为自定义的一个等待器。产生相同效果的低效率代码可能像这样简单:

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与适当的任务调度程序替换。

参见:

Task.Yield()可以在异步方法的模拟实现中使用。

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();
}

Task.Yield()类似于async-await中的Thread.Yield(),但具有更具体的条件。你需要多少次Thread.Yield()?我将回答标题&;什么时候你会使用__abc0 &;广泛的第一位。当你满足以下条件时,你就可以:

  • 希望将控件返回到异步上下文(建议任务调度器先执行队列中的其他任务)
  • 需要在异步上下文中继续
  • 最好在任务调度程序空闲时立即继续
  • 不想被取消
  • 更喜欢短代码

术语“异步上下文”;这里的意思是“先SynchronizationContext,然后__abc1”。它被斯蒂芬·克利里使用。

Task.Yield()近似于做(许多帖子在这里和那里都略有错误):

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

如果其中任何一个条件被打破,您需要使用其他替代方案。

如果任务的继续应该在Task.DefaultScheduler中,通常使用ConfigureAwait(false)。相反,Task.Yield()给你一个没有ConfigureAwait(bool)的可等待对象。你需要使用TaskScheduler.Default的近似代码。

如果Task.Yield()阻塞了队列,你需要重新构造你的代码,正如noseratio解释的那样。

如果你需要延续发生在更晚的时间,比如以毫秒为单位,你可以使用Task.Delay

如果您希望在队列中取消任务,但不想检查取消令牌,也不想自己抛出异常,则需要使用带有取消令牌的近似代码。

Task.Yield()是如此小众,很容易被规避。结合我的经验,我只有一个假想的例子。它是解决由自定义调度器约束的异步进餐哲学家问题。在我的多线程助手库InSync系统中,它支持异步锁的无序获取。如果当前异步获取失败,它将对异步获取进行排队。代码是在这里。它需要ConfigureAwait(false)作为通用库,所以我需要使用Task.Factory.StartNew。在一个闭源项目中,我的程序需要执行与异步代码混合的重要同步代码

  • 用于半实时工作的高线程优先级
  • 一些后台工作的低线程优先级
  • UI的正常线程优先级

因此,我需要一个自定义调度程序。我可以很容易地想象到,一些糟糕的开发人员需要以某种方式将同步和异步代码与一些特殊的调度程序混合在一起(一个平行宇宙可能不包含这样的开发人员);但是为什么他们不使用更健壮的近似代码,这样他们就不需要写一个冗长的注释来解释为什么和它做了什么?