本文同步发布于字节谈云公众号。背景前段时间,旧款MacBook已经到了报废的年龄。听了朋友的安利,换了一台基于苹果M1的MacBook。跑项目的时候,发现本来可以正常执行的任务,在新笔记本上还在进行中。bug调试之旅。初步探索有如下信息:Python版本为2.7.10+,eventlet版本为v0.21.0。导致这个问题的直接原因是代码中使用了eventlet.sleep。一旦当前协程执行到sleep语句,切出协程后eventlet不会切换回来。使用py-spy观察流程,发现形状如下调用信息:%Own%TotalOwnTimeTotalTimeFunction(filename:line)0.00%0.00%0.010s0.010srun(eventlet/hubs/hub.py:334)可以初步判断是sleep的问题,因为项目逻辑太复杂,不方便debug,不如自己写个最小的测试代码来验证一下猜想。重现问题既然我们怀疑问题出在sleep,又认为eventlet在sleep之后不会切换协程,那么我们不妨在一个协程池中开启两个协程,每个协程输出和sleep不断,那么就有如下代码:importeventletdefecho(name):whileTrue:print(name)eventlet.sleep(2)defmain():p=eventlet.GreenPool()p.spawn(echo,'foo')p.spawn(echo,'bar')p.waitall()if__name__=='__main__':main()运行后,依次输出foo和bar后,停止输出。调试分析既然输出了foo和bar,说明第一个echo协程输出foo进入sleep后,eventlet的调度没有阻塞,所以eventlet可以调度第二个echo协程输出bar。但是为什么两个协程在sleep之后不会再被调度呢?由于我们通过py-spy监控了进程调用栈,发现eventlet/hubs/hub.py被调用了,所以不妨在这个文件中查看和调试运行逻辑。defrun(self,*a,**kw):"""Runrunloopuntilabortiscalled."""#acceptanddiscardvariableargumentsbecausetheywillbesuppliedifothergreenletshaverunandexitedbeforethehub'sgreenlet有机会运行ifself.running:raiseRuntimeError("Alreadyrunning!")try:self.running=Trueself.stopping=Falsewhilenotself.stopping:whileself.closed:#Weditchallthesefirst.self.close_one()self.prepare_timers()如果self.debug_blocking:self.block_detect_pre()self.fire_timers(self.clock())如果self.debug_blocking:self.block_detect_post()self.prepare_timers()wakeup_when=self.sleep_until()如果wakeup_when为None:sleep_time=self.default_sleep()else:sleep_time=wakeup_when-self.clock()ifsleep_time>0:self.wait(sleep_time)else:self.wait(0)else:self.timers_canceled=0delself.timers[:]delself.next_timers[:]finally:self.running=Falseself.stopping=False这段代码中可以看到sleep的相关处理(第23~31行),获取协程被唤醒的时间减去当前时间,如果超过0则表示还是需要等待一段时间,不然唤醒实测发现是self.clock()返回的时钟值有问题。这里的时钟是eventlet/support/monotonic.py中的monotonic()函数,用来返回单调递增的时钟数(每递增1表示1秒)。此功能在macOS上的实现如下:ifsys.platform=='darwin':#OSX,iOS#参见Mac开发者库的技术问答QA1398:#
