“await Task.Yield()” and its alternatives

2019-01-22 12:50发布

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:

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

This would be similar to 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.

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.

So, in what situations await Task.Yield() is actually useful?

4条回答
在下西门庆
2楼-- · 2019-01-22 13:26

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:

public async Task<int> DoSomething()

To use Task.Factory.StartNew(), change the one-line body of the method to:

// 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()

// 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.

查看更多
不美不萌又怎样
3楼-- · 2019-01-22 13:30

One situation where Task.Yield() is actually useful is when you are await recursively-called synchronously-completed Tasks. 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. I think this is also partly due to tail-calls not being able to be supported because of the Task indirection. 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.

Also, Task.Yield() can be used to cut short the synchronous portion of a method. 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. 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()):

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. 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:

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
查看更多
老娘就宠你
4楼-- · 2019-01-22 13:42

Task.Yield isn't an alternative to Task.Factory.StartNew or Task.Run. They're totally different. When you await Task.Yield you allow other code on the current thread to execute without blocking the thread. Think of it like awaiting Task.Delay, except Task.Yield waits until the tasks are complete, rather than waiting for a specific time.

Note: Do not use Task.Yield on the UI thread and assume the UI will always remain responsive. It's not always the case.

查看更多
贪生不怕死
5楼-- · 2019-01-22 13:48

Task.Yield() is great for "punching a hole" in an otherwise synchronous part of an async method.

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 (i.e. by interdependent UI elements' event handlers). 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.

Here's an example where only the last queued call to SelfCancellingAsync gets to perform expensive work and run to completion.

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. 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.

查看更多
登录 后发表回答