Reputation: 148514
I've been reading about Task.Yield
, And as a Javascript developer I can tell that's it's job is exactly the same as setTimeout(function (){...},0);
in terms of letting the main single thread deal with other stuff aka :
"don't take all the power , release from time time - so others would have some too..."
In js it's working particular in long loops. ( don't make the browser freeze...)
But I saw this example here :
public static async Task < int > FindSeriesSum(int i1)
{
int sum = 0;
for (int i = 0; i < i1; i++)
{
sum += i;
if (i % 1000 == 0) ( after a bulk , release power to main thread)
await Task.Yield();
}
return sum;
}
As a JS programmer I can understand what they did here.
BUT as a C# programmer I ask myself : why not open a task for it ?
public static async Task < int > FindSeriesSum(int i1)
{
//do something....
return await MyLongCalculationTask();
//do something
}
Question
With Js I can't open a Task (yes i know i can actually with web workers) . But with c# I can.
If So -- why even bother with releasing from time to time while I can release it at all ?
Adding references :
From here :
From here (another ebook):
Upvotes: 53
Views: 34215
Reputation: 3520
This is a supplement to Maxim T's answer from 2018 (which I agree with). I decided to test the theory that Task.Yield
can improve performance when the CPU is under heavy stress from many parallel CPU-intensive tasks/requests (e.g., extremely busy servers). The answer - which came as a surprise - is that Task.Yield
indeed gives much better overall results than Task.Run
alone or relying solely on multithreading. Now, before anyone heads straight for that down arrow, I hope you'll read on.
The Test
Each test initiates 512 parallel loops as quickly as possible, which all perform an infinite summation of +1, -1, +1, etc. Each loop further records in a static array (one for each loop) the number of iterations completed, and checks a CancellationToken
to know when to terminate.
Here's how the loops are initiated (time keeping and logging code omitted):
internal static class EatMyThreads
{
const int c_MaxTasks = 512;
static long[] _iterCounter = new long[c_MaxTasks];
static CancellationTokenSource _cts = new CancellationTokenSource();
public delegate Task InfiniteSeriesDelegate(int i, CancellationToken token);
public static void EatMyThreads(InfiniteSeriesDelegate series, string methodName, bool upMinThreads)
{
if (upMinThreads)
{
ThreadPool.GetMinThreads(out _, out var minPortThreads);
System.Threading.ThreadPool.SetMinThreads(c_MaxTasks * 2, minPortThreads);
}
for (int i = 0; i < c_MaxTasks; i++)
{
_ = series(i, _cts.Token);
}
Thread.Sleep(5000);
_cts.Cancel();
Thread.Sleep(5000); // Give some time for cancellation
// log results
}
// individual loop methods
}
I next define 3 methods conforming to InfiniteSeriesDelegate
, each with a different approach to parallelism.
Method 1 - One Thread per Loop
This executes each loop within a single Task.Run
without regard to blocking the thread, i.e., each loop monopolizes the entire thread.
public static async Task DoInfiniteSeries_Method1(int i, CancellationToken c)
{
await Task.Run(() =>
{
double n = 0;
while (true)
{
if (c.IsCancellationRequested)
break;
n += 1;
n -= 1;
_iterCounter[i]++;
}
});
}
Method 2 - Chunks Wrapped in Task.Run
Here, chunks of 1000 loop iterations are each queued to the threadpool via an independent Task.Run
call as soon as the last one completes. In theory this should be equivalent to Method 3 below.
public static async Task DoInfiniteSeries_Method2(int i, CancellationToken c)
{
double n = 0;
while (true)
{
if (c.IsCancellationRequested)
break;
await Task.Run(() =>
{
for (int j = 0; j < 1000; j++)
{
n += 1;
n -= 1;
_iterCounter[i]++;
}
});
}
}
Method 3 - Chunks Punctuated with Task.Yield()
This final method does the same thing as Method 2 except with Task.Yield
instead of Task.Run
. Again, I was expecting this method to perform nearly identically to Method 2.
public static async Task DoInfiniteSeries_Method3(int i, CancellationToken c)
{
await Task.Run(async () =>
{
double n = 0;
while (true)
{
if (c.IsCancellationRequested)
break;
for (int j = 0; j < 1000; j++)
{
n += 1;
n -= 1;
_iterCounter[i]++;
}
await Task.Yield();
}
});
}
For each method, I record how many iterations each of the 512 parallel loops completes in the aggregate within the allowed time (i.e. how much raw work is able to be done), and also how uniformly they do so (i.e. is each loop able to complete roughly the same number of iterations or do some loops never even get off the ground?). I also test each method with and without increasing the minimum number of threads beforehand using SetMinThreads
to see if this is will force the runtime to give us the extra needed threads quickly enough to prevent starvation.
Other Details
Tests were run in a .NET8 console app in release mode without a debugger attached, on a 3.6GHz Core i9 with 20 logical cores. Compiler optimizations were disabled to eliminate the possibility of an unintended optimization impacting one or more of the methods differently. Each test was also performed with a fresh start of the application to eliminate the possibility of one test affecting the thread pool in a way that influenced the other tests.
Results
Method 3 - employing Task.Yield
on every 1,000th iteration - simply blew away the other methods both as a function of raw iterations completed and the uniformity of progress between each loop. Interestingly performance was not dramatically affected by SetMinThreads
. In other words using Task.Yield
to punctuate the loop allowed us to get a lot more work done, and with fewer threads, than the other methods did.
Method 2 (using Task.Run
to schedule chunks of work) performed less than half as well as Method 3 in terms of raw power, and abysmally in terms of parallelism, with only a fraction of the loops able to get more than a single chunk queued up and executed. Like Method 3, Method 2 benefits only modestly from increasing the thread count at the start.
Method 1 - allowing thread blocking and relying entirely on the thread pool to grow as needed - is the undisputed loser. Unsurprisingly Method 1 does benefit from increasing the minimum thread count, but what is surprising is that setting SetMinThreads
was still not enough to allow more than a few dozen loops to get underway within the first 5 seconds.
Conclusion
Task.Yield
seems to offer a clear benefit over Task.Run
as a means to promote parallelism and maximize raw performance in otherwise synchronous CPU-intensive code while minimizing the risk of threadpool starvation. Potentially long-running in-memory operations such as complex Regex
s, hashes of large data sets, image processing without a GPU, etc., may benefit from being rewritten as async
methods and periodically Yield
ing to other work. Simply trusting that cranking up the threadpool size will solve all problems and otherwise treating in-memory operations as cost-free would appear to be a fallacy.
I hope this exercise encourages folks to take a new look at this outlier of a .NET method and reconsider its utility. The entire code for these tests is available here, so I also would encourage anyone to see if they can reproduce these results or improve upon them. Of course if anyone sees a flaw in the methodology or has an explanation for the observed performance difference between Task.Run
and Task.Yield
I hope they'll weigh in.
Upvotes: 4
Reputation: 60055
First of all let me clarify: Yield
is not exactly the same as setTimeout(function (){...},0);
. JS is executed in single thread environment, so that is the only way to let other activities to happen. Kind of cooperative multitasking. .net is executed in preemptive multitasking environment with explicit multithreading.
Now back to Task.Yield
. As I told .net lives in preemptive world, but it is a little more complicated than that. C# await/async
create interesting mixture of those multitasking mode ruled by state machines. So if you omit Yield
from your code it will just block the thread and that's it. If you make it a regular task and just call start (or a thread) then it will just do it's stuff in parallel and later block calling thread when task.Result
is called. What happens when you do await Task.Yield();
is more complicated. Logically it unblocks the calling code (similar to JS) and execution goes on. What it actually does - it picks another thread and continue execution in it in preemptive environment with calling thread. So it is in calling thread until first Task.Yield
and then it is on it's own. Subsequent calls to Task.Yield
apparently don't do anything.
Simple demonstration:
class MainClass
{
//Just to reduce amount of log items
static HashSet<Tuple<string, int>> cache = new HashSet<Tuple<string, int>>();
public static void LogThread(string msg, bool clear=false) {
if (clear)
cache.Clear ();
var val = Tuple.Create(msg, Thread.CurrentThread.ManagedThreadId);
if (cache.Add (val))
Console.WriteLine ("{0}\t:{1}", val.Item1, val.Item2);
}
public static async Task<int> FindSeriesSum(int i1)
{
LogThread ("Task enter");
int sum = 0;
for (int i = 0; i < i1; i++)
{
sum += i;
if (i % 1000 == 0) {
LogThread ("Before yield");
await Task.Yield ();
LogThread ("After yield");
}
}
LogThread ("Task done");
return sum;
}
public static void Main (string[] args)
{
LogThread ("Before task");
var task = FindSeriesSum(1000000);
LogThread ("While task", true);
Console.WriteLine ("Sum = {0}", task.Result);
LogThread ("After task");
}
}
Here are results:
Before task :1
Task enter :1
Before yield :1
After yield :5
Before yield :5
While task :1
Before yield :5
After yield :5
Task done :5
Sum = 1783293664
After task :1
If you move Task.Yield
on top of the method it will by async from the beginning and will not block the calling thread.
Conclusion: Task.Yield
can make possible to mix sync and async code. Some more or less realistic scenario: you have some heavy computational operation and local cache and task CalcThing
. In this method you check if item is in cache, if yes - return item, if it is not there Yield
and proceed to calculate it. Actually sample from your book is rather meaningless because nothing useful is achieved there. Their remark regarding GUI interactivity is just bad and incorrect (UI thread will be locked until first call to Yield
, you should never do that, MSDN is clear (and correct) on that: "do not rely on await Task.Yield(); to keep a UI responsive".
Upvotes: 3
Reputation: 61656
When you see:
await Task.Yield();
you can think about it this way:
await Task.Factory.StartNew(
() => {},
CancellationToken.None,
TaskCreationOptions.None,
SynchronizationContext.Current != null?
TaskScheduler.FromCurrentSynchronizationContext():
TaskScheduler.Current);
All this does is makes sure the continuation will happen asynchronously in the future. By asynchronously I mean that the execution control will return to the caller of the async
method, and the continuation callback will not happen on the same stack frame.
When exactly and on what thread it will happen completely depends on the caller thread's synchronization context.
For a UI thread, the continuation will happen upon some future iteration of the message loop, run by Application.Run
(WinForms) or Dispatcher.Run
(WPF). Internally, it comes down to the Win32 PostMessage
API, which post a custom message to the UI thread's message queue. The await
continuation callback will be called when this message gets pumped and processed. You're completely out of control about when exactly this is going to happen.
Besides, Windows has its own priorities for pumping messages: INFO: Window Message Priorities. The most relevant part:
Under this scheme, prioritization can be considered tri-level. All posted messages are higher priority than user input messages because they reside in different queues. And all user input messages are higher priority than WM_PAINT and WM_TIMER messages.
So, if you use await Task.Yield()
to yield to the message loop in attempt to keep the UI responsive, you are actually at risk of obstructing the UI thread's message loop. Some pending user input messages, as well as WM_PAINT
and WM_TIMER
, have a lower priority than the posted continuation message. Thus, if you do await Task.Yield()
on a tight loop, you still may block the UI.
This is how it is different from the JavaScript's setTimer
analogy you mentioned in the question. A setTimer
callback will be called after all user input message have been processed by the browser's message pump.
So, await Task.Yield()
is not good for doing background work on the UI thread. In fact, you very rarely need to run a background process on the UI thread, but sometimes you do, e.g. editor syntax highlighting, spell checking etc. In this case, use the framework's idle infrastructure.
E.g., with WPF you could do await Dispatcher.Yield(DispatcherPriority.ApplicationIdle)
:
async Task DoUIThreadWorkAsync(CancellationToken token)
{
var i = 0;
while (true)
{
token.ThrowIfCancellationRequested();
await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);
// do the UI-related work item
this.TextBlock.Text = "iteration " + i++;
}
}
For WinForms, you could use Application.Idle
event:
// await IdleYield();
public static Task IdleYield()
{
var idleTcs = new TaskCompletionSource<bool>();
// subscribe to Application.Idle
EventHandler handler = null;
handler = (s, e) =>
{
Application.Idle -= handler;
idleTcs.SetResult(true);
};
Application.Idle += handler;
return idleTcs.Task;
}
It is recommended that you do not exceed 50ms for each iteration of such background operation running on the UI thread.
For a non-UI thread with no synchronization context, await Task.Yield()
just switches the continuation to a random pool thread. There is no guarantee it is going to be a different thread from the current thread, it's only guaranteed to be an asynchronous continuation. If ThreadPool
is starving, it may schedule the continuation onto the same thread.
In ASP.NET, doing await Task.Yield()
doesn't make sense at all, except for the workaround mentioned in @StephenCleary's answer. Otherwise, it will only hurt the web app performance with a redundant thread switch.
So, is await Task.Yield()
useful? IMO, not much. It can be used as a shortcut to run the continuation via SynchronizationContext.Post
or ThreadPool.QueueUserWorkItem
, if you really need to impose asynchrony upon a part of your method.
Regarding the books you quoted, in my opinion those approaches to using Task.Yield
are wrong. I explained why they're wrong for a UI thread, above. For a non-UI pool thread, there's simply no "other tasks in the thread to execute", unless you running a custom task pump like Stephen Toub's AsyncPump
.
Updated to answer the comment:
... how can it be asynchronouse operation and stay in the same thread ?..
As a simple example: WinForms app:
async void Form_Load(object s, object e)
{
await Task.Yield();
MessageBox.Show("Async message!");
}
Form_Load
will return to the caller (the WinFroms framework code which has fired Load
event), and then the message box will be shown asynchronously, upon some future iteration of the message loop run by Application.Run()
. The continuation callback is queued with WinFormsSynchronizationContext.Post
, which internally posts a private Windows message to the UI thread's message loop. The callback will be executed when this message gets pumped, still on the same thread.
In a console app, you can run a similar serializing loop with AsyncPump
mentioned above.
Upvotes: 94
Reputation: 1282
I think that nobody provided the real answer when to use the Task.Yield. It is mostly needed if a task uses a never ending loop (or lengthy synchronous job), and can potentially hold a threadpool thread exclusively (not allowing other tasks to use this thread). This can happen if inside the loop the code runs synchronously. the Task.Yield reschedules the task to the threadpool queue and the other tasks which waited for the thread can be executed.
The example:
CancellationTokenSource cts;
void Start()
{
cts = new CancellationTokenSource();
// run async operation
var task = Task.Run(() => SomeWork(cts.Token), cts.Token);
// wait for completion
// after the completion handle the result/ cancellation/ errors
}
async Task<int> SomeWork(CancellationToken cancellationToken)
{
int result = 0;
bool loopAgain = true;
while (loopAgain)
{
// do something ... means a substantial work or a micro batch here - not processing a single byte
loopAgain = /* check for loop end && */ cancellationToken.IsCancellationRequested;
if (loopAgain) {
// reschedule the task to the threadpool and free this thread for other waiting tasks
await Task.Yield();
}
}
cancellationToken.ThrowIfCancellationRequested();
return result;
}
void Cancel()
{
// request cancelation
cts.Cancel();
}
Upvotes: 4
Reputation: 456322
I've only found Task.Yield
useful in two scenarios:
Upvotes: 28
Reputation: 700152
No, it's not exactly like using setTimeout
to return control to the UI. In Javascript that would always let the UI update as the setTimeout
always has a minimum pause of a few milliseconds, and pending UI work has priority over timers, but await Task.Yield();
doesn't do that.
There is no guarantee that the yield will let any work be done in the main thread, on the contrary the code that called the yield will often be prioritised over UI work.
"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."
Upvotes: 8
Reputation:
You're assuming the long-running function is one that can run on a background thread. If it isn't, for example because it has UI interaction, then there is no way to prevent blocking the UI while it runs, so the times it runs should be kept short enough not to cause problems for the users.
Another possibility is that you have more long-running functions than you have background threads. In that scenario, it may be better (or it may not matter, it depends) to prevent a few of those functions from taking up all of your threads.
Upvotes: 0