本文記錄我讀 dotnet 的原始碼瞭解到為什麼呼叫 Thread.Sleep 的時候,傳入的是不足一毫秒,如半毫秒時或 0.99 毫秒,與傳入是一毫秒時,兩者的等待時間差距非常大
大概如下的程式碼,分別進行兩次傳入給 Thread.Sleep 不同等待時間的迴圈測試。其中一次傳入的是 0.99 毫秒,一次傳入的是 1 毫秒
using System.Diagnostics;
var stopwatch = Stopwatch.StartNew();
for (int i = 0; i < 1000; i++)
{
Thread.Sleep(TimeSpan.FromMilliseconds(0.99));
}
stopwatch.Stop();
Console.WriteLine($"耗時:{stopwatch.ElapsedMilliseconds}ms");
stopwatch.Restart();
for (int i = 0; i < 1000; i++)
{
Thread.Sleep(TimeSpan.FromMilliseconds(1));
}
Console.WriteLine($"耗時:{stopwatch.ElapsedMilliseconds}ms");
在我的裝置上執行的輸出內容如下
耗時:0ms
耗時:15665ms
透過如上程式碼可以看到傳入 0.99 毫秒時,居然接近統計不出來其耗時
而傳入 1 毫秒時,由於在 Windows 下的最低 Thread.Sleep 時間大概在 15-16毫秒 左右,於是差不多是 15 秒左右的時間,這是符合預期的。即寫入 Thread.Sleep(TimeSpan.FromMilliseconds(1));
也可能差不多等待 15 毫秒的量程時間
那為什麼 0.99 毫秒和 1 毫秒只差大概 0.1 毫秒的時間,卻在等待過程中有如此長的時間差距
透過閱讀 dotnet 的原始碼,可以看到 Thread.Sleep 的實現程式碼大概如下
namespace System.Threading
{
public sealed partial class Thread
{
... // 忽略其他程式碼
public static void Sleep(TimeSpan timeout) => Sleep(WaitHandle.ToTimeoutMilliseconds(timeout));
}
}
namespace System.Threading
{
public abstract partial class WaitHandle : MarshalByRefObject, IDisposable
{
internal static int ToTimeoutMilliseconds(TimeSpan timeout)
{
long timeoutMilliseconds = (long)timeout.TotalMilliseconds;
... // 忽略其他程式碼
return (int)timeoutMilliseconds;
}
... // 忽略其他程式碼
}
}
透過以上可以可見,這是直接將 TotalMilliseconds 強行轉換為 int 型別,換句話說就是不到 1 毫秒的,都會被轉換為 0 毫秒的值
於是即使是 0.99 毫秒,在這裡的轉換之下,依然會返回 0 毫秒回去
而 Thread.Sleep 底層裡面專門為傳入 0 毫秒做了特殊處理,將會進入自旋邏輯。大家都知道,進入自旋時,自旋的速度是非常快的
以上的 Thread.Sleep(TimeSpan.FromMilliseconds(0.99));
程式碼和 Thread.Sleep(0)
在執行上等價的,意味著第一次只執行了一千次自旋,自然就幾乎測試不出來耗時了
在 Windows 下的 Thread.Sleep 底層程式碼是寫在 Thread.Windows.cs 程式碼裡的,實現如下
namespace System.Threading
{
public sealed partial class Thread
{
internal static void UninterruptibleSleep0() => Interop.Kernel32.Sleep(0);
#if !CORECLR
private static void SleepInternal(int millisecondsTimeout)
{
Debug.Assert(millisecondsTimeout >= -1);
Interop.Kernel32.Sleep((uint)millisecondsTimeout);
}
#endif
... // 忽略其他程式碼
}
}
如上面程式碼,底層為 Kernel32 的 Sleep 函式,如官方文件所述,傳入 0 是特殊的實現邏輯
If you specify 0 milliseconds, the thread will relinquish the remainder of its time slice but remain ready.
因此如果在 Thread.Sleep 方法裡面傳入的 TimeSpan 不足一毫秒,那就和傳入 0 毫秒是相同的執行邏輯
更多基礎技術部落格,請參閱 部落格導航