【译文】满月时,代码工作异常
我喜欢好的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) { ... |
他说,这都是凯文的功劳:
currentTimeMs
是 Environment.TickCount
,在本例中恰好是负数。
if 子句控制是否运行爬山。
_separated.priorCompletedWorkRequestsTime
和 _separated.nextCompletedWorkRequestsTime
在进程开始时为零,只有在运行爬坡代码时才会更新。
因此,requiredInterval = 0 - 0
,elapsedInterval = 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
你也许感兴趣的:
- 我是如何在第一款登月游戏中发现一个 55 年前的漏洞的
- 【外评】航空公司总是把 101 岁的老太太误认为婴儿
- 【译文】经常嗡嗡叫的虫子(bug)
- 【程序员搞笑图片】要不要上报?
- 【译文】bug 经济学
- 【译文】一行代码如何造成 6000 万美元的损失
- 我所见过的最奇怪的Bug
- Google在一个函数中放入2万个变量,引发Firefox大崩溃
- 离职两年后,程序员遭前东家索赔:Bug 是你写的
- 40岁程序员谈修bug的心态问题
你对本文的反应是: