乐趣区

关于python:为什么-eventlet-在-Apple-M1-上卡住了

本文同步公布于字节话云公众号。

背景

前段时间老的 MacBook 到了退休的年纪,听了好友的安利换了基于 Apple M1 的 MacBook,在运行一个我的项目时发现本来能失常执行的工作在新笔记本上就始终是进行中,由此开展了一段 Bug 调试之旅。

初步摸索

有如下几点信息:

  • Python 版本为 2.7.10 +,eventlet 版本为 v0.21.0
  • 造成这个问题的间接起因是代码中应用了 eventlet.sleep,一旦以后协程执行到了 sleep 语句,eventlet 在切出协程后就不会再切回来
  • 应用 py-spy 察看过程发现,看到形如下方的调用信息:
%Own   %Total  OwnTime  TotalTime  Function (filename:line)
0.00%   0.00%   0.010s    0.010s   run (eventlet/hubs/hub.py:334)

能够初步断定问题出在 sleep 上,因为我的项目逻辑过于简单不便调试,不如写个最小化测试代码来验证猜测。

复现问题

既然狐疑问题出在 sleep 上,并且认为 eventlet 在 sleep 后不会切换协程,无妨在一个协程池中开两个协程,每个协程一直输入和 sleep,那么就有如下代码:

import eventlet


def echo(name):
    while True:
        print(name)
        eventlet.sleep(2)


def main():
    p = eventlet.GreenPool()
    p.spawn(echo, 'foo')
    p.spawn(echo, 'bar')
    p.waitall()


if __name__ == '__main__':
    main()

运行后,在顺次输入 foobar 后,就不再持续输入了。

调试和剖析

既然输入了 foobar,就阐明在执行第一个 echo 协程输入 foo 并进入 sleep 后并没阻塞 eventlet 的调度,所以 eventlet 能力调度到第二个 echo 协程输入 bar。然而为什么两个协程 sleep 后都不会再次被调度了?既然后面通过 py-spy 监控过程调用栈发现始终在调用 eventlet/hubs/hub.py,无妨查看并调试此文件中的 run 逻辑。

def run(self, *a, **kw):
    """Run the runloop until abort is called."""
    # accept and discard variable arguments because they will be
    # supplied if other greenlets have run and exited before the
    # hub's greenlet gets a chance to run
    if self.running:
        raise RuntimeError("Already running!")
    try:
        self.running = True
        self.stopping = False
        while not self.stopping:
            while self.closed:
                # We ditch all of these first.
                self.close_one()
            self.prepare_timers()
            if self.debug_blocking:
                self.block_detect_pre()
            self.fire_timers(self.clock())
            if self.debug_blocking:
                self.block_detect_post()
            self.prepare_timers()
            wakeup_when = self.sleep_until()
            if wakeup_when is None:
                sleep_time = self.default_sleep()
            else:
                sleep_time = wakeup_when - self.clock()
            if sleep_time > 0:
                self.wait(sleep_time)
            else:
                self.wait(0)
        else:
            self.timers_canceled = 0
            del self.timers[:]
            del self.next_timers[:]
    finally:
        self.running = False
        self.stopping = False

在这段代码中,咱们能够看到对 sleep 的相干解决(第 23\~31 行),获取协程要被唤醒的工夫并减去以后工夫,如果超过 0 阐明还须要等一段时间,反之就唤醒。实测发现 self.clock() 返回的时钟数值有问题。

此处的 clock 也就是 eventlet/support/monotonic.py 中的 monotonic() 函数,用于返回枯燥递增的时钟数(每加 1 即通过 1 秒)。此函数在 macOS 上,相干实现如下:

if sys.platform == 'darwin':  # OS X, iOS
    # See Technical Q&A QA1398 of the Mac Developer Library:
    #  <https://developer.apple.com/library/mac/qa/qa1398/>
    libc = ctypes.CDLL('/usr/lib/libc.dylib', use_errno=True)

    class mach_timebase_info_data_t(ctypes.Structure):
        """System timebase info. Defined in <mach/mach_time.h>."""
        _fields_ = (('numer', ctypes.c_uint32),
                    ('denom', ctypes.c_uint32))

    mach_absolute_time = libc.mach_absolute_time
    mach_absolute_time.restype = ctypes.c_uint64

    timebase = mach_timebase_info_data_t()
    libc.mach_timebase_info(ctypes.byref(timebase))
    ticks_per_second = timebase.numer / timebase.denom * 1.0e9

    def monotonic():
        """Monotonic clock, cannot go backward."""
        t = mach_absolute_time()
        return mach_absolute_time() / ticks_per_second

monotonic() 函数调用 mach/mach_time.hmach_absolute_time() 零碎函数并通过系数解决失去最终的时钟数,这里的解决形式和 mach_absolute_time() / timebase.numer * timebase.denom / 1.0e9 等价,然而这个算法是谬误的。

正确的表达式应该是 mach_absolute_time() * timebase.numer / timebase.denom / 1.0e9,可参照 monotonic.py 的实现。将此实现批改后,再运行测试代码,eventlet 就可能失常进行 sleep 和调度了。

为什么谬误的逻辑在 Intel 芯片上运行失常?

mach_timebase_info_data_t 中的 numerdenom 别离是时钟数缩放因子的分子和分母,在 Intel 芯片上固定都是 1。这也就意味着 number / denomdenom / number 的后果一样,换句话说,即便缩放因子表达式写反了,计算出的后果也不会有所变动。

但在 M1 芯片上,分子和分母并不相等,numer 是大于 denom 的,应用谬误的缩放因子 denom / number 会导致计算出的后果小于正确值,也就意味着实在工夫过来 1 秒时,计算出的工夫只过来 0.000x 秒,那么 eventlet 就会认为还没达到须要唤醒的工夫而持续期待,从而造成卡死的假象。

对于此问题的修复,eventletv0.24.0 版本中引入了第三方库 monotonic.py 解决了这个问题。

总结

应用 Apple M1 开发程序的真的都是壮士,因为 CPU 架构和 Intel 不同,极有可能呈现非预期的问题。一旦呈现问题,基本思路就是先察看景象,应用诸如 py-spy 的工具去进一步看看过程在做什么,而后尝试复现和调试,逐渐从下层到底层剖析代码,定位问题。

退出移动版