Помимо устранения конфликтов при выводе данных, блокировки модуля потоков имеют и другие очень полезные применения. Они могут использоваться в качестве основы парадигм синхронизации более высокого уровня (например, семафоров) и использоваться как универсальные инструменты взаимодействий между потоками.[XVI] В частности, в примере 5.8 глобальный список блокировок позволяет установить окончание работы всех дочерних потоков.
Пример 5.8. PP4E\System\Threads\thread-count-wait1.py
использование мьютексов в родительском/главном потоке выполнения для определения момента завершения дочерних потоков, взамен time.sleep; блокирует stdout, чтобы избежать конфликтов при выводе;
import _thread as thread
stdoutmutex = thread.allocate_lock()
exitmutexes = [thread.allocate_lock() for i in range(10)]
def counter(myId, count):
for i in range(count):
stdoutmutex.acquire()
print(‘[%s] => %s’ % (myId, i))
stdoutmutex.release()
exitmutexes[myId].acquire() # сигнал главному потоку
for i in range(10):
thread.start_new_thread(counter, (i, 100))
for mutex in exitmutexes:
while not mutex.locked(): pass
print(‘Main thread exiting.’)
Для проверки состояния блокировки можно использовать ее метод locked. Главный поток создает по одной блокировке для каждого дочернего потока, помещая их в глобальный список exitmutexes (не забывайте, что функция потока использует глобальную область совместно с главным потоком). По завершении каждый поток приобретает свою блокировку в списке, а главный поток просто ждет, когда будут приобретены все блокировки. Это значительно более точный подход, чем просто приостанавливать работу на определенное время, пока выполняются дочерние потоки, в надежде обнаружить после возобновления, что все они будут завершены.
В зависимости от операций, выполняемых в потоках, все это можно организовать еще проще: поскольку потоки в любом случае совместно используют глобальную память, того же результата можно добиться с помощью простого глобального списка целых чисел, а не блокировок. В примере 5.9 пространство имен модуля (область видимости), как и прежде, совместно используется программным кодом верхнего уровня и функцией, выполняемой в потоке. Имя exitmutexes ссылается на один и тот же объект списка в главном потоке и во всех порождаемых потоках. По этой причине изменения, производимые в потоке, видны в главном потоке без использования лишних блокировок.
Пример 5.9. PP4E\System\Threads\thread-count-wait2.py
использование простых глобальных данных (не мьютексов) для определения момента завершения всех потоков в родительском/главном потоке; потоки совместно используют список, но не его элементы, при этом предполагается, что после создания список не будет перемещаться в памяти
import _thread as thread
stdoutmutex = thread.allocate_lock()
exitmutexes = [False] * 10
def counter(myId, count): for i in range(count): stdoutmutex.acquire() print(‘[%s] => %s’ % (myId, i)) stdoutmutex.release()
exitmutexes[myId] = True # сигнал главному потоку
for i in range(10):
thread.start_new_thread(counter, (i, 100))
while False in exitmutexes: pass print(‘Main thread exiting.’)
Вывод этого сценария похож на вывод предыдущего — 10 потоков параллельно ведут счет до 100 и в процессе работы синхронизируют свои обращения к функции print. Фактически оба последних сценария с потоками-счетчиками производят вывод, в общем аналогичный первоначальному сценарию thread_count.py, но данные при выводе в stdout не повреждаются, значения счетчиков больше и отличается случайный порядок вывода строк. Основное отличие состоит в том, что главный поток завершает работу сразу после (и не раньше!) порожденных дочерних потоков:
C:\…\PP4E\System\Threads> python thread-count-wait2.py
…часть вывода удалена…
[4] => 98
[6] => 98
[8] => 98
[5] => 98
[0] => 99
[7] => 98
[9] => 98
[1] => 99
[3] => 99
[2] => 99
[4] => 99
[6] => 99
[8] => 99
[5] => 99
[7] => 99
[9] => 99
Main thread exiting.
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, I том, 2011