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


218

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

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

Answers:


241

当使用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


26
我觉得我在误解这里的内容。如果await Task.Yield()强制该方法异步,为什么还要麻烦编写“真实的”异步代码?想象一下繁重的同步方法。要使其异步,只需添加,async然后await Task.Yield()在开头神奇地将其异步?这几乎就像将所有同步代码包装成Task.Run()一个伪造的异步方法一样。
Krumelur 2014年

14
@Krumelur有很大的不同-看我的例子。如果使用A Task.Run来实现,ExecuteFooOnUIThread它将运行在线程池上,而不是UI线程上。使用await Task.Yield(),您可以强制其异步,以使后续代码仍在当前上下文上运行(只是在稍后的时间点)。这不是您通常要做的,但是如果出于某些奇怪的原因需要此选项,则很好。
里德·科普西2014年

7
还有一个问题:如果ExecuteFooOnUIThread()运行时间很长,它仍然会在某个时候长时间阻塞UI线程并使UI无响应,这是正确的吗?
Krumelur 2014年

7
@Krumelur是的,会的。只是不是立即-以后会发生。
Reed Copsey 2014年

33
尽管从技术上这个答案是正确的,但“其余代码将在以后执行”的说法过于抽象,可能会引起误解。Task.Yield()之后的代码执行时间表在很大程度上取决于具体的SynchronisationContext。而且MSDN文档明确指出:“在大多数UI环境中,UI线程上存在的同步上下文通常会将发布到上下文的工作的优先级高于输入和呈现工作。因此,请勿依赖于wait Task.Yield() ;以保持用户界面的响应速度。”
Vitaliy Tsvayer

36

在内部,await Task.Yield()仅在当前同步上下文或随机池线程(如果SynchronizationContext.Current是)上对继续进行排队null

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

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适当的任务调度程序代替的情况。

也可以看看:


在您的示例中,那里和那里有什么区别var dialogTask = await showAsync();
Erik Philips

@ErikPhilips,var dialogTask = await showAsync()将不会编译,因为该await showAsync()表达式不会返回a Task(与不使用则不同await)。就是说,如果您这样做await showAsync(),则仅在关闭对话框之后才可以继续执行它,这就是不同之处。这是因为它window.ShowDialog是一个同步API(尽管它仍然会泵送消息)。在该代码中,我想继续显示对话框。
noseratio

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();
    }

4
这可以防止堆栈溢出,但是如果您让它运行足够长的时间,最终将耗尽系统内存。每次迭代都会创建一个永远不会完成的新任务,因为外部任务正在等待内部任务,而内部任务正在等待另一个内部任务,依此类推。这不行。或者,您可以只拥有一个永远不会完成的最外面的任务,而让它循环而不是递归。该任务将永远不会完成,但其中只有一个。在循环内部,它可以产生或等待您想要的任何东西。
Triynko

我无法重现堆栈溢出。似乎await Task.Delay(1)足以阻止它。(控制台应用程序,.NET Core 3.1,C#8)
Theodor Zoulias

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.