C#/.NET 中 Thread.Sleep(0), Task.Delay(0), Thread.Yield(), Task.Yield() 不同的執行效果和用法...
在 C#/.NET 中,有 Thread.Sleep(0)
, Task.Delay(0)
, Thread.Yield()
, Task.Yield()
中,有幾種不同的讓當前執行緒釋放執行權的方法。他們的作用都是放棄當前執行緒當前的執行權,讓其他執行緒得以排程。但是他們又不太一樣。
本文說說他們的原理區別和用法區別。
原理區別
Thread.Sleep(0)
Thread.Sleep(int millisecondsTimeout)
的程式碼貼在下面,其內部實際上是在呼叫 SleepInternal
,而 SleepInternal
由 CLR 內部實現。其目的是將當前執行緒掛起一個指定的時間間隔。
如果將超時時間設定為 0,即 Thread.Sleep(0)
,那麼這將強制當前執行緒放棄剩餘的 CPU 時間片。
放棄當前執行緒剩餘的 CPU 時間片就意味著其他比此執行緒優先順序高且可以被排程的執行緒將會在此時被排程。然而此方法只是放棄當前 CPU 執行的時間片,如果當前系統環境下其他可以被排程的其他執行緒的優先順序都比這個執行緒的優先順序低,實際上此執行緒依然還是會優先執行。
如果你的方法不會被其他執行緒影響,那麼不會有執行上的區別,但如果你的方法涉及到多個執行緒的呼叫,那麼 Thread.Sleep(0)
的呼叫可能導致其他執行緒也進入此方法(而不是等此執行緒的當前時間片執行完後再進入)。當然,CPU 對單個執行緒的執行時間片是納秒級別的,所以實際上你因為此方法呼叫獲得的多執行緒重入效果是“純屬巧合”的。
/*========================================================================= ** Suspends the current thread for timeout milliseconds. If timeout == 0, ** forces the thread to give up the remainer of its timeslice.If timeout ** == Timeout.Infinite, no timeout will occur. ** ** Exceptions: ArgumentException if timeout < 0. **ThreadInterruptedException if the thread is interrupted while sleeping. =========================================================================*/ [System.Security.SecurityCritical]// auto-generated [ResourceExposure(ResourceScope.None)] [MethodImplAttribute(MethodImplOptions.InternalCall)] private static extern void SleepInternal(int millisecondsTimeout); [System.Security.SecuritySafeCritical]// auto-generated public static void Sleep(int millisecondsTimeout) { SleepInternal(millisecondsTimeout); // Ensure we don't return to app code when the pause is underway if(AppDomainPauseManager.IsPaused) AppDomainPauseManager.ResumeEvent.WaitOneWithoutFAS(); }
Thread.Yield()
Thread.Yield()
的程式碼貼在下面,其內部呼叫 YieldInternal
,實際上也是由 CLR 內部實現。
此方法也是放棄當前執行緒的剩餘時間片,所以其效果與 Thread.Sleep(0)
是相同的。
[System.Security.SecurityCritical]// auto-generated [ResourceExposure(ResourceScope.None)] [DllImport(JitHelpers.QCall, CharSet = CharSet.Unicode)] [SuppressUnmanagedCodeSecurity] [HostProtection(Synchronization = true, ExternalThreading = true), ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] private static extern bool YieldInternal(); [System.Security.SecuritySafeCritical]// auto-generated [HostProtection(Synchronization = true, ExternalThreading = true), ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] public static bool Yield() { return YieldInternal(); }
Thread.Sleep(1)
Thread.Sleep(1)
與 Thread.Sleep(0)
雖然只有引數上的微小差別,但實際上做了不同的事情。
Thread.Sleep(1)
會使得當前執行緒掛起一個指定的超時時間,這裡設定為 1ms。於是,在這個等待的超時時間段內,你的當前執行緒處於不可被排程的狀態。那麼即便當前剩餘的可以被排程的執行緒其優先順序比這個更低,也可以得到排程。
下面是針對這三個方法執行時間的一個實驗結果:
▲ Thread 不同方法的耗時實驗結果
其中,Nothing 表示沒有寫任何程式碼。
測量使用的是 Stopwatch
,你可以通過閱讀 ofollow,noindex" target="_blank">.NET/C# 在程式碼中測量程式碼執行耗時的建議(比較系統性能計數器和系統時間) 瞭解 Stopwatch
測量的原理和精度。
var stopwatch = Stopwatch.StartNew(); Thread.Sleep(0); var elapsed = stopwatch.Elapsed; Console.WriteLine($"Thread.Sleep(0) : {elapsed}");
Task.Delay(0)
Task.Delay
是 Task
系列的執行緒模型(TAP)中的方法。關於 TAP 可參見 Task-based Asynchronous Pattern (TAP) Microsoft Docs 。
這是一套基於非同步狀態機(AsyncStateMachine)實現的執行緒模型,這也是與 Thread
系列方法最大的不同。
當傳入引數 0 的時候,會直接返回 Task.CompletedTask
。這意味著你在 Task.Delay(0)
後面寫的程式碼會被立刻呼叫(如果還有剩餘 CPU 時間片的話)。
/// <summary> /// Creates a Task that will complete after a time delay. /// </summary> /// <param name="millisecondsDelay">The number of milliseconds to wait before completing the returned Task</param> /// <param name="cancellationToken">The cancellation token that will be checked prior to completing the returned Task</param> /// <returns>A Task that represents the time delay</returns> /// <exception cref="T:System.ArgumentOutOfRangeException"> /// The <paramref name="millisecondsDelay"/> is less than -1. /// </exception> /// <exception cref="T:System.ObjectDisposedException"> /// The provided <paramref name="cancellationToken"/> has already been disposed. /// </exception> /// <remarks> /// If the cancellation token is signaled before the specified time delay, then the Task is completed in /// Canceled state.Otherwise, the Task is completed in RanToCompletion state once the specified time /// delay has expired. /// </remarks> public static Task Delay(int millisecondsDelay, CancellationToken cancellationToken) { // Throw on non-sensical time if (millisecondsDelay < -1) { throw new ArgumentOutOfRangeException("millisecondsDelay", Environment.GetResourceString("Task_Delay_InvalidMillisecondsDelay")); } Contract.EndContractBlock(); // some short-cuts in case quick completion is in order if (cancellationToken.IsCancellationRequested) { // return a Task created as already-Canceled return Task.FromCancellation(cancellationToken); } else if (millisecondsDelay == 0) { // return a Task created as already-RanToCompletion return Task.CompletedTask; } // Construct a promise-style Task to encapsulate our return value var promise = new DelayPromise(cancellationToken); // Register our cancellation token, if necessary. if (cancellationToken.CanBeCanceled) { promise.Registration = cancellationToken.InternalRegisterWithoutEC(state => ((DelayPromise)state).Complete(), promise); } // ... and create our timer and make sure that it stays rooted. if (millisecondsDelay != Timeout.Infinite) // no need to create the timer if it's an infinite timeout { promise.Timer = new Timer(state => ((DelayPromise)state).Complete(), promise, millisecondsDelay, Timeout.Infinite); promise.Timer.KeepRootedWhileScheduled(); } // Return the timer proxy task return promise; }
Task.Yield()
Task.Yield()
實際上只是返回一個 YieldAwaitable
的新例項,而 YieldAwaitable.GetAwaiter
方法返回一個 YieldAwaiter
的新例項。也就是說,後續的執行效果完全取決於 YieldAwaiter
是如何實現這個非同步過程的(非同步狀態機會執行這個過程)。我有另一篇部落格說明 Awaiter
是如何實現的: 如何實現一個可以用 await 非同步等待的 Awaiter 。
YieldAwaiter
靠 QueueContinuation
來決定後續程式碼的執行時機。此方法的核心程式碼貼在了下面。
有兩個分支,如果指定了 SynchronizationContext
,那麼就會使用 SynchronizationContext
自帶的 Post
方法來執行非同步任務的下一個步驟。呼叫 continuation
就是執行非同步狀態機中的下一個步驟以進入下一個非同步狀態;不過,為了簡化理解,你可以認為這就是呼叫 await
後面的那段程式碼。
WPF UI 執行緒的 SynchronizationContext
被設定為了 DispatcherSynchronizationContext
,它的 Post
方法本質上是用訊息迴圈來實現的。其他執行緒如果沒有特殊設定,則是 null
。這一部分知識可以看參見: 出讓執行權:Task.Yield, Dispatcher.Yield 。
如果沒有指定 SynchronizationContext
或者當前的 SynchronizationContext
就是 SynchronizationContext
型別基類,那麼就會執行後面 else
中的邏輯。主要就是線上程池中尋找一個執行緒然後執行程式碼,或者再次啟動一個 Task
任務並加入佇列;這取決於 TaskScheduler.Current
的設定。
// Get the current SynchronizationContext, and if there is one, // post the continuation to it.However, treat the base type // as if there wasn't a SynchronizationContext, since that's what it // logically represents. var syncCtx = SynchronizationContext.CurrentNoFlow; if (syncCtx != null && syncCtx.GetType() != typeof(SynchronizationContext)) { syncCtx.Post(s_sendOrPostCallbackRunAction, continuation); } else { // If we're targeting the default scheduler, queue to the thread pool, so that we go into the global // queue.As we're going into the global queue, we might as well use QUWI, which for the global queue is // just a tad faster than task, due to a smaller object getting allocated and less work on the execution path. TaskScheduler scheduler = TaskScheduler.Current; if (scheduler == TaskScheduler.Default) { if (flowContext) { ThreadPool.QueueUserWorkItem(s_waitCallbackRunAction, continuation); } else { ThreadPool.UnsafeQueueUserWorkItem(s_waitCallbackRunAction, continuation); } } // We're targeting a custom scheduler, so queue a task. else { Task.Factory.StartNew(continuation, default(CancellationToken), TaskCreationOptions.PreferFairness, scheduler); } }
Task.Delay(1)
與 Thread
一樣, Task.Delay(1)
與 Task.Delay(0)
雖然只有引數上的微小差別,但實際上也做了不同的事情。
Task.Delay(1)
實際上是啟動了一個 System.Threading.Timer
,然後訂閱時間抵達之後的回撥函式。
會從 Timer.TimerSetup
設定,到使用 TimerHolder
並在內部使用 TimerQueueTimer
來設定回撥;內部實際使用 TimerQueue.UpdateTimer
來完成時間等待之後的回撥通知,最終通過 EnsureAppDomainTimerFiresBy
呼叫到 ChangeAppDomainTimer
來完成時間抵達之後的回撥。
而 await
之後的那段程式碼會被非同步狀態機封裝,傳入上面的回撥中。
[System.Security.SecurityCritical] [ResourceExposure(ResourceScope.None)] [DllImport(JitHelpers.QCall, CharSet = CharSet.Unicode)] [SuppressUnmanagedCodeSecurity] static extern bool ChangeAppDomainTimer(AppDomainTimerSafeHandle handle, uint dueTime);
相比於 Thread
相關方法僅涉及到當前執行緒的排程, Task
相關的方法會涉及到執行緒池的排程,並且使用 System.Threading.Timer
來進行計時,耗時更加不可控:
▲ Task 不同方法的耗時實驗結果(三次不同的實驗結果)
其中,Nothing 表示沒有寫任何程式碼。
測量使用的是 Stopwatch
,你依然可以通過閱讀 .NET/C# 在程式碼中測量程式碼執行耗時的建議(比較系統性能計數器和系統時間) 瞭解 Stopwatch
測量的原理和精度。
var stopwatch = Stopwatch.StartNew(); await Task.Delay(0); var elapsed = stopwatch.Elapsed; Console.WriteLine($"Thread.Sleep(0) : {elapsed}");
在 [c# - Task.Delay(
You’re seeing an artifact of the Windows interrupt rate, which is (by default) approx every 15ms. Thus if you ask for 1-15ms, you’ll get an approx 15ms delay. ~16-30 will yield 30ms… so on.
用法區別
Thread.Sleep(0)
和 Thread.Yield
線上程排程的效果上是相同的, Thread.Sleep(int)
是帶有超時的等待,本質上也是執行緒排程。如果你希望通過放棄當前執行緒時間片以便給其他執行緒一些執行實際,那麼考慮 Thread.Sleep(0)
或者 Thread.Yield
;如果希望進行執行緒排程級別的等待(效果類似於阻塞執行緒),那麼使用 Thread.Sleep(int)
。
如果你允許有一個非同步上下文,可以使用 async/await
,那麼可以使用 Task.Delay(0)
或者 Task.Yield()
。另外,如果等待時使用 Task.Delay
而不是 Thread.Sleep
,那麼你可以節省一個執行緒的資源,尤其是在一個執行緒池的執行緒中 Sleep
的話,會使得執行緒池中更多的執行緒被進行無意義的佔用,對其他任務線上程池中的排程不利。
參考資料
- Thread.Sleep(0) vs Sleep(1) vs Yeild - stg609 - 部落格園
- [c# - Task.Delay(
).Wait(); sometimes causing a 15ms delay in messaging system - Stack Overflow](https://stackoverflow.com/q/41830216/6233938) - c# - When to use Task.Delay, when to use Thread.Sleep? - Stack Overflow
- c# - Should I always use Task.Delay instead of Thread.Sleep? - Stack Overflow
- What’s the difference between Thread.Sleep(0) and Thread,Yield()?
本文會經常更新,請閱讀原文: https://walterlv.com/post/sleep-delay-zero-vs-yield.html ,以避免陳舊錯誤知識的誤導,同時有更好的閱讀體驗。
本作品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名 呂毅 (包含連結:https://walterlv.com ),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。如有任何疑問,請 與我聯絡 ([email protected]) 。