Python的threading模块有一个比较严重的bug:那就是可能会让线程的等待提前结束或者延迟,具体的原因是因为线程的wait操作判断超时时依赖于实时时间,即通过time.time()获取到的时候,为了显示这个问题,请看下面的例子:



[python] view plaincopy在CODE上查看代码片派生到我的代码片

  1. from threading import Thread  

  2. from threading import Event  

  3. import time  

  4. e = Event()  

  5. stop = False  

  6. class MyThread(Thread):  

  7.     def __init__(self):  

  8.         super(MyThread,self).__init__()  

  9.     def run(self):  

  10.         counter = 0  

  11.         while not stop:  

  12.             time.sleep(1.0)  

  13.             counter += 1  

  14.             print counter  

  15.             if counter >= 60:  

  16.                 break  

  17.   

  18. t = MyThread()  

  19. t.start()  

  20. e.wait(60)  

  21. stop = True  

  22. print 'done!'  





这段代码创建一个计时的线程,每1秒打印一次计数,线程总共等待60秒,然后结束。为了更好的说明问题,代码并没有用Thread.join来等待,而是用Event.wait来等待,

(其实通过下面的代码分析,可以知道,Event.wait和Thread.join的等待都是调用的Condition.wait,所以情况是一样的)

按照正常情况,一段程序会打印60次计数,然后结束。

现在,把系统的时候调前60秒或者调后60秒,你会发现一个很有意思的情况:

1、当系统调前60秒时,计时打印输出会立即停止,等待会立即结束,并输出done!

2、当系统的时间被调后60秒时,计时打印输出也会立即停止,过60秒后才恢复打印,总共要等待120秒程序才会结束。


然后你可以想到,当系统的时候改变被调后超过一小时,二小时时,甚至一天,二天时,就会发生死锁的情况(其实这里的死锁也不太准确,

系统在等待超过改变的时间后还是会返回的,但是这种长时间的等待几乎可以认可为死锁了)


好,说明了问题,接下来就要来说说这个问题有多严重了。也许有人会说这个问题出现的概率几乎为零,现在的机器的时间一般都是很准的,不会出现大的时间调整。


但是凡事都有例外,例如你的机器的给rtc供电的钮扣电池没电了,这时候会在改变时间的时候出现比较大的时间变化,

再如有些设备上根本就没有rtc,在开机的时候通过NTP服务器来获取时间等等 ,但是这两种情况都是属于把时间调前的情况,只会让等待提前结束,这种情况的影响相对来说比较小。

那么什么情况下会产生时间被调后的情况呢,还是可能出在NTP服务器上,现在有些NTP服务器并不是非常的准确和权威的,像非常流行的免费的NTP服务器pool.ntp.org更是依赖于个人共享ip资源,一旦个人共享的电脑时间出现问题,就有可能出现时间不正确的情况。时间被调前和调后都有可能。


如果问题出现在终端设备可能影响会小些,但是如果代码是运行在服务端的,那就可能会是灾难性的,有可能会直接造成服务器的宕机。

关键这个问题很隐密,很难重现,所以,如果不知道问题之所在,查起来比较麻烦。

当然,这个问题还是被我给解决掉了,要不然也不会有这篇文章的产生。


好,说了这么一大堆废话,接下来我们来看下具体的原因:


找到threading.py,我把关键的代码帖出来:


[python] view plaincopy在CODE上查看代码片派生到我的代码片

  1. from time import time as _time, sleep as _sleep  



[python] view plaincopy在CODE上查看代码片派生到我的代码片

  1. class _Condition(_Verbose):  

  2. ...  

  3.     def wait(self, timeout=None):  

  4.         if not self._is_owned():  

  5.             raise RuntimeError("cannot wait on un-acquired lock")  

  6.         waiter = _allocate_lock()  

  7.         waiter.acquire()  

  8.         self.__waiters.append(waiter)  

  9.         saved_state = self._release_save()  

  10.         try:    # restore state no matter what (e.g., KeyboardInterrupt)  

  11.             if timeout is None:  

  12.                 waiter.acquire()  

  13.                 if __debug__:  

  14.                     self._note("%s.wait(): got it"self)  

  15.             else:  

  16.                 # Balancing act:  We can't afford a pure busy loop, so we  

  17.                 # have to sleep; but if we sleep the whole timeout time,  

  18.                 # we'll be unresponsive.  The scheme here sleeps very  

  19.                 # little at first, longer as time goes on, but never longer  

  20.                 # than 20 times per second (or the timeout time remaining).  

  21.                 endtime = _time() + timeout  

  22.                 delay = 0.0005 # 500 us -> initial delay of 1 ms  

  23.                 while True:  

  24.                     gotit = waiter.acquire(0)  

  25.                     if gotit:  

  26.                         break  

  27.                     remaining = endtime - _time()  

  28.                     if remaining <= 0:  

  29.                         break  

  30.                     delay = min(delay * 2, remaining, .05)  

  31.                     _sleep(delay)  

  32.                 if not gotit:  

  33.                     if __debug__:  

  34.                         self._note("%s.wait(%s): timed out"self, timeout)  

  35.                     try:  

  36.                         self.__waiters.remove(waiter)  

  37.                     except ValueError:  

  38.                         pass  

  39.                 else:  

  40.                     if __debug__:  

  41.                         self._note("%s.wait(%s): got it"self, timeout)  

  42.         finally:  

  43.             self._acquire_restore(saved_state)  

  44.   

  45.   

  46. def Condition(*args, **kwargs):  

  47.     return _Condition(*args, **kwargs)   

  48.   

  49. class _Event(_Verbose):  

  50.     def __init__(self, verbose=None):  

  51.         _Verbose.__init__(self, verbose)  

  52.         self.__cond = Condition(Lock())  

  53.       

  54. def Event(*args, **kwargs):  

  55.     return _Event(*args, **kwargs)  



问题就出在_Condition.wait上,

这个方法首先计算等待的结束时间,

endtime = _time() + timeout

然后不断的判断时间有没有到,如果没到,就等上delay*2的时间,每次等待最多0.05秒:

                while True:
                    gotit = waiter.acquire(0)
                    if gotit:
                        break
                    remaining = endtime - _time()
                    if remaining <= 0:
                        break

                    delay = min(delay * 2, remaining, .05)
                    _sleep(delay)

这里,如果在等待时时间发生了改变,就会出现等待提前结束或者延迟结束的问题。


另外,Thread.join里的等待也是调用的_Condition.wait,所以也会有这个问题,具体的看下面的代码:


[python] view plaincopy在CODE上查看代码片派生到我的代码片

  1. class Thread(_Verbose):  

  2.         self.__block = Condition(Lock())  

  3.     def join(self, timeout=None):  

  4. ...  

  5.         self.__block.acquire()  

  6.         try:  

  7.             if timeout is None:  

  8.                 while not self.__stopped:  

  9.                     self.__block.wait()  

  10.                 if __debug__:  

  11.                     self._note("%s.join(): thread stopped"self)  

  12.             else:  

  13.                 deadline = _time() + timeout  

  14.                 while not self.__stopped:  

  15.                     delay = deadline - _time()  

  16.                     if delay <= 0:  

  17.                         if __debug__:  

  18.                             self._note("%s.join(): timed out"self)  

  19.                         break  

  20.                     self.__block.wait(delay)  

  21.                 else:  

  22.                     if __debug__:  

  23.                         self._note("%s.join(): thread stopped"self)  

  24.         finally:  

  25.             self.__block.release()  






好,知道了问题,接下来看下怎么去解决:

这里也没有什么好卖关子的,解决的方法很简单,就是用开机时间来替代实时时间。

在linux下面,通过clock_gettime来获取开机时间,

在windows下面,通过GetTickCount来获取开机时间,代码如下:


startup_linux.pyx


[python] view plaincopy在CODE上查看代码片派生到我的代码片

  1. cdef extern from "time.h":  

  2.     enum:  

  3.         CLOCK_MONOTONIC  

  4.     struct timespec:  

  5.         int tv_sec  

  6.         long tv_nsec  

  7.     cdef int clock_gettime(int type,timespec* ts)  

  8.   

  9. def getboottime():  

  10.     cdef timespec ts  

  11.     clock_gettime(CLOCK_MONOTONIC,&ts)  

  12.     return <double>ts.tv_sec + ts.tv_nsec/1000000000.0  


startup_win32.pyx:




[python] view plaincopy在CODE上查看代码片派生到我的代码片

  1. cdef extern from "Windows.h":  

  2.     int GetTickCount()  

  3.   

  4. def getboottime():  

  5.     return GetTickCount()/1000.0  

  6.       



由于代码是用cython来写的,所以要用cython来编译下:


Setup.py


[python] view plaincopy在CODE上查看代码片派生到我的代码片

  1. from distutils.core import setup  

  2. from distutils.extension import Extension  

  3. from Cython.Build import cythonize  

  4.   

  5. import os  

  6. if os.name == 'nt':  

  7.     print 'build startup_win32'  

  8.     sources = ["startup_win32.pyx"]  

  9.     setup(  

  10.       name = 'startup_win32',  

  11.       ext_modules=cythonize([  

  12.         Extension("startup_win32", sources),  

  13.         ]),  

  14.     )  

  15. else:  

  16.     sources = ["startup_linux.pyx"]  

  17.     setup(  

  18.       name = 'startup',  

  19.       ext_modules=cythonize([  

  20.         Extension("startup_linux", sources,  

  21.         libraries = ['rt']  

  22.         ),  

  23.         ]),  

  24.     )  




接下来,看下怎么使用:

替换threading.py开始的如下代码:

from time import time as _time, sleep as _sleep

为:



[python] view plaincopy在CODE上查看代码片派生到我的代码片

  1. from time import sleep as _sleep  

  2.   

  3. import os  

  4. if os.name == 'nt':  

  5.     from startup_win32 import getboottime as _time  

  6. else:  

  7.     from startup_linux import getboottime as _time  




其它的代码都不用改。

当然,不只是threading这个模块有这个问题,系统的Queue模块也同样也有这个问题,解决方法和threading模块一样。