【问题标题】:Why does Python's math.factorial not play nice with threads?为什么 Python 的 math.factorial 不能很好地处理线程?
【发布时间】:2012-04-06 12:14:42
【问题描述】:

为什么 math.factorial 在线程中表现得如此怪异?

这里是一个例子,它创建了三个线程:

  • 只是休眠一段时间的线程
  • 在一段时间内递增 int 的线程
  • 对大量数执行 math.factorial 的线程。

它在线程上调用start,然后在超时后调用join

睡眠和自旋线程按预期工作并立即从start 返回,然后在join 中等待超时。

另一方面,阶乘线程直到运行到最后才从start返回!

import sys
from threading import Thread
from time import sleep, time
from math import factorial

# Helper class that stores a start time to compare to
class timed_thread(Thread):
    def __init__(self, time_start):
        Thread.__init__(self)
        self.time_start = time_start

# Thread that just executes sleep()
class sleep_thread(timed_thread):
    def run(self):
        sleep(15)
        print "st DONE:\t%f" % (time() - time_start)

# Thread that increments a number for a while       
class spin_thread(timed_thread):
    def run(self):
        x = 1
        while x < 120000000:
            x += 1
        print "sp DONE:\t%f" % (time() - time_start)

# Thread that calls math.factorial with a large number
class factorial_thread(timed_thread):
    def run(self):
        factorial(50000)
        print "ft DONE:\t%f" % (time() - time_start)

# the tests

print
print "sleep_thread test"
time_start = time()

st = sleep_thread(time_start)
st.start()
print "st.start:\t%f" % (time() - time_start)
st.join(2)
print "st.join:\t%f" % (time() - time_start)
print "sleep alive:\t%r" % st.isAlive()


print
print "spin_thread test"
time_start = time()

sp = spin_thread(time_start)
sp.start()
print "sp.start:\t%f" % (time() - time_start)
sp.join(2)
print "sp.join:\t%f" % (time() - time_start)
print "sp alive:\t%r" % sp.isAlive()

print
print "factorial_thread test"
time_start = time()

ft = factorial_thread(time_start)
ft.start()
print "ft.start:\t%f" % (time() - time_start)
ft.join(2)
print "ft.join:\t%f" % (time() - time_start)
print "ft alive:\t%r" % ft.isAlive()

这是 CentOS x64 上 Python 2.6.5 的输出:

sleep_thread test
st.start:       0.000675
st.join:        2.006963
sleep alive:    True

spin_thread test
sp.start:       0.000595
sp.join:        2.010066
sp alive:       True

factorial_thread test
ft DONE:        4.475453
ft.start:       4.475589
ft.join:        4.475615
ft alive:       False
st DONE:        10.994519
sp DONE:        12.054668

我已经在 CentOS x64 上的 python 2.6.5 和 Windows x86 上的 2.7.2 上尝试过这个,并且阶乘线程不会从其中任何一个上的开始返回,直到线程完成执行。

我也在 Windows x86 上使用 PyPy 1.8.0 进行了尝试,结果略有不同。开始确实立即返回,但随后加入不会超时!

sleep_thread test
st.start:       0.001000
st.join:        2.001000
sleep alive:    True

spin_thread test
sp.start:       0.000000
sp DONE:        0.197000
sp.join:        0.236000
sp alive:       False

factorial_thread test
ft.start:       0.032000
ft DONE:        9.011000
ft.join:        9.012000
ft alive:       False
st DONE:        12.763000

也试过 IronPython 2.7.1,它产生了预期的结果。

sleep_thread test
st.start:       0.023003
st.join:        2.028122
sleep alive:    True

spin_thread test
sp.start:       0.003014
sp.join:        2.003128
sp alive:       True

factorial_thread test
ft.start:       0.002991
ft.join:        2.004105
ft alive:       True
ft DONE:        5.199295
sp DONE:        5.734322
st DONE:        10.998619

【问题讨论】:

    标签: python multithreading blocking factorial gil


    【解决方案1】:

    由于Global Interpreter Lock,线程通常只允许在 Python 中交错不同的事情,而不是同时发生不同的事情。

    如果你查看 Python 字节码:

    from math import factorial
    
    def fac_test(x):
        factorial(x)
    
    import dis
    dis.dis(fac_test)
    

    你得到:

      4           0 LOAD_GLOBAL              0 (factorial)
                  3 LOAD_FAST                0 (x)
                  6 CALL_FUNCTION            1
                  9 POP_TOP             
                 10 LOAD_CONST               0 (None)
                 13 RETURN_VALUE        
    

    如您所见,对 math.factorial 的调用是 Python 字节码级别 (6 CALL_FUNCTION) 的单个操作——它是在 C 中实现的。factorial 由于工作类型而没有发布 GIL确实如此(请参阅我的答案中的 cmets),因此 Python 在运行时不会切换到其他线程,并且您会得到观察到的结果。

    【讨论】:

    • “因此,Python 在运行时不会切换到其他线程……”不,这不是原因。
    • 问题不在于函数调用是单个字节码——许多允许其他线程执行的事情都发生在单个字节码中。问题是 math.factorial 函数没有释放全局解释器锁,因为它所做的所有工作都涉及 PyObjects,这意味着它必须有 GIL 才能做任何事情。可以想象,它可以释放并重新获取 GIL,尽管它没有。
    • @ThomasWouters 我稍微修改了我的答案;我过于简单化了。
    • 哇,这很有趣。更好的工作 PyPy 怎么样?他们的阶乘实现都是 python bitbucket.org/pypy/pypy/src/2346207d9946/pypy/module/math/…,start() 很好,但可以防止 join 超时。
    • @W1N9Zr0 您应该将其作为一个单独的问题提出。我不知道为什么会发生这种情况——可能需要很长时间吗?只是没有加入的阶乘吗?尝试一个较小的数字。
    【解决方案2】:

    Python 有一个全局解释器锁 (GIL),它要求 CPU 绑定的线程轮流运行,而不是并发运行。由于阶乘函数是用 C 编写的并且不会释放 GIL,因此即使设置 sys.setswitchinterval 也不足以让线程协作。

    multiprocessing 模块提供 Process 对象,这些对象类似于线程,但在不同的地址空间中工作。对于 CPU 密集型任务,您应该强烈考虑使用 multiprocessing 模块。

    【讨论】:

      猜你喜欢
      • 2012-06-01
      • 1970-01-01
      • 1970-01-01
      • 2012-02-06
      • 2011-12-24
      • 1970-01-01
      • 2017-03-29
      • 2014-08-17
      • 1970-01-01
      相关资源
      最近更新 更多