我经常使用 async/await 和 Task
,但从未使用过 Task.Yield()
,老实说,即使有所有的解释,我也不明白为什么我需要这种方法。
有人可以举一个需要 Yield()
的好例子吗?
setTimeout(_, 0)
。
当您使用 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.Current
是 null
,await Task.Yield()
只是在当前同步上下文或随机池线程上对延续进行排队。
它是 efficiently implemented 作为自定义等待者。产生相同效果的效率较低的代码可能就像这样简单:
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
替换的情况。
也可以看看:
“await Task.Yield()”及其替代方案
Task.Yield - 实际用途?
var dialogTask = await showAsync();
之间有什么区别?
var dialogTask = await showAsync()
不会编译,因为 await showAsync()
表达式不返回 Task
(与没有 await
不同)。也就是说,如果你做await showAsync()
,它之后的执行只有在对话框关闭后才会恢复,这就是它的不同之处。那是因为 window.ShowDialog
是一个同步 API(尽管它仍然会泵送消息)。在该代码中,我想在对话框仍然显示时继续。
await Task.Yield()
与 await Task.Delay(1)
的效果相同吗?
await Task.Delay(1)
而不是 await 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();
}
await Task.Delay(1)
足以阻止它。 (控制台应用程序、.NET Core 3.1、C# 8)
await Task.Delay(1);
和 await Task.Yield();
会做几乎完全相同的事情?
Task.Yield()
与 async-await 中的 Thread.Yield()
类似,但条件更具体。您甚至需要多少次Thread.Yield()
?我将首先广泛地回答标题“您何时使用 Task.Yield()
”。当满足以下条件时,您会:
想要将控制权返回到异步上下文(建议任务调度器先执行队列中的其他任务)
需要在异步上下文中继续
更喜欢在任务调度程序空闲时立即继续
不想被取消
更喜欢更短的代码
术语“异步上下文”在这里的意思是“先同步上下文,然后是任务调度器”。它被 Stephen Cleary 使用。
Task.Yield()
大约在做 this(许多帖子在这里和那里都有点错误):
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 中,它支持异步锁的无序获取。如果当前采集失败,它会将异步采集排入队列。代码是 here。它需要 ConfigureAwait(false)
作为通用库,所以我需要使用 Task.Factory.StartNew
。在一个闭源项目中,我的程序需要执行与异步代码混合的重要同步代码
半实时工作的高线程优先级
一些后台工作的低线程优先级
UI 的正常线程优先级
因此,我需要一个custom scheduler。我可以很容易地想象一些糟糕的开发人员需要以某种方式将同步和异步代码与一些特殊的调度程序混合在一个平行宇宙中(一个宇宙可能不包含这样的开发人员);但是他们为什么不只使用更健壮的近似代码,这样他们就不需要写冗长的评论来解释它的原因和作用?
Task.Yield()
可用于异步方法的模拟实现。
不定期副业成功案例分享
await Task.Yield()
强制方法是异步的,我们为什么还要费心编写“真正的”异步代码?想象一个重度同步方法。要使其异步,只需在开头添加async
和await Task.Yield()
,神奇的是,它会是异步的?这几乎就像将所有同步代码包装到Task.Run()
并创建一个虚假的异步方法一样。Task.Run
来实现它,ExecuteFooOnUIThread
将在线程池上运行,而不是在 UI 线程上运行。使用await Task.Yield()
,您可以强制它异步,以使后续代码仍然在当前上下文中运行(只是在稍后的时间点)。这不是您通常会做的事情,但是如果出于某种奇怪的原因需要它,那么有一个选项很好。ExecuteFooOnUIThread()
运行时间很长,它仍然会在某个时候长时间阻塞 UI 线程并使 UI 无响应,对吗?