什么时候使用Task.Yield()
本文关键字:Yield Task 什么时候 | 更新日期: 2023-09-27 17:53:44
我使用async/await和Task
很多,但从来没有使用过Task.Yield(),说实话,即使有所有的解释,我也不明白为什么我需要这个方法。
谁能给一个很好的例子,其中Yield()
是必需的?
当您使用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
。
在内部,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.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()
?我会回答标题"你什么时候会使用Task.Yield()
"。广泛的第一位。当满足以下条件时:
- 希望将控制权返回给异步上下文(建议任务调度器先执行队列中的其他任务)
- 需要在异步上下文中继续
- 倾向于在任务调度程序空闲时立即继续
- 不希望被取消
- 偏好较短的代码
术语"async context";这里的意思是"先SynchronizationContext
,再TaskScheduler
。它被Stephen Cleary使用。
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的正常线程优先级
Task.Yield()
可用于模拟异步方法的实现。