21

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?


  • I've only used Task.Yield in unit tests and to work around an obscure ASP.NET issue where an async method must not complete synchronously. - Stephen Cleary
  • Related: Task.Yield - real usages? - noseratio
  • I understand that your question didn’t ask this, but it is kind of related. Calling MessageBox.Show() without passing the IWin32Window owner argument may result in the messagebox “popping under” your window if that code executes when your window does not have focus. This is particularly confusing if done on the GUI thread. Also, if you do pass IWin32Window to MessageBox.Show(), you need to do so on the UI thread. So, in that case, you must not use Task.Run() and must pass TaskScheduler to StartNew(). - binki
  • Additionally, there’s no need to put MessageBox.Show() on a separate thread because MessageBox.Show() pumps the message queue and runs continuations scheduled to the GUI’s SynchronizationContext. I.e., you can continue updating your form and displaying things async methods just like you normally would even while the MessageBox.Show() is showing. - binki

4 답변


1

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


  • Tackling recursion is indeed a good use for it, although the cost of it highly depends on a particular synchronization context. - noseratio
  • @noseratio True. The fact that it has overhead can be mitigated a bit by passing a counter around and only using it every 30 recursions or so. I'm not so sure what a SynchronizationContext would have to do with it, though. If you are doing something like this, hopefully it's not on the GUI thread if in a graphical application. But if you are recurring with Task APIs, you might not know for certain that the Tasks you're awaiting aren't complete, so it might be necessary to do it. You can always make it conditional on the Task being Task.IsCompleted and a counter. - binki

6

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.


  • To be fair, it's still a single line: return await Task.Factory.StartNew(() => SomeMethodThatReturnsAnInt(), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());. Nevertheless, I see the point, +1. The problem with this is somewhat non-intuitive alteration of control flow, discussed here. - noseratio
  • well, you also need the await task; line - Moho
  • Revisiting this, if I need to turn SomeMethodThatReturnsAnInt into async, I could simply do: public Task<int> DoSomething() { return Task.FromResult(SomeMethodThatReturnsAnInt()); }. Or, for async semantic of exception propagation: public async Task<int> DoSomething() { return await Task.FromResult(SomeMethodThatReturnsAnInt()); }. Clearly, await Task.Yield() would be redundant and undesired here. Sorry for undoing my vote. - noseratio
  • It’s much better to simply suppress the CS1998 warning than force a context switch just to indirectly suppress the compiler warning. - binki

4

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.


  • Thanks for your thoughts, although I'm doing the same without Task.Yield or helper Task.Delay, here's my version of cancel-and-restart pattern. - noseratio
  • Btw, you may have issues with handling exceptions from your "meaty" work, because of async void signature of SelfCancellingAsync. - noseratio
  • I am specifically not handling any exceptions for the sake of brevity - that is the job of whoever chooses to use the pattern. The example had to be a void, because making it a Task suggests that it should be awaited, which is not the case here. It's also why I'm returning from the method when a cancellation is detected as opposed to throwing an exception (which would generally be the preferred way of going about communicating cancellation to the caller). - Kirill Shlenskiy
  • That is correct, and it's a key difference between the two types of async methods. Whenever you have an async Task, throwing exceptions and letting the caller handle them is the preferred way of going about things. In the case of async void, however, the exception handling generally needs to be wired into the body of the async method itself. - Kirill Shlenskiy
  • I guess it may be OK if your task is totally isolated and you handle all exceptions inside SelfCancellingAsync. However, eventually you're going to request a cancellation from outside (e.g., when your Main method exits and the app terminates). At this point, you'd probably want to wait for the pending task, started by SelfCancellingAsync, to let it finish gracefully. I can't see how this can be done with your approach. - noseratio

-1

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.


  • I believe I have a good understanding of how Task.Yield and other custom awaiters work. In this light, I mostly disagree with everything besides your last sentence. - noseratio
  • “except Task.Yield waits until the tasks are complete, rather than waiting for a specific time.”—it doesn’t wait for other tasks to complete. It just schedules a “ready-to-run” continuation and switches to the next, existing “ready-to-run” continuation which is waiting in line (which might be itself). I.e., this just forces your task it to stop being synchronous at that point. - binki

Linked


Related

Latest