本文记录我读 dotnet 的源代码了解到为什么调用 Thread.Sleep 的时候,传入的是不足一毫秒,如半毫秒时或 0.99 毫秒,与传入是一毫秒时,两者的等待时间差距非常大
大概如下的代码,分别进行两次传入给 Thread.Sleep 不同等待时间的循环测试。其中一次传入的是 0.99 毫秒,一次传入的是 1 毫秒
在我的设备上运行的输出内容如下
通过如上代码可以看到传入 0.99 毫秒时,居然接近统计不出来其耗时
而传入 1 毫秒时,由于在 Windows 下的最低 Thread.Sleep 时间大概在 15-16毫秒 左右,于是差不多是 15 秒左右的时间,这是符合预期的。即写入 Thread.Sleep(TimeSpan.FromMilliseconds(1));
也可能差不多等待 15 毫秒的量程时间
那为什么 0.99 毫秒和 1 毫秒只差大概 0.1 毫秒的时间,却在等待过程中有如此长的时间差距
通过阅读 dotnet 的源代码,可以看到 Thread.Sleep 的实现代码大概如下
通过以上可以可见,这是直接将 TotalMilliseconds 强行转换为 int 类型,换句话说就是不到 1 毫秒的,都会被转换为 0 毫秒的值
于是即使是 0.99 毫秒,在这里的转换之下,依然会返回 0 毫秒回去
而 Thread.Sleep 底层里面专门为传入 0 毫秒做了特殊处理,将会进入自旋逻辑。大家都知道,进入自旋时,自旋的速度是非常快的 将会直接出让线程执行时间片。也就是说假设系统给当前线程分配了 10 毫秒的执行时间,当前线程执行到 Thread.Sleep 之前,只花了 5 毫秒,当执行了 Thread.Sleep 将会直接让线程出让执行权,无论线程还剩余多少可执行时间。出让之后线程会重新加入调度,这个过程也是非常快速的。在 dotnet 里面的自旋 SpinWait 辅助类里面,是对 Thread.Sleep 和 Thread.Yield 之间的封装,确保不会进入长时间的自旋而导致影响系统整体运行
从狭义的自旋锁定义上看,自旋锁要求线程在这一过程中保持执行。因此自旋锁从定义上和 Thread.Sleep 会出让的行为是冲突的。但是在工程上,实现“自旋”概念的行为时,却会间断采用 Thread.Sleep(0)
等出让的方式,用于减少 CPU 的空转,如以下的 dotnet 源代码所示
以上的代码虽然是写在 SpinOnce 里面,但不意味着一定会占用着 CPU 进行空跑,而是会根据其等待时间决定是否将线程切出去,从而最大化利用系统资源
通过上文的分析,可以看到 Thread.Sleep(TimeSpan.FromMilliseconds(0.99));
代码和 Thread.Sleep(0)
在执行上等价的,意味着第一次只执行了一千次自旋线程出让,自然就几乎测试不出来耗时了
在 Windows 下的 Thread.Sleep 底层代码是写在 Thread.Windows.cs 代码里的,实现如下
如上面代码,底层为 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 毫秒是相同的执行逻辑
更多基础技术博客,请参阅 博客导航
原文链接: http://blog.lindexi.com/post/%E8%AF%BB-dotnet-%E6%BA%90%E4%BB%A3%E7%A0%81-%E4%B8%BA%E4%BD%95-Thread.Sleep-%E5%8D%8A%E6%AF%AB%E7%A7%92%E5%92%8C%E4%B8%80%E6%AF%AB%E7%A7%92%E7%AD%89%E5%BE%85%E6%97%B6%E9%97%B4%E5%B7%AE%E8%B7%9D%E5%A6%82%E6%AD%A4%E4%B9%8B%E5%A4%A7
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
欢迎转载、使用、重新发布,但务必保留文章署名 林德熙 (包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我 联系。