When would I use Task.Yield()?
Asked Answered
Y

7

361

I'm using async/await and Task a lot but have never been using Task.Yield() and to be honest even with all the explanations I do not understand why I would need this method.

Can somebody give a good example where Yield() is required?

Yaya answered 25/3, 2014 at 20:0 Comment(3)
For any js developers out here, this is equivalent to setTimeout(_, 0).Stuck
I find this blog post really helpful: duongnt.com/task-yieldGallantry
A real world usage is to prevent any synchronized implement of BackgroundService.ExecuteAsync() blocking IHostedService.StartAsync(), see blog.stephencleary.com/2020/05/… and github.com/dotnet/runtime/issues/36063Infuriate
D
377

When you use async/await, there is no guarantee that the method you call when you do await FooAsync() will actually run asynchronously. The internal implementation is free to return using a completely synchronous path.

If you're making an API where it's critical that you don't block and you run some code asynchronously, and there's a chance that the called method will run synchronously (effectively blocking), using await Task.Yield() will force your method to be asynchronous, and return control at that point. The rest of the code will execute at a later time (at which point, it still may run synchronously) on the current context.

This can also be useful if you make an asynchronous method that requires some "long running" initialization, ie:

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

Without the Task.Yield() call, the method will execute synchronously all the way up to the first call to await.

Doorkeeper answered 25/3, 2014 at 20:5 Comment(16)
I feel like I'm misinterpreting something here. If await Task.Yield() forces the method to be async, why would we bother writing "real" async code? Imagine a heavy sync method. To make it async, just add async and await Task.Yield() in the beginning and magically, it will be async? That would pretty much be like wrapping all sync code into Task.Run() and create a fake async method.Yaya
@Yaya There's a big difference - look at my example. If you use a Task.Run to implement it, ExecuteFooOnUIThread will run on the thread pool, not the UI thread. With await Task.Yield(), you force it to be asynchronous in a way that the subsequent code is still run on the current context (just at a later point in time). It's not something you'd normally do, but it is nice that there is the option if it's required for some strange reason.Doorkeeper
One more question: if ExecuteFooOnUIThread() was very long running, it would still block the UI thread for a long time at some point and make the UI unresponsive, is that correct?Yaya
@Yaya Yes, it would. Just not immediately - it'd happen at a later time.Doorkeeper
Although this answer is technically correct, the statement that "the rest of the code will execute at a later time" is too abstract and may be misleading. Execution schedule of the code after Task.Yield() is very much dependent on concrete SynchronisationContext. And MSDN documentation clearly states that "The synchronization context that is present on a UI thread in most UI environments will often prioritize work posted to the context higher than input and rendering work. For this reason, do not rely on await Task.Yield(); to keep a UI responsive."Melinite
How is this different from Thread.Yield? It smells the same to me.Spicer
It's better to use Dispatcher.BeginInvoke in this casePious
In your case, Task.Yield() did nothing different to Dispatcher.InvokeAsync with an empty action. Also, Invoke provide DispatcherPriority param to let the UI more responsive than Yield which without any params.Whitehorse
@ReedCopsey you said "@Yaya Yes, it would. Just not immediately - it'd happen at a later time." what you mean by that? I mean who is the boss here? what is the point, it will block the UI anyway, now or later?Erasure
I'm confused... Wouldn't a random await Task.Delay(1).ConfigureAwait(true); causes all the stuff below it to run at later time on the UI thread? Is Task.Yield() designed for this purpose, or is it just one possible way to use it?Regina
Great write up, the only thing that I feel needs to be pointed out just to avoid confusion in the method signature; is that it is returning void. Although because you are in an event handler, here it's OK. But generally, When returning from a function / method declared as async that doesn't technically return an expected value, it should almost always return as type Task. Although there are a few exceptions. See section "Valid Reasons for Returning Void" here: Returning Void From a C# Async MethodOdilo
Here's a linqpad example that demonstrates the utility of Task.Yield() : gist.github.com/ronnieoverby/c401c97bbf7702088a4e9ccd58f79732Aleppo
In my case, I want the code after Task.Yield to be run on the UI thread since it's UI-related. I just don't want it to block when I call a normally user-blocked method (Window.ShowDialog() specifically) so I created a ShowDialogAsync extension method that returns Task<bool?> and is comprised of two lines... Task.Yield() followed by return window.ShowDialog(). Does that use-case make sense? Here's the q: stackoverflow.com/questions/65453171Wescott
If I understand that, it works a similar way as using BeginInvoke.... within form creation to shift the code within begininvoke to a later time and give the UI time to create the form.... BeginInvoke used in the ui context does nothing else than adding the code to the message queue and continues with everything else until the message queue reaches the point where my code has been added for execution... So yield may be a replacement for that begininvoke "hack" ?Jillene
Interesting, but why actually await the Task.Yield()?Bronze
Isn't this use of await Task.Yield() just a glorified Application.DoEvents()? If not, how is it better than the generally inadvisable Application.DoEvents?Fehr
B
56

Internally, await Task.Yield() simply queues the continuation on either the current synchronization context or on a random pool thread, if SynchronizationContext.Current is null.

It is efficiently implemented as custom awaiter. A less efficient code producing the identical effect might be as simple as this:

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() can be used as a short-cut for some weird execution flow alterations. For example:

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  
}

That said, I can't think of any case where Task.Yield() cannot be replaced with Task.Factory.StartNew w/ proper task scheduler.

See also:

Brigham answered 26/3, 2014 at 6:0 Comment(4)
In your example, what's the difference between what's there and var dialogTask = await showAsync();?Leicestershire
@ErikPhilips, var dialogTask = await showAsync() won't compile because the await showAsync() expression doesn't return a Task (unlike it does without await). That said, if you do await showAsync(), the execution after it will be resumed only after the dialog has been closed, that's how it's different. That's because window.ShowDialog is a synchronous API (despite it still pumps messages). In that code, I wanted to continue while the dialog is still shown.Brigham
Does await Task.Yield() have the same effect as await Task.Delay(1)?Fernandez
@DavidKlempfner, yep in this case it should behave the same, but why would you pick await Task.Delay(1) over await Task.Yield()?Brigham
U
7

Task.Yield() is like a counterpart of Thread.Yield() in async-await but with much more specific conditions. How many times do you even need Thread.Yield()? I will answer the title "when would you use Task.Yield()" broadly first. You would when the following conditions are fulfilled:

  • want to return the control to the async context (suggesting the task scheduler to execute other tasks in the queue first)
  • need to continue in the async context
  • prefer to continue immediately when the task scheduler is free
  • do not want to be cancelled
  • prefer shorter code

The term "async context" here means "SynchronizationContext first then TaskScheduler". It was used by Stephen Cleary.

Task.Yield() is approximately doing this (many posts get it slightly wrong here and there):

await Task.Factory.StartNew( 
    () => {}, 
    CancellationToken.None, 
    TaskCreationOptions.PreferFairness,
    SynchronizationContext.Current != null?
        TaskScheduler.FromCurrentSynchronizationContext(): 
        TaskScheduler.Current);

If any one of the conditions is broken, you need to use other alternatives instead.

If the continuation of a task should be in Task.DefaultScheduler, you normally use ConfigureAwait(false). On the contrary, Task.Yield() gives you an awaitable not having ConfigureAwait(bool). You need to use the approximated code with TaskScheduler.Default.

If Task.Yield() obstructs the queue, you need to restructure your code instead as explained by noseratio.

If you need the continuation to happen much later, say, in the order of millisecond, you would use Task.Delay.

If you want the task to be cancellable in the queue but do not want to check the cancellation token nor throw an exception yourself, you need to use the approximated code with a cancellation token.

Task.Yield() is so niche and easily dodged. I only have one imaginary example by mixing my experience. It is to solve an async dining philosopher problem constrained by a custom scheduler. In my multi-thread helper library InSync, it supports unordered acquisitions of async locks. It enqueues an async acquisition if the current one failed. The code is here. It needs ConfigureAwait(false) as a general purpose library so I need to use Task.Factory.StartNew. In a closed source project, my program needs to execute significant synchronous code mixed with async code with

  • a high thread priority for semi-realtime work
  • a low thread priority for some background work
  • a normal thread priority for UI

Thus, I need a custom scheduler. I could easily imagine some poor developers somehow need to mix sync and async code together with some special schedulers in a parallel universe (one universe probably does not contain such developers); but why wouldn't they just use the more robust approximated code so they do not need to write a lengthy comment to explain why and what it does?

Urn answered 30/1, 2021 at 16:17 Comment(4)
I don't think that the Task.Yield has any affiliation with the TaskScheduler.Current. It yields back on the SynchronizationContext.Current, not on the current TaskScheduler.Fehr
@TheodorZoulias Are you referring to the approximated code? I am not sure what "Task.Yield has any affiliation with the TaskScheduler.Current" means. Task.Yield() prefers to continue on SynchronizationContext.Current. If SynchronizationContext.Current is null, the continuation will be enqueued to the original queue. I am not aware of a way to do the same thing as Task.Yield(). The closest way, although still different, is the code above. This is one of the reasons why there exists Task.Yield().Urn
Yes, I am referring to the approximated code, that schedules the continuation on the TaskScheduler.Current when the SynchronizationContext.Current is null.Fehr
I just checked the source code of the YieldAwaitable and I was wrong. The Task.Yield does indeed capture to the TaskScheduler.Current when there is no SynchronizationContext.Current to capture. So your approximation of Task.Yield is correct.Fehr
M
6

One use of Task.Yield() is to prevent a stack overflow when doing async recursion. Task.Yield() prevents syncronous continuation. Note, however, that this can cause an OutOfMemory exception (as noted by Triynko). Endless recursion is still not safe and you're probably better off rewriting the recursion as a loop.

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();
    }
Monocycle answered 27/8, 2019 at 7:29 Comment(4)
This might prevent a stack overflow, but this will eventually run out of system memory if you let it run long enough. Each iteration would create a new Task that never completes, as the outer Task is awaiting an inner Task, which is awaiting yet another inner Task, and so on. This is not OK. Alternatively, you could simply have one outermost Task that never completes, and just have it loop instead of recurse. The Task would never complete, but there would only be one of them. Inside the loop, it could yield or await anything you'd like.Rescript
I can't reproduce the stack overflow. It seems that the await Task.Delay(1) is enough to prevent it. (Console App, .NET Core 3.1, C# 8)Fehr
I would have thought await Task.Delay(1); and await Task.Yield(); would do almost exactly the same thing?Fernandez
@DavidKlempfner I haven't worked on .Net for a while, so my information might be a bit outdated, but for .Net 4.7 at least, await Task.Delay(1) left it up to the runtime to decide if the delay should happen sequentially without yielding control of the thread or if an unfinished task should be returned and control yielded. On the other hand, await Task.Yield() guaranteed that the execution context would yield control of the current thread.Monocycle
F
1

I have never been using Task.Yield(), and to be honest even with all the explanations I do not understand why I would need this method.

You have never used the Task.Yield, because this API has no clear purpose in life. Apart from Noseratio's rather esoteric (yet clever and sophisticated) use for juggling between nested UI message loops, I have yet to see someone using this API for a purpose that could be described as "intended", and also a purpose that cannot be fulfilled in a better way using other APIs with clear-cut semantics.

Most people discover the Task.Yield when they want to offload work to the ThreadPool, and then use it because it works. There are two problems with this:

  1. There is a Task.Run API that exists for exactly this reason. Offloading work to the ThreadPool is its documented purpose.
  2. The await Task.Yield() yields to the ThreadPool only in the absence of a synchronization context, in other words only when the SynchronizationContext.Current is null. Otherwise it yields to wherever the current synchronization context thinks it should yield. Since the Task.Yield is documented to "asynchronously yield back to the current context when awaited", it could be said that any behavior it has in the absence of a synchronization context is accidental and not to be relied on.

I'll finish this answer with an example showing how to modify existing solutions that use incorrectly the await Task.Yield() for the purpose of offloading, by replacing it with the correct API Task.Run.

Incorrect:

foreach (var file in files)
{
    await Task.Yield();
    Process(file);
}

Fix #1 (preserving the above behavior that schedules on the ThreadPool the processing of each file individually):

foreach (var file in files) 
{
    await Task.Run(() => Process(file));
}

Fix #2 (doing all the work on a single ThreadPool thread without any application-level interruption):

await Task.Run(() =>
{
    foreach (var file in files) 
    {
        Process(file);
    }
});

If you think that the await Task.Yield(); is sexier than the await Task.Run(() => { });, you might be looking for the SwitchTo API that currently exists in the Microsoft.VisualStudio.Threading package. To learn why this API doesn't exist in the standard .NET libraries, see this question.

Fehr answered 26/2 at 18:23 Comment(6)
-1 because your example #3 WILL FULLY BLOCK an entire thread for an arbitrary amount of time and will cause scalability and stability problems, as I learned firsthand. Suppose 16 simultaneous API requests came in trying to do the same thing. Otherwise what you label as "incorrect" and "fixed" are identical if "incorrect" were wrapped in a single Task.Run.Patin
@EmperorEto if you have a problem with a saturated ThreadPool, and hope to fix it by splitting each big job to many smaller jobs, a better solution might be to configure your ThreadPool by using the SetMinThreads API. If you know before-hand that your application needs 100 threads, please configure your ThreadPool accordingly, and don't rely on its algorithm which upon reaching the soft limit Environment.ProcessorCount starts injecting slowly one new thread per second (undocumented behavior).Fehr
Also note that the default server configuration (which should be the case if this is an API) is going to automatically set the min thread pool threads to a lot (I think 100-200?). So this is only the case if you are not using a standard web project or have already set the min threads to something lower.Irrupt
@TheodorZoulias I did some tests of the theory that Task.Run and Task.Yield can be used equivalently and posted the results as an asnwer here - https://mcmap.net/q/93785/-task-yield-real-usages/…. In my tests Task.Yield outperformed Task.Run by almost 3x, which surprised me. I'd love to get your take on the results.Patin
@TheodorZoulias I'm not familiar with Task.Yield, but reading the docs it looks equivalent to old-school VB DoEvents - is that fair to say?Kehr
@Kehr yes, I consider await Task.Yield() and Application.DoEvents() to be very similar. I am not sure how exactly they differ.Fehr
A
-2

One useful case I found for Yield is when creating a method (specially in tests) that returns an IAsyncEnumerable type.

private static async IAsyncEnumerable<Response> CreateAsyncEnumerable() {
        await Task.Yield();
        yield return new Response ("data");
    }

In this case, if I don't add the await Task.Yield();, it still works but it complains with:

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Other examples often simulate async by adding Task.Delay(), but I'd rather not in tests.

Archon answered 28/11, 2023 at 13:29 Comment(0)
F
-3

Task.Yield() may be used in mock implementations of async methods.

Fastigiate answered 11/3, 2018 at 9:20 Comment(3)
You should provide some details.Detached
For this purpose, I'd rather use Task.CompletedTask - see section Task.CompletedTask in this msdn blog post for more considerations.Grith
The problem with using Task.CompletedTask, or Task.FromResult is that you could miss bugs that only appear when the method executes asynchronous.Monocycle

© 2022 - 2024 — McMap. All rights reserved.