Синхронизация доступа к глобальным объектам и переменным

sinhronizaciya dostupa k globalnym obektam i peremennym Системные инструменты параллельного выполнения

Приятной особенностью потоков выполнения является наличие готового механизма обмена данными между заданиями: объектов и переменных процесса, совместно используемых потоками. Например, поскольку все потоки выполняются в одном и том же процессе, то при изменении глобальной переменной одним потоком это изменение видно всем другим потокам в процессе: и главному потоку, и дочерним. Точно так же потоки могут совместно использовать изменяемые объекты в памяти процесса, при условии, что они хранят ссылки на них (например, полученные в виде аргументов). Это упрощает возможность передачи информации между потоками программы — флаги завершения, объекты с результатами, индикаторы событий и так далее.

Недостатком такой схемы является то, что потоки должны следить за тем, чтобы не изменять глобальные объекты одновременно. Если два потока одновременно изменяют объект, может случиться так, что одно из двух изменений будет утрачено (или, что еще хуже, совместно используемый объект придет в полностью негодное состояние): один поток может приступить к выполнению операций, когда другой поток еще не завершил работу с объектом. Приводит ли это к ошибке, зависит от приложения; иногда проблем вообще не возникает.

Проблемы могут возникнуть и там, где этого совсем не ждешь. Например, файлы и потоки ввода-вывода совместно используются всеми потоками выполнения программы — если несколько потоков выполнения одновременно производят запись в один и тот же поток ввода-вывода, в последнем могут появиться перемежающиеся искаженные данные. Пример 5.6 из предыдущего раздела является простой, но показательной демонстрацией подобного рода конфликтов, которые могут происходить при параллельном выполнении нескольких потоков. Даже простейшие изменения могут пустить все вкривь и вкось, когда есть вероятность одновременного их выполнения. Чтобы исключить подобные ошибки, программы должны управлять доступом к глобальным объектам, чтобы в каждый конкретный момент времени только один поток выполнения мог использовать их.

К счастью, в модуле _thread имеются собственные простые в использовании инструменты синхронизации потоков, выполняющих операции с совместно используемыми объектами. Эти инструменты основаны на понятии блокировки — чтобы изменить совместно используемый объект, потоки приобретают блокировку, производят требуемые изменения и освобождают блокировку для использования в других потоках выполнения. Интерпретатор гарантирует, что в каждый конкретный момент времени только один поток выполнения будет владеть блокировкой, — если запрос на приобретение блокировки поступит в тот момент, когда она удерживается некоторым потоком, запросивший поток будет приостановлен до того момента, пока блокировка не будет освобождена.

Объекты блокировки размещаются в памяти, обрабатываются с помощью простых и переносимых функций из модуля _thread и автоматически отображаются на механизмы блокировки потоков, существующие на соответствующей платформе.

Так, в примере 5.7 с помощью функции _thread.allocate_lock создается объект блокировки, который приобретается и освобождается каждым потоком выполнения перед вызовом функции print, с помощью которой осуществляется вывод в совместно используемый стандартный поток вывода.

Пример 5.7. PP4E\System\Threads\thread-count-mutex.py

синхронизирует доступ к stdout: так как это общий глобальный объект, данные, которые выводятся из потоков выполнения, могут перемешиваться, если не синхронизировать операции

import _thread as thread, time

def counter(myId, count): # эта функция выполняется в потоках

for i in range(count):

time.sleep(1) # имитировать работу

mutex.acquire()

print(‘[%s] => %s’ % (myId, i)) # теперь работа функции print

# не будет прерываться mutex.release()

mutex = thread.allocate_lock() # создать объект блокировки

for i in range(5): # породить 5 потоков выполнения

thread.start_new_thread(counter, (i, 5)) # каждый поток выполняет 5 циклов

time.sleep(6)

print(‘Main thread exiting.’) # задержать выход из программы

В действительности этот сценарий является всего лишь расширенной версией примера 5.6, в которую была добавлена синхронизация обращений к функции print с применением блокировки. Благодаря этому никакие два потока выполнения в этом сценарии не смогут одновременно вызвать функцию print — блокировка гарантирует исключительный доступ к стандартному потоку вывода stdout. Таким образом, мы получаем вывод, сходный с выводом оригинальной версии, за исключением того, что текст на выходе никогда не будет перемешиваться из-за перекрывающихся операций вывода:

C:\\PP4E\System\Threads> thread-count-mutex.py

[0] => 0

[1]  => 0

[3]  => 0

[2]  => 0

[4]  => 0

[0] => 1

[1]  => 1

[3]  => 1

[2]  => 1

[4]  => 1

[0] => 2

[1] => 2

[3]  => 2

[4]  => 2

[5]  => 2

[0] => 3

[1]  => 3

[6]  => 3

[7]  => 3

[8]  => 3

[0] => 4

[1]  => 4

[9]  => 4

[10] => 4

[11] => 4

Main thread exiting

Порядок, в каком потоки выполнения выводят свои данные, зависит от платформы и по-прежнему может изменяться от запуска к запуску, потому что они выполняются параллельно (в конце концов, потоки выполнения как раз и предназначены для параллельной обработки данных). Но они больше не конфликтуют при выводе текста. Далее в этой главе мы увидим другие случаи использования блокировок — блокировки являются важной составляющей многопоточной модели выполнения.

Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, I том, 2011

Оцените статью
Секреты программирования
Добавить комментарий