Выше мы видели, что вызовы функции print в потоках выполнения необходимо синхронизировать с помощью блокировок, чтобы избежать смешивания выводимых данных, потому что стандартный поток вывода совместно используется всеми потоками выполнения. Строго говоря, потоки выполнения должны синхронизировать любые операции изменения совместно используемых объектов и переменных. В зависимости от целей программы в число этих объектов могут входить:
• Изменяемые объекты в памяти (объекты, ссылки на которые передаются потокам или приобретаются каким-то иным способом, продолжительность существования которых превышает время работы потоков)
• Переменные в глобальной области видимости (изменяемые переменные, объявленные за пределами функций и классов, выполняемых в потоках)
• Содержимое модулей (для каждого модуля существует всего одна копия записи в системной таблице модулей)
Даже при работе с простыми глобальными переменными может потребоваться координация действий, если есть вероятность одновременных попыток их изменения, как показано в примере 5.12.
Пример 5.12. PP4E\System\Threads\thread-add-random.py
“выводит различные результаты при каждом запуске под Windows 7”
import threading, time
count = 0
def adder():
global count
count = count + 1 # изменяет глобальную переменную
time.sleep(0.5) # потоки выполнения совместно используют count = count + 1 # глобальные объекты и переменные
threads = []
for i in range(100):
thread = threading.Thread(target=adder, args=())
thread.start()
threads.append(thread)
for thread in threads: thread.join() print(count)
Этот пример порождает 100 потоков выполнения, каждый из которых дважды изменяет одну и ту же глобальную переменную (с задержкой между ними, чтобы обеспечить чередование операций в различных потоках). При каждом запуске в Windows 7 этот сценарий будет воспроизводить различные результаты:
C:\…\PP4E\System\Threads> thread-add-random.py
189
C:\…\PP4E\System\Threads> thread-add-random.py
200
C:\…\PP4E\System\Threads> thread-add-random.py 194
C:\…\PP4E\System\Threads> thread-add-random.py 191
Это объясняется тем, что потоки выполнения произвольно перекрываются друг с другом по времени: интерпретатор не гарантирует, что инструкции — даже такие простые инструкции присваивания, как в данном примере, — будут выполнены полностью до того, как управление перейдет другому потоку выполнения (то есть они не являются атомарными). Когда один поток изменяет значение глобальной переменной, он может получить промежуточный результат, произведенный другим потоком. Как следствие этого мы наблюдаем непредсказуемое поведение. Чтобы заставить этот сценарий работать корректно, необходимо снова воспользоваться блокировками для синхронизации изменений — в какой бы момент мы ни запускали сценарий из примера 5.13, он всегда будет выводить число 200.
Пример 5.13. PP4E\System\Threads\thread-add-synch.py
“всегда выводит 200 — благодаря синхронизации доступа к глобальному ресурсу”
import threading, time count = 0
def adder(addlock): # совместно используемый объект блокировки
global count with addlock: # блокировка приобретается/освобождается
count = count + 1 # автоматически
time.sleep(0.5)
with addlock: # в каждый конкретный момент времени
count = count + 1 # только 1 поток может изменить значение переменной
addlock = threading.Lock() threads = [] for i in range(100):
thread = threading.Thread(target=adder, args=(addlock,)) thread.start()
threads.append(thread)
for thread in threads: thread.join() print(count)
Некоторые простейшие операции в языке Python являются атомарными и не требуют синхронизации, тем не менее лучше все-таки предусматривать координацию потоков выполнения, если есть вероятность одновременных попыток изменения значения. Со временем может измениться не только набор атомарных операций, но и внутренняя реализация механизма потоков выполнения (такие изменения ожидаются в версии Python 3.2, как описывается далее).
Конечно, это во многом искусственный пример (порождать 100 потоков выполнения, чтобы в каждом из них дважды увеличить счетчик, — это определенно не самый практичный случай использования потоков!), но он наглядно иллюстрирует проблемы, с которыми можно столкнуться, когда существует вероятность параллельного изменения объекта или переменной, совместно используемой потоками. К счастью, для многих, если не для большинства применений, модуль queue, описываемый в следующем разделе, способен обеспечить автоматическую синхронизацию потоков выполнения.
Прежде чем двинуться дальше, я должен отметить, что помимо классов Thread и Lock в модуле threading имеются и другие высокоуровневые инструменты синхронизации доступа к совместно используемым объектам (например, Semaphore, Condition, Event) — много больше, чем позволяет вместить объем этой книги, поэтому за дополнительными подробностями обращайтесь к руководству по библиотеке. Дополнительные примеры использования потоков выполнения и дочерних процессов вы найдете в оставшейся части этой главы, а также среди примеров в разделах книги, посвященных реализации графического интерфейса и сетевых взаимодействий. В графических интерфейсах, например, мы будем использовать потоки, чтобы избежать их блокирования. Мы также будем порождать потоки и дочерние процессы в сетевых серверах, чтобы исключить вероятность отказа в обслуживании клиентов.
Кроме того, мы будем исследовать приемы использования модуля threading для завершения программы без применения метода join, но в соединении с очередями — которые являются темой следующего раздела.
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, I том, 2011