Почему Python math.factorial плохо работает с потоками?

Почему 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()

А вот вывод на Python 2.6.5 на CentOS x64:

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

Я пробовал это на python 2.6.5 в CentOS x64, 2.7.2 в Windows x86, и факториальный поток не возвращается с начала ни на одном из них, пока поток не завершится.

Я также пробовал это с PyPy 1.8.0 в Windows x86, и результат немного отличается. Начало действительно возвращается немедленно, но время соединения не истекает!

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

person W1N9Zr0    schedule 21.03.2012    source источник


Ответы (2)


Потоки часто позволяют чередовать разные вещи в 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 из-за типа выполняемой работы (см. комментарии к моему ответу ), поэтому Python не переключается на другие потоки во время работы, и вы получаете наблюдаемый результат.

person agf    schedule 21.03.2012
comment
Из-за этого Python не переключается на другие потоки во время работы... Нет, не поэтому. - person Ignacio Vazquez-Abrams; 22.03.2012
comment
Проблема не в том, что вызов функции является одним байт-кодом — многие вещи, которые позволяют выполняться другим потокам, происходят в одном байт-коде. Проблема в том, что функция math.factorial не освобождает глобальную блокировку интерпретатора, потому что вся работа, которую она выполняет, касается PyObjects, а это значит, что для выполнения каких-либо действий ей необходим GIL. Предположительно, он мог бы выпустить и повторно получить GIL, хотя этого не происходит. - person Thomas Wouters; 22.03.2012
comment
@ThomasWouters Я немного изменил свой ответ; Я упростил. - person agf; 22.03.2012
comment
Вау, это интересно. А как насчет немного лучше работающего PyPy? Их факториальная реализация полностью написана на python pypy/src/2346207d9946/pypy/module/math/, который start() работает нормально, но предотвращает истечение времени ожидания соединения. - person W1N9Zr0; 22.03.2012
comment
@ W1N9Zr0 Вы должны задать это как отдельный вопрос. Я не знаю, почему это произошло — может быть, это просто заняло очень много времени? Это просто факториал, который не соединяется? Попробуйте меньшее число. - person agf; 22.03.2012
comment
Реализация PyPy - это RPython, а не Python, он не запускает байт-коды и не выпускает GIL в этом коде. Я предполагаю, что он просто работает быстрее или GIL немного умнее, но на самом деле ничего особенного. - person fijal; 22.03.2012

Python имеет глобальную блокировку интерпретатора (GIL), которая требует, чтобы потоки, привязанные к ЦП, выполнялись по очереди, а не одновременно. Поскольку функция factorial написана на C и не освобождает GIL, даже установки sys.setswitchinterval недостаточно, чтобы разрешить взаимодействие потоков.

Модуль multiprocessing предоставляет объекты Process, которые похожи на потоки, но работают в отдельных адресных пространствах. Для задач, связанных с ЦП, вам следует настоятельно рассмотреть возможность использования модуля multiprocessing.

person amcnabb    schedule 21.03.2012