This code consumes near zero CPU (i5 family)
public void SpinWait() {
for (int i = 0; i < 10000; i++)
{
Task.Factory.StartNew(() =>
{
var sw = new SpinWait();
while (true)
{
sw.SpinOnce();
}
});
}
}
In my code performance difference compared to SemaphoreSlim is 3x times or more for the case when spinning is really justified (5 Mops). However, I am concerned about using it for long-term wait. The standard advice is to implement two-phase wait operation. I could check NextSpinWillYield
property and introduce a counter+reset to increase the default spinning iterations without yielding, and than back off to a semaphore.
But what are downsides of using just SpinWait.SpinOnce
for long-term waiting? I have looked through its implementation and it properly yields when needed. It uses Thread.SpinWait
which on modern CPUs uses PAUSE instruction and is quite efficient according to Intel.
One issue that I have found while monitoring Task Manager is that number of threads if gradually increasing due to the default ThreadPool algorithm (it adds a thread every second when all tasks are busy). This could be solved by using ThreadPool.SetMaxThreads
, and then the number of threads is fixed and CPU usage is still near zero.
If the number of long-waiting tasks is bounded, what are other pitfalls of using SpinWait.SpinOnce
for long-term waiting. Does it depend on CPU family, OS, .NET version?
(Just to clarify: I will still implement two-phase waiting, I am just curios why not using SpinOnce all the time?)
Well, the down-side is exactly the one you see, your code is occupying a thread without accomplishing anything. Preventing other code from running and forcing the threadpool manager to do something about it. Tinkering with ThreadPool.SetMaxThreads() is just a band-aid on what is likely to be a profusely bleeding wound, only ever use it when you need to catch the plane home.
Spinning should only ever be attempted when you have a very good guarantee that doing so is more efficient than a thread context switch. Which means that you have to be sure that the thread can continue within ~10,000 cpu cycles or less. That is only 5 microseconds, give or take, a wholeheckofalot less than what most programmers consider "long-term".
Use a sync object that will trigger a thread context switch instead. Or the lock
keyword.
Not only will that yield the processor so other waiting threads can get their job done, thus accomplishing a lot more work, it also provides an excellent cue to the OS thread scheduler. A sync object that is signaled will bump the priority of the thread so it is very likely to get the processor next.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With