ChatGPT解决这个技术问题 Extra ChatGPT

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

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

有人可以举一个需要 Yield() 的好例子吗?

对于这里的任何 js 开发人员,这相当于 setTimeout(_, 0)

R
Reed Copsey

当您使用 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() 强制方法是异步的,我们为什么还要费心编写“真正的”异步代码?想象一个重度同步方法。要使其异步,只需在开头添加 asyncawait Task.Yield(),神奇的是,它会是异步的?这几乎就像将所有同步代码包装到 Task.Run() 并创建一个虚假的异步方法一样。
@Krumelur 有很大的不同 - 看看我的例子。如果您使用 Task.Run 来实现它,ExecuteFooOnUIThread 将在线程池上运行,而不是在 UI 线程上运行。使用 await Task.Yield(),您可以强制它异步,以使后续代码仍然在当前上下文中运行(只是在稍后的时间点)。这不是您通常会做的事情,但是如果出于某种奇怪的原因需要它,那么有一个选项很好。
还有一个问题:如果 ExecuteFooOnUIThread() 运行时间很长,它仍然会在某个时候长时间阻塞 UI 线程并使 UI 无响应,对吗?
@Krumelur 是的,会的。只是不会立即发生——它会在以后发生。
虽然这个答案在技术上是正确的,但“其余代码将在以后执行”的说法过于抽象,可能会产生误导。 Task.Yield() 之后代码的执行计划在很大程度上取决于具体的 SynchronisationContext。 MSDN 文档明确指出“在大多数 UI 环境中,UI 线程上存在的同步上下文通常会将发布到上下文的工作优先于输入和渲染工作。因此,不要依赖 await Task.Yield() ; 保持 UI 响应。”
n
noseratio

在内部,如果 SynchronizationContext.Currentnullawait 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 - 实际用途?


在您的示例中,what's there 和 var dialogTask = await showAsync(); 之间有什么区别?
@ErikPhilips,var dialogTask = await showAsync() 不会编译,因为 await showAsync() 表达式不返回 Task(与没有 await 不同)。也就是说,如果你做await showAsync(),它之后的执行只有在对话框关闭后才会恢复,这就是它的不同之处。那是因为 window.ShowDialog 是一个同步 API(尽管它仍然会泵送消息)。在该代码中,我想在对话框仍然显示时继续。
await Task.Yield()await Task.Delay(1) 的效果相同吗?
@DavidKlempfner,是的,在这种情况下,它的行为应该相同,但您为什么要选择 await Task.Delay(1) 而不是 await Task.Yield()
J
Joakim M. H.

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(); 会做几乎完全相同的事情?
k
keithyip

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。我可以很容易地想象一些糟糕的开发人员需要以某种方式将同步和异步代码与一些特殊的调度程序混合在一个平行宇宙中(一个宇宙可能不包含这样的开发人员);但是他们为什么不只使用更健壮的近似代码,这样他们就不需要写冗长的评论来解释它的原因和作用?


S
Stephen Kennedy

Task.Yield() 可用于异步方法的模拟实现。


你应该提供一些细节。
为此,我宁愿使用 Task.CompletedTask - 有关更多注意事项,请参阅 this msdn blog post 中的 Task.CompletedTask 部分。
使用 Task.CompletedTask 或 Task.FromResult 的问题在于,您可能会错过仅在方法异步执行时出现的错误。