简体   繁体   English

“await Task.Yield()”及其替代方案

[英]"await Task.Yield()" and its alternatives

If I need to postpone code execution until after a future iteration of the UI thread message loop, I could do so something like this:如果我需要将代码执行推迟到 UI 线程消息循环的未来迭代之后,我可以这样做:

await Task.Factory.StartNew(
    () => {
        MessageBox.Show("Hello!");
    },
    CancellationToken.None,
    TaskCreationOptions.None,
    TaskScheduler.FromCurrentSynchronizationContext());

This would be similar to await Task.Yield(); MessageBox.Show("Hello!");这类似于await Task.Yield(); MessageBox.Show("Hello!"); await Task.Yield(); MessageBox.Show("Hello!"); , besides I'd have an option to cancel the task if I wanted to. ,此外,如果我愿意,我可以选择取消任务。

In case with the default synchronization context, I could similarly use await Task.Run to continue on a pool thread.在使用默认同步上下文的情况下,我可以类似地使用await Task.Run在池线程上继续。

In fact, I like Task.Factory.StartNew and Task.Run more than Task.Yield , because they both explicitly define the scope for the continuation code.事实上,我更喜欢Task.Factory.StartNewTask.Run而不是Task.Yield ,因为它们都明确定义了延续代码的范围。

So, in what situations await Task.Yield() is actually useful?那么,在什么情况下await Task.Yield()真的有用呢?

Task.Yield() is great for "punching a hole" in an otherwise synchronous part of an async method. Task.Yield()非常适合在async方法的其他同步部分“打孔”。

Personally I've found it useful in cases where I have a self-cancelling async method (one which manages its own corresponding CancellationTokenSource and cancels the previously created instance on each subsequent call) that can be called multiple times within an extremely short time period (ie by interdependent UI elements' event handlers).就我个人而言,我发现它在我有一个可以在极短的时间内多次调用的自取消async方法(管理自己相应的CancellationTokenSource并在每次后续调用中取消先前创建的实例的方法)的情况下很有用(即通过相互依赖的 UI 元素的事件处理程序)。 In such a situation using Task.Yield() followed by an IsCancellationRequested check as soon as the CancellationTokenSource is swapped out can prevent doing potentially expensive work whose results will end up discarded anyway.在这种情况下,一旦CancellationTokenSource被换出,使用Task.Yield()后跟IsCancellationRequested检查可以防止做潜在的昂贵工作,其结果最终将被丢弃。

Here's an example where only the last queued call to SelfCancellingAsync gets to perform expensive work and run to completion.这是一个示例,其中只有对SelfCancellingAsync的最后排队调用才能执行昂贵的工作并运行到完成。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace TaskYieldExample
{
    class Program
    {
        private static CancellationTokenSource CancellationTokenSource;

        static void Main(string[] args)
        {
            SelfCancellingAsync();
            SelfCancellingAsync();
            SelfCancellingAsync();

            Console.ReadLine();
        }

        private static async void SelfCancellingAsync()
        {
            Console.WriteLine("SelfCancellingAsync starting.");

            var cts = new CancellationTokenSource();
            var oldCts = Interlocked.Exchange(ref CancellationTokenSource, cts);

            if (oldCts != null)
            {
                oldCts.Cancel();
            }

            // Allow quick cancellation.
            await Task.Yield();

            if (cts.IsCancellationRequested)
            {
                return;
            }

            // Do the "meaty" work.
            Console.WriteLine("Performing intensive work.");

            var answer = await Task
                .Delay(TimeSpan.FromSeconds(1))
                .ContinueWith(_ => 42, TaskContinuationOptions.ExecuteSynchronously);

            if (cts.IsCancellationRequested)
            {
                return;
            }

            // Do something with the result.
            Console.WriteLine("SelfCancellingAsync completed. Answer: {0}.", answer);
        }
    }
}

The goal here is to allow the code which executes synchronously on the same SynchronizationContext immediately after the non-awaited call to the async method returns (when it hits its first await ) to change the state that affects the execution of the async method.这里的目标是允许在对 async 方法的非等待调用返回后立即在同一个SynchronizationContext上同步执行的代码(当它遇到第一个await时)来更改影响 async 方法执行的状态。 This is throttling much like that achieved by Task.Delay (i'm talking about a non-zero delay period here), but without the actual , potentially noticeable delay, which can be unwelcome in some situations.这与Task.Delay实现的节流非常相似(我在这里谈论的是非零延迟期),但没有实际的、潜在的明显延迟,这在某些情况下可能不受欢迎。

Consider the case when you want your async task to return a value.考虑您希望异步任务返回值的情况。

Existing synchronous method:现有同步方法:

public int DoSomething()
{
    return SomeMethodThatReturnsAnInt();
}

To make async, add async keyword and change return type:要进行异步,请添加 async 关键字并更改返回类型:

public async Task<int> DoSomething()

To use Task.Factory.StartNew(), change the one-line body of the method to:要使用 Task.Factory.StartNew(),请将方法的单行正文更改为:

// start new task
var task = Task<int>.Factory.StartNew(
    () => {
        return SomeMethodThatReturnsAnInt();
    },
    CancellationToken.None,
    TaskCreationOptions.None,
    TaskScheduler.FromCurrentSynchronizationContext() );

// await task, return control to calling method
await task;

// return task result
return task.Result;

vs. adding a single line if you use await Task.Yield()与如果您使用await Task.Yield()则添加一行

// this returns control to the calling method
await Task.Yield();

// otherwise synchronous method scheduled for async execution by the 
// TaskScheduler of the calling thread
return SomeMethodThatReturnsAnInt();

The latter is far more concise, readable, and really doesn't change the existing method much.后者更加简洁、易读,并且实际上并没有太多改变现有的方法。

One situation where Task.Yield() is actually useful is when you are await recursively-called synchronously-completed Task s . Task.Yield()真正有用的一种情况是当您await递归调用同步完成的Task时。 Because csharp's async / await “releases Zalgo” by running continuations synchronously when it can, the stack in a fully synchronous recursion scenario can get big enough that your process dies.因为 csharp 的async / await通过尽可能同步运行延续来“释放 Zalgo” ,所以完全同步递归场景中的堆栈可以变得足够大,以至于您的进程死亡。 I think this is also partly due to tail-calls not being able to be supported because of the Task indirection.我认为这也部分是由于Task间接导致无法支持尾调用。 await Task.Yield() schedules the continuation to be run by the scheduler rather than inline, allowing growth in the stack to be avoided and this issue to be worked around. await Task.Yield()安排由调度程序而不是内联运行延续,从而避免堆栈增长并解决此问题。

Also, Task.Yield() can be used to cut short the synchronous portion of a method.此外, Task.Yield()可用于缩短方法的同步部分。 If the caller needs to receive your method's Task before your method performs some action, you can use Task.Yield() to force returning the Task earlier than would otherwise naturally happen.如果调用者需要在您的方法执行某些操作之前接收您的方法的Task ,您可以使用Task.Yield()强制提前返回Task ,否则会自然发生。 For example, in the following local method scenario, the async method is able to get a reference to its own Task safely (assuming you are running this on a single-concurrency SynchronizationContext such as in winforms or via nito's AsyncContext.Run() ):例如,在以下本地方法场景中, async方法能够安全地获取对其自身Task的引用(假设您在单并发SynchronizationContext上运行它,例如在 winforms 中或通过nito 的AsyncContext.Run() ):

using Nito.AsyncEx;
using System;
using System.Threading.Tasks;

class Program
{
    // Use a single-threaded SynchronizationContext similar to winforms/WPF
    static void Main(string[] args) => AsyncContext.Run(() => RunAsync());

    static async Task RunAsync()
    {
        Task<Task> task = null;
        task = getOwnTaskAsync();
        var foundTask = await task;
        Console.WriteLine($"{task?.Id} == {foundTask?.Id}: {task == foundTask}");

        async Task<Task> getOwnTaskAsync()
        {
            // Cause this method to return and let the 「task」 local be assigned.
            await Task.Yield();
            return task;
        }
    }
}

output:输出:

3 == 3: True

I am sorry that I cannot think up any real-life scenarios where being able to forcibly cut short the synchronous portion of an async method is the best way to do something.很抱歉,我想不出任何现实生活中的场景,其中能够强制缩短async方法的同步部分是做某事的最佳方式。 Knowing that you can do a trick like I just showed can be useful sometimes, but it tends to be more dangerous too.知道你可以像我刚刚展示的那样做一个技巧有时会很有用,但它也往往更危险。 Often you can pass around data in a better, more readable, and more threadsafe way.通常,您可以以更好、更易读和更线程安全的方式传递数据。 For example, you can pass the local method a reference to its own Task using a TaskCompletionSource instead:例如,您可以使用TaskCompletionSource向本地方法传递对其自身Task的引用:

using System;
using System.Threading.Tasks;

class Program
{
    // Fully free-threaded! Works in more environments!
    static void Main(string[] args) => RunAsync().Wait();

    static async Task RunAsync()
    {
        var ownTaskSource = new TaskCompletionSource<Task>();
        var task = getOwnTaskAsync(ownTaskSource.Task);
        ownTaskSource.SetResult(task);
        var foundTask = await task;
        Console.WriteLine($"{task?.Id} == {foundTask?.Id}: {task == foundTask}");

        async Task<Task> getOwnTaskAsync(
            Task<Task> ownTaskTask)
        {
            // This might be clearer.
            return await ownTaskTask;
        }
    }
}

output:输出:

2 == 2: True

Task.Yield isn't an alternative to Task.Factory.StartNew or Task.Run . Task.Yield不是Task.Factory.StartNewTask.Run的替代品。 They're totally different.他们完全不同。 When you await Task.Yield you allow other code on the current thread to execute without blocking the thread.当您await Task.Yield时,您允许当前线程上的其他代码在不阻塞线程的情况下执行。 Think of it like awaiting Task.Delay , except Task.Yield waits until the tasks are complete, rather than waiting for a specific time.把它想象成等待Task.Delay ,除了Task.Yield等到任务完成,而不是等待一个特定的时间。

Note: Do not use Task.Yield on the UI thread and assume the UI will always remain responsive.注意:不要在 UI 线程上使用Task.Yield并假设 UI 将始终保持响应。 It's not always the case.并非总是如此。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM