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

Kru*_*lur 196 c# async-await

我正在使用异步/等待和Task很多,但从来没有使用过Task.Yield(),说实话,即使有所有的解释,我不明白为什么我需要这种方法.

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

Ree*_*sey 211

当您使用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);
 }
Run Code Online (Sandbox Code Playgroud)

没有Task.Yield()调用,该方法将一直执行到第一次调用await.

  • 尽管这个答案在技术上是正确的,但"其余代码将在以后执行"的陈述过于抽象,可能会产生误导.Task.Yield()之后代码的执行计划非常依赖于具体的SynchronisationContext.MSDN文档明确指出"大多数UI环境中UI线程上存在的同步上下文通常会优先考虑发布到上下文的工作,而不是输入和渲染工作.因此,不要依赖于等待Task.Yield() ;保持用户界面的响应能力." (28认同)
  • 我觉得我在这里误解了一些东西.如果`await Task.Yield()`强制该方法是异步的,为什么我们要编写"真正的"异步代码呢?想象一下沉重的同步方法.要使它异步,只需在开头添加`async`和`await Task.Yield()`并且神奇地,它将是异步的?这几乎就像将所有同步代码包装成`Task.Run()`并创建一个伪异步方法. (22认同)
  • @Krumelur有一个很大的不同 - 看看我的例子.如果使用`Task.Run`来实现它,`ExecuteFooOnUIThread`将在线程池上运行,而不是在UI线程上运行.使用`await Task.Yield()`,可以强制它以后续代码仍在当前上下文中运行的方式异步(仅在稍后的时间点).这不是你通常会做的事情,但如果出于某些奇怪的原因需要它,那就很好了. (8认同)
  • 还有一个问题:如果`ExecuteFooOnUIThread()`长时间运行,它仍然会在某个时刻阻止UI线程长时间并使UI无响应,这是正确的吗? (6认同)
  • @Krumelur是的,它会的.只是没有立即 - 它会在以后发生. (6认同)
  • @ReedCopsey 你说“@Krumelur 是的,会的。只是不会立即发生 - 它会__稍后发生__。” 你那是什么意思?我的意思是,这里的老大是谁?有什么意义,它无论如何都会阻塞 UI,现在还是以后? (3认同)
  • 我很困惑...随机的“await Task.Delay(1).ConfigureAwait(true);”不会导致其下面的所有内容稍后在 UI 线程上运行吗?“Task.Yield()”是为此目的而设计的,还是只是*一种可能的使用方式*? (3认同)

nos*_*tio 29

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

它被有效地实现为定制awaiter.产生相同效果的效率较低的代码可能就像这样简单:

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;
Run Code Online (Sandbox Code Playgroud)

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  
}
Run Code Online (Sandbox Code Playgroud)

也就是说,我无法想到任何Task.Yield()无法用Task.Factory.StartNew适当的任务调度程序替换的情况.

也可以看看:


kei*_*yip 6

Task.Yield()类似于Thread.Yield()async-await 的对应部分,但具有更具体的条件。您甚至需要多少次Thread.Yield()?我将首先广泛回答标题“您什么时候会使用Task.Yield()”。当满足以下条件时您会:

  • 想要将控制权返回给异步上下文(建议任务调度程序首先执行队列中的其他任务)
  • 需要在异步上下文中继续
  • 更愿意在任务调度程序空闲时立即继续
  • 不想被取消
  • 更喜欢较短的代码

术语“异步上下文”在这里的意思是“SynchronizationContext先然后TaskScheduler”。它是由史蒂芬·克利里(Stephen Cleary)使用的。

Task.Yield()大约是这样做的(许多帖子在这里和那里都略有错误):

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

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

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

如果Task.Yield()阻塞队列,您需要按照nosratio的解释重新构建代码。

如果您需要在更晚的时间(例如,以毫秒为单位)继续发生,您可以使用Task.Delay.

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

Task.Yield()是如此利基,很容易被躲避。我只是结合我的经验想象出一个例子。它是为了解决受自定义调度程序约束的异步哲学家就餐问题。在我的多线程帮助程序库InSync中,它支持异步锁的无序获取。如果当前的异步获取失败,它会将异步获取排入队列。代码在这里。它需要ConfigureAwait(false)作为通用库,所以我需要使用Task.Factory.StartNew. 在闭源项目中,我的程序需要执行与异步代码混合的重要同步代码

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

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


Joa*_* H. 5

一种用途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();
    }
Run Code Online (Sandbox Code Playgroud)

  • 这可能会防止堆栈溢出,但如果让它运行足够长的时间,最终会耗尽系统内存。每次迭代都会创建一个永远不会完成的新任务,因为外部任务正在等待内部任务,而内部任务又在等待另一个内部任务,依此类推。这是不行的。或者,您可以简单地设置一个永远不会完成的最外层任务,并让它循环而不是递归。任务永远不会完成,但只有其中之一。在循环内部,它可以产生或等待任何你想要的东西。 (5认同)
  • 我无法重现堆栈溢出。看来“await Task.Delay(1)”足以阻止它。(控制台应用程序、.NET Core 3.1、C# 8) (2认同)