1

【译文】满月时,代码工作异常

 6 months ago
source link: https://www.techug.com/post/the-code-worked-differently-when-the-moon-was-full/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

【译文】满月时,代码工作异常

我喜欢好的bug,尤其是那些一开始很难解释,但后来却变成了拍额头时刻的错误–当然!

Github 上有一个名为 “线程池爬山的滞后效应 “的 bug,读起来非常有趣。

爬坡是一种算法技术,当你遇到一个问题(一座山),然后你会不断改进(爬坡),直到达到某个最大可接受的解决方案(到达山顶)。

该 bug 的作者塞巴斯蒂安说,线程池存在 “滞后效应”。”滞后是指系统状态对其历史的依赖性。因为以前发生过一些事情,所以现在发生了一些奇怪的事情……但到底是什么呢?

锯齿形的上下图并不那么有趣……但看看 X 轴。这并不像你以前看到的那样显示每分钟甚至每毫秒的涨跌。这个 x 轴以月为计量单位。再读一遍,好好体会。

二月份天气凉爽,直到连续几周都很糟糕,然后三月份天气又凉爽起来,如此循环往复。虽然不是严格意义上的月亮周期,但也差不多。

在使用 PortableThreadPool 时,他看到使用的内核数在逐月变化

我们注意到线程池爬山逻辑的周期性模式,它使用 n 个内核或 n 个内核 + 20,每 3-4 周切换一次,具有滞后效应。

你知道(我知道是因为我老了)Windows 95 有一段时间无法运行超过 49.7 天的运行时间吗?如果你运行了那么久,它最终会崩溃!这是因为一天有 8600 万毫秒,即 1000 * 60 * 60 * 24 = 86,400,000 而 32 位是 4,294,967,296 所以 4,294,967,296 / 86,400,000 = 49.7102696 天!

凯文在 Github 问题中也指出了这一点:

方波的整个周期听起来非常像 49.7 天左右。这就是 GetTickCount() 绕一圈所需的时间。在 POSIX 平台上,平台抽象层实现了这一功能,返回的值不是基于正常运行时间,而是基于挂钟时间,这与所有机器在同一天发生变化相吻合。

这个 49.7 天的数字是众所周知的,因为这是在 GetTickCount() 溢出/包裹之前所需要的时间。凯文接着给出了与图表相对应的更换日期!

  • Thu Jan 14 2021
  • Sun Feb 07 2021
  • Thu Mar 04 2021
  • Mon Mar 29 2021
  • Fri Apr 23 2021

然后,他在 PortableThreadPool.cs 中找到了解释该问题的代码:

private bool ShouldAdjustMaxWorkersActive(int currentTimeMs)
{
// We need to subtract by prior time because Environment.TickCount can wrap around, making a comparison of absolute times unreliable.
int priorTime = Volatile.Read(ref _separated.priorCompletedWorkRequestsTime);
int requiredInterval = _separated.nextCompletedWorkRequestsTime - priorTime;
int elapsedInterval = currentTimeMs - priorTime;
if (elapsedInterval >= requiredInterval)
{
...

他说,这都是凯文的功劳:

currentTimeMsEnvironment.TickCount,在本例中恰好是负数。

if 子句控制是否运行爬山。

_separated.priorCompletedWorkRequestsTime_separated.nextCompletedWorkRequestsTime 在进程开始时为零,只有在运行爬坡代码时才会更新。

因此,requiredInterval = 0 - 0elapsedInterval = negativeNumber - 0。这使得 if 语句变成
if (negativeNumber - 0 >= 0 - 0) 返回 false,因此爬坡代码不会运行,变量也不会更新,保持为零。原生版本的线程池代码使用无符号数进行所有数学运算,可以避免出现这样的错误,而且它的等价部分首先就不是完全相同的数学运算

最简单的解决方法可能是使用无符号运算,或者将两个字段初始化为 Environment.TickCount 也行得通

回到我这里。好极了。解决方法是通过 (uint) 将结果转换为无符号整数。

int requiredInterval = _separated.nextCompletedWorkRequestsTime - priorTime;
int elapsedInterval = currentTimeMs - priorTime;
uint requiredInterval = (uint)(_separated.nextCompletedWorkRequestsTime - priorTime);
uint elapsedInterval = (uint)(currentTimeMs - priorTime);

真是一个有趣而阴险的bug!基于时间计算的错误往往会在日后显现出来,如果用更长的视角和时间范围来观察……有时比你想象的要长很多。

本文文字及图片出自 The code worked differently when the moon was full

O1CN011Fay0n1OQdoDMGBKF_!!2768491700.jpg_640x640q80_.webp

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK