Поскольку базовый модуль _thread немного проще, чем более мощный модуль threading, о котором рассказывается далее в этом разделе, начнем с рассмотрения его интерфейсов. Этот модуль предоставляет переносимый интерфейс к любой системе потоков выполнения, имеющейся на вашей платформе: его интерфейсы одинаково работают в Windows, Solaris, SGI и любой другой системе, где установлена реализация pthreads потоков POSIX (включая Linux). Сценарии на языке Python, использующие модуль _thread, будут работать на всех этих платформах без внесения каких-либо изменений в исходный программный код.
Основы использования
Для начала поэкспериментируем со сценарием, демонстрирующим применение основных интерфейсов механизма потоков выполнения. Сценарий в примере 5.5 порождает потоки выполнения, пока в консоли не будет нажата клавиша q, и напоминает по духу (будучи немного проще) сценарий в примере 5.1; но он запускает параллельно потоки, а не дочерние процессы.
Пример 5.5. PP4E\System\Threads\thread1.py
“порождает потоки выполнения, пока не будет нажата клавиша ‘q’”
import _thread
def child(tid):
print(‘Hello from thread’, tid)
def parent(): i = 0 while True: i += 1 _thread.start_new_thread(child, (i,)) if input() == ‘q’: break
parent()
В действительности в этом сценарии только две строки имеют отношение к потокам выполнения: инструкция импортирования модуля _thread и вызов функции, создающей поток. Чтобы запустить новый поток выполнения, достаточно просто вызвать функцию _thread.start_new_thread, независимо от того, на какой платформе выполняется программа.[XV] Эта функция принимает функцию (или другой вызываемый объект) и кортеж аргументов, и запускает новый поток выполнения, в котором будет вызвана указанная функция с переданными аргументами. Это очень похоже на синтаксис вызова function(*args) — и тут, и там принимается необязательный словарь именованных аргументов, — но в данном случае функция начинает выполняться параллельно основной программе.
Сама функция _thread.start_new_thread сразу же возвращает управление вызывающей, не возвращая какого-либо полезного значения, а порож
денный ею поток тихо завершается, когда происходит возврат из выполняемой функции (значение, возвращаемое функцией, выполняемой в потоке, просто игнорируется). Кроме того, если выполняемая в потоке функция возбудит исключение, интерпретатор выведет трассировочную информацию и завершит работу потока, но остальная программа продолжит работу. На большинстве платформ при использовании модуля _thread вся программа завершит работу без вывода каких-либо сообщений, когда завершится главный поток (однако, как будет показано далее, при использовании модуля threading может потребоваться предпринять дополнительные действия, если дочерние потоки к этому моменту еще продолжают выполняться).
На практике, однако, использование потоков выполнения в сценариях на языке Python почти тривиально. Запустим эту программу и позволим ей породить несколько новых потоков. На этот раз ее можно выполнять как в Unix-подобных системах, так и в Windows, потому что потоки переносятся лучше, чем ветвление процессов. Ниже приводится пример порождения потоков в Windows:
C:\…\PP4E\System\Threads> python thread1.py
Hello from thread 1
Hello from thread 2
Hello from thread 3
Hello from thread 4 q
Здесь каждое сообщение выводится новым потоком выполнения, который завершается почти сразу после запуска.
Другие способы реализации потоков с помощью модуля _thread
В предыдущем примере сценарий запускает простую функцию, тем не менее в отдельном потоке выполнения можно запустить любой вызываемый объект, благодаря тому что все потоки выполняются в рамках одного и того же процесса. Например, в отдельном потоке можно запустить lambda-функцию или связанный метод объекта (ниже приводится фрагмент сценария thread—alts.py, входящего в состав пакета с примерами к книге):
import _thread # во всех 3 случаях
выводится 4294967296
def action(i): print(i ** 32)
def __init__(self, i)
self.i = i
def action(self): # связанный метод
print(self.i ** 32)
_thread.start_new_thread(action, (2,)) # запуск простой функции
_thread.start_new_thread((lambda: action(2)), ()) # запуск lambda—функции
obj = Power(2)
_thread.start_new_thread(obj.action, ()) # запуск связанного метода
Как будет показано далее в книге, в более крупных примерах, в этой роли особенно полезными оказываются связанные методы — так как они хранят в себе и ссылку на функцию, и ссылку на экземпляр объекта, то они обладают доступом к информации о состоянии и методам класса, которые могут использовать в процессе выполнения внутри потока.
Если смотреть глубже — так как все потоки выполняются в рамках одного и того же процесса, то связанные методы, выполняемые в отдельных потоках, имеют доступ к оригинальному экземпляру объекта, а не к его копии. Следовательно, любые изменения, выполненные в потоке, автоматически будут видимы для всех остальных потоков. Кроме того, связанные методы экземпляров классов, как вызываемые объекты, могут использоваться вместо простых функций, поэтому использование их в потоках выполнения не влечет никаких сложностей. И, как будет показано далее, тот факт, что они являются обычными объектами, позволяет сохранять их в общедоступных очередях.
Запуск нескольких потоков
По-настоящему ощутить всю мощь параллельно выполняющихся потоков можно, только если реализовать в них выполнение продолжительных операций, как мы делали это выше для процессов. Изменим программу fork—count из предыдущего раздела так, чтобы в ней использовались потоки выполнения. В сценарии из примера 5.6 запускается 5 экземпляров функции counter, которые выполняются параллельно в отдельных потоках.
Пример 5.6. PP4E\System\Threads\thread-count.py
основы потоков: запускает 5 копий функции в параллельных потоках; функция time. sleep используется, чтобы главный поток не завершился слишком рано, так как на некоторых платформах это приведет к завершению остальных потоков выполнения; поток вывода stdout — общий: результаты, выводимые потоками выполнения, в этой версии могут перемешиваться произвольным образом.
import _thread as thread, time
def counter(myId, count): # эта функция выполняется в потоках
for i in range(count):
time.sleep(1) # имитировать работу
print(‘[%s] => %s’ % (myId, i))
for i in range(5): # породить 5 потоков выполнения
thread.start_new_thread(counter, (i, 5)) # каждый поток выполняет 5 циклов
time.sleep(6)
print(‘Main thread exiting.’) # задержать выход из программы
Каждая параллельно выполняющаяся копия функции counter просто считает здесь от нуля до четырех и при каждом увеличении счетчика выводит сообщение в поток стандартного вывода.
Обратите внимание, что в самом конце этот сценарий приостанавливается на 6 секунд. В Windows и в Linux, как было проверено, главный поток не должен завершаться, пока все порожденные потоки не закончили работу, если важно, чтобы они доработали. Если главный поток завершится раньше, все порожденные потоки будут немедленно завершены. Этим потоки выполнения отличаются от процессов, где дочерние процессы продолжают работать после завершения родительского процесса. Если убрать вызов функции sleep в конце сценария, порожденные потоки выполнения будут немедленно завершены, практически сразу же после их запуска.
Может показаться, что так сделано специально, но это необходимо не на всех платформах, и программы обычно реализованы так, чтобы главный поток выполнения продолжал работать столько же, сколько потоки, им запущенные. Например, интерфейс пользователя может начать загрузку файла по протоколу FTP в потоке, но продолжительность операции загрузки значительно короче, чем время жизни самого интерфейса пользователя. Далее в этом разделе мы увидим, как различными способами можно избежать этой паузы с помощью глобальных блокировок и флагов, позволяющих потокам выполнения сигнализировать о своем завершении.
Кроме того, далее мы узнаем, что модуль threading предоставляет метод join, который позволяет дождаться завершения порожденных потоков и не дает программе завершиться до того, пока хотя бы один обычный поток выполнения продолжает работу (что было бы полезно в данном случае, но в других случаях может потребовать выполнения дополнительных операций по принудительному завершению потоков). Пакет multiprocessing, с которым мы встретимся далее в этой главе, также позволяет потомкам продолжать работу после завершения родителя, но это в значительной степени объясняется использованием модели процессов.
Если теперь запустить сценарий из примера 5.6 в Windows 7 под управлением Python 3.1, он выведет:
C:\…\PP4E\System\Threads> python thread-count.py
[1] => 0
[1] => 0
[0] => 0
[1] => 0
[0] => 0
[2] => 0
[3] => 0
[4] => 0
[1] => 1
[3] => 1
[3] => 1
[0] => 1[2] => 1
[3] => 1
[0] => 1[2] => 1
[4] => 1
[1] => 2
[3] => 2[4] => 2
[3] => 2[4] => 2
[0] => 2
[3] => 2[4] => 2
[0] => 2
[2] => 2
[3] => 2[4] => 2
[0] => 2
[2] => 2
…часть вывода опущена…
Main thread exiting.
Полученные результаты, возможно, покажутся вам странными, но так они и должны выглядеть. Данный пример демонстрирует один из наиболее необычных аспектов потоков выполнения. В этом примере результаты 5 потоков, действующих параллельно, перемешались между собой. Поскольку все потоки выполняются в рамках одного и того же процесса, все они совместно используют один и тот же поток стандартного вывода (в терминах языка Python они совместно используют файл sys.stdout, куда выводит текст функция print). В результате вывод потоков выполнения может перемешиваться произвольно. На практике при каждом запуске этого сценария могут быть получены разные результаты. В Python 3 перемешивание вывода стало еще более явным, что, вероятно, обусловлено новой реализацией вывода в файлы.
Из этого следует важный вывод: когда несколько потоков выполнения могут совместно использовать некоторый ресурс, как в данном примере, операции доступа в них должны синхронизироваться, чтобы избежать перекрытия во времени, а как — будет описано в следующем разделе.
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, I том, 2011