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

我喜欢好的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

余下全文(1/3)
分享这篇文章:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注