当你看到:
await Task.Yield();
你可以这样想:
await Task.Factory.StartNew(
() => {},
CancellationToken.None,
TaskCreationOptions.None,
SynchronizationContext.Current != null?
TaskScheduler.FromCurrentSynchronizationContext():
TaskScheduler.Current);
所有这一切都是为了确保继续在未来异步发生。 异步我的意思是执行控制将返回给async方法的调用者,而继续回调将不会发生在同一个堆栈帧上。
具体何时发生以及发生在哪个线程上完全取决于调用者线程的同步上下文。
对于 UI 线程,继续将在消息循环的某些未来迭代中发生,由 Application.Run (WinForms) 或 Dispatcher.Run (WPF) 运行。在内部,它归结为 Win32 PostMessage API,它将自定义消息发布到 UI 线程的消息队列。 await 继续回调将在此消息被抽取和处理时调用。你完全无法控制这到底什么时候会发生。
此外,Windows 有其自己的消息发送优先级:INFO: Window Message Priorities。最相关的部分:
在这个方案下,优先级可以被认为是三级的。全部
发布消息的优先级高于用户输入消息,因为
他们驻留在不同的队列中。并且所有用户输入消息都是
优先级高于 WM_PAINT 和 WM_TIMER 消息。
因此,如果您使用await Task.Yield() 屈服于消息循环以试图保持 UI 响应,那么您实际上有阻碍 UI 线程的消息循环的风险。一些待处理的用户输入消息以及WM_PAINT 和WM_TIMER 的优先级低于已发布的继续消息。因此,如果您在紧密循环中执行 await Task.Yield(),您仍然可能会阻塞 UI。
这就是它与您在问题中提到的 JavaScript 的 setTimer 类比的不同之处。在浏览器的消息泵处理完所有用户输入消息后,将调用setTimer 回调。
所以,await Task.Yield() 不适合在 UI 线程上进行后台工作。事实上,你很少需要在 UI 线程上运行后台进程,但有时你会这样做,例如编辑器语法高亮、拼写检查等。在这种情况下,请使用框架的空闲基础设施。
例如,使用 WPF 你可以做到await Dispatcher.Yield(DispatcherPriority.ApplicationIdle):
async Task DoUIThreadWorkAsync(CancellationToken token)
{
var i = 0;
while (true)
{
token.ThrowIfCancellationRequested();
await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);
// do the UI-related work item
this.TextBlock.Text = "iteration " + i++;
}
}
对于 WinForms,您可以使用 Application.Idle 事件:
// await IdleYield();
public static Task IdleYield()
{
var idleTcs = new TaskCompletionSource<bool>();
// subscribe to Application.Idle
EventHandler handler = null;
handler = (s, e) =>
{
Application.Idle -= handler;
idleTcs.SetResult(true);
};
Application.Idle += handler;
return idleTcs.Task;
}
It is recommended 在 UI 线程上运行的此类后台操作的每次迭代不超过 50 毫秒。
对于没有同步上下文的非 UI 线程,await Task.Yield() 只是将延续切换到随机池线程。不能保证它会是一个与当前线程不同的 线程,它只能保证是一个异步 延续。如果ThreadPool 处于饥饿状态,它可能会将继续调度到同一个线程上。
在 ASP.NET 中,除了 @StephenCleary's answer 中提到的解决方法之外,执行 await Task.Yield() 完全没有意义。否则,它只会通过冗余线程切换而损害 Web 应用程序的性能。
那么,await Task.Yield() 有用吗? 国际海事组织,不多。如果您确实需要在方法的一部分上施加异步,它可以用作通过SynchronizationContext.Post 或ThreadPool.QueueUserWorkItem 运行延续的快捷方式。
关于您引用的书籍,我认为那些使用Task.Yield 的方法是错误的。我在上面解释了为什么它们对于 UI 线程是错误的。对于非 UI 池线程,根本没有“线程中要执行的其他任务”,除非您运行像 Stephen Toub's AsyncPump 这样的自定义任务泵。
更新回答评论:
... 怎么可能是异步操作并留在同一个线程中
?..
举个简单的例子:WinForms 应用:
async void Form_Load(object s, object e)
{
await Task.Yield();
MessageBox.Show("Async message!");
}
Form_Load 将返回给调用者(已触发 Load 事件的 WinFroms 框架代码),然后在 Application.Run() 运行的消息循环的某些未来迭代中,消息框将异步显示。延续回调与WinFormsSynchronizationContext.Post 一起排队,它在内部将私有 Windows 消息发布到 UI 线程的消息循环。当此消息被抽出时,回调将被执行,仍然在同一个线程上。
在控制台应用程序中,您可以使用上面提到的AsyncPump 运行类似的序列化循环。