什么时候使用Task.Yield()

本文关键字:Yield Task 什么时候 | 更新日期: 2023-09-27 17:53:44

我使用async/await和Task很多,但从来没有使用过Task.Yield(),说实话,即使有所有的解释,我也不明白为什么我需要这个方法。

谁能给一个很好的例子,其中Yield()是必需的?

什么时候使用Task.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.Currentnull

它被有效地实现为自定义侍者。产生相同效果的效率较低的代码可能像下面这样简单:

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()可用于模拟异步方法的实现。