Файл: Многопоточное программированиеВ этой главе.pdf

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 12.12.2023

Просмотров: 96

Скачиваний: 3

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

187 4.3. Поддержка потоков в языке Python
По умолчанию поддержка потоков включается при построении интерпретатора
Python из исходного кода (начиная с версии Python 2.0) или при установке исполняе- мой программы интерпретатора в среде Win32. Чтобы определить, предусмотрено ли применение потоков на конкретном установленном интерпретаторе, достаточно про- сто попытаться импортировать модуль thread из интерактивного интерпретатора, как показано ниже (если потоки доступны, то не появляется сообщение об ошибке).
>>> import thread
>>>
Если интерпретатор Python не был откомпилирован с включенными потоками, то попытка импорта модуля оканчивается неудачей:
>>> import thread
Traceback (innermost last):
File "", line 1, in ?
ImportError: No module named thread
В таких случаях может потребоваться повторно откомпилировать интерпретатор
Python, чтобы получить доступ к потокам. Для этого обычно достаточно вызвать сце- нарий configure с опцией --with-thread. Прочитайте файл README для применя- емого вами дистрибутива, чтобы ознакомиться с инструкциями, касающимися того, как откомпилировать исполняемую программу интерпретатора Python с поддерж- кой потоков для своей системы.
4.3.4. Организация программы без применения потоков
В первом ряде примеров для демонстрации работы потоков воспользуемся функ- цией time.sleep(). Функция time.sleep() принимает параметр в формате с пла- вающей запятой и приостанавливается (“засыпает”) на указанное количество секунд; иными словами, выполнение программы временно прекращается на заданное время.
Создадим два цикла во времени: приостанавливающийся на 4 секунды (функция loop0()) и на 2 секунды (функция loop1()) соответственно. (В данной программе имена loop0 и loop1 используются в качестве указания на то, что в конечном ито- ге будет создана последовательность циклов.) Если бы задача состояла в том, чтобы функции loop0() и loop1() выполнялись последовательно в однопроцессной или однопоточной программе, по аналогии со сценарием onethr.py в примере 4.1, то общее время выполнения составляло бы по меньшей мере 6 секунд. Между заверше- нием работы loop0() и запуском loop1() может быть предусмотрен промежуток в
1 секунду, кроме того, в ходе выполнения могут возникнуть другие задержки, поэто- му общая продолжительность работы программы может достичь 7 секунд.

Пример 4.1. Выполнение циклов в одном потоке (
onethr.py)
В этом сценарии два цикла выполняются последовательно в однопоточной про- грамме. Вначале должен быть завершен один цикл, чтобы мог начаться другой. Об- щее истекшее время представляет собой сумму значений времени, затраченных в ка- ждом цикле.
1 #!/usr/bin/env python
2 3 from time import sleep, ctime
4 06_ch04.indd 187 22.01.2015 22:00:37

Глава 4

Многопоточное программирование
188 5 def loop0():
6 print 'start loop 0 at:', ctime()
7 sleep(4)
8 print 'loop 0 done at:', ctime()
9 10 def loop1():
11 print 'start loop 1 at:', ctime()
12 sleep(2)
13 print 'loop 1 done at:', ctime()
14 15 def main():
16 print 'starting at:', ctime()
17 loop0()
18 loop1()
19 print 'all DONE at:', ctime()
20 21 if __name__ == '__main__':
22 main()
В этом можно убедиться, выполнив сценарий onethr.py и ознакомившись со сле- дующим выводом:
$ onethr.py starting at: Sun Aug 13 05:03:34 2006 start loop 0 at: Sun Aug 13 05:03:34 2006 loop 0 done at: Sun Aug 13 05:03:38 2006 start loop 1 at: Sun Aug 13 05:03:38 2006 loop 1 done at: Sun Aug 13 05:03:40 2006 all DONE at: Sun Aug 13 05:03:40 2006
Теперь предположим, что работа функций loop0() и loop1() не организована по принципу приостановки, а предусматривает выполнение отдельных и независимых вычислений, предназначенных для выработки общего решения. При этом не исклю- чена такая возможность, что выполнение этих функций можно осуществлять парал- лельно в целях сокращения общей продолжительности работы программы. В этом состоит идея, лежащая в основе многопоточного программирования, к рассмотре- нию которого мы теперь приступим.
4.3.5. Многопоточные модули Python
В языке Python предусмотрено несколько модулей, позволяющих упростить зада- чу многопоточного программирования, включая модули thread, threading и Queue.
Для создания потоков и управления ими программисты могут использовать моду- ли thread и threading. В модуле thread предусмотрены простые средства управле- ния потоками и блокировками, а модуль threading обеспечивает высокоуровневое, полноценное управление потоками. С помощью модуля Queue пользователи могут создать структуру данных очереди, совместно используемую несколькими потоками.
Рассмотрим эти модули отдельно и представим примеры и более крупные прило- жения.
Избегайте использования модуля
thread
Мы рекомендуем использовать высокоуровневый модуль threading вместо моду- ля thread по многим причинам. Модуль threading имеет более широкий набор
06_ch04.indd 188 22.01.2015 22:00:38


189 4.4. Модуль thread функций по сравнению с модулем thread, обеспечивает лучшую поддержку пото- ков, и в нем исключены некоторые конфликты атрибутов, обнаруживаемые в модуле thread. Еще одна причина отказаться от использования модуля thread состоит в том, что thread — это модуль более низкого уровня и имеет мало примитивов син- хронизации (фактически только один), в то время как модуль threading обеспечивает более широкую поддержку синхронизации.
Тем не менее мы представим некоторые примеры кода, в которых используется модуль thread, поскольку это будет способствовать изучению языка Python и многопоточ- ной организации программ в целом. Но эти примеры представлены исключительно в учебных целях, в надежде на то, что они позволят гораздо лучше понять обоснован- ность рекомендации, касающейся отказа от использования модуля thread. Мы также покажем, как использовать более удобные инструменты, предусмотренные в модулях
Queue и threading.
Еще одна причина отказа от работы с модулем thread состоит в том, что этот мо- дуль не позволяет взять под свое управление выход из процесса. После завершения основного потока происходит также уничтожение всех прочих потоков без предупреж- дения или надлежащей очистки памяти. Как было указано выше, модуль threading позволяет по меньшей мере дождаться завершения работы важных дочерних потоков и только после этого выйти из программы.
Использование модуля thread рекомендуется только для экспертов, которым требует- ся получить доступ к потоку на более низком уровне. Для того чтобы эта особенность модуля стала более очевидной, в Python 3 он был переименован в _thread. В любом создаваемом многопоточном приложении следует использовать threading, а также, возможно, другие высокоуровневые модули.
4.4. Модуль
thread
Вначале рассмотрим, какие задачи возлагались на модуль thread. От модуля thread требовалось не только порождать потоки, но и обеспечивать работу с основ- ной структурой синхронизации данных, называемой объектом блокировки (таковыми являются примитивная блокировка, простая блокировка, блокировка со взаимным исключением, мьютекс и двоичный семафор). Как было указано выше, без подобных примитивов синхронизации сложно обойтись при управлении потоками.
В табл. 4.1 приведен список наиболее широко используемых функций потока и методов объекта блокировки LockType.
Таблица 4.1. Модуль
thread и объекты блокировки
Функция/метод
Описание
Функции модуля
thread
start_new_thread(function,
args, kwargs=None
)
Порождает новый поток и вызывает на выполнение функцию function с заданными параметрами args и необязательными параметрами
kwargs
allocate_lock()
Распределяет объект блокировки
LockType exit()
Дает указание о выходе из потока
Методы объекта
LockType Lock
acquire(wait=None)
Предпринимает попытки захватить объект блокировки
06_ch04.indd 189 22.01.2015 22:00:38


Глава 4

Многопоточное программирование
190
Функция/метод
Описание
locked()
Возвращает
True, если блокировка захвачена, в противном слу- чае возвращает
False release()
Освобождает блокировку
Ключевой функцией модуля thread является start_new_thread(). Эта функция получает предназначенную для вызова функцию (объект) с позиционными параме- трами и (необязательно) с ключевыми параметрами. Специально для вызова функ- ции создается новый поток.
Возвратимся к примеру onethr.py, чтобы встроить в него многопоточную под- держку. В примере 4.2 представлен сценарий mtsleepA.py, в котором внесены неко- торые изменения в функции loop*():
Пример 4.2. Использование модуля
thread (mtsleepA.py)
Выполняются те же циклы, что и в сценарии onethr.py, но на этот раз с использо- ванием простого многопоточного механизма, предоставленного модулем thread. Эти два цикла выполняются одновременно (разумеется, не считая того, что менее про- должительный цикл завершается раньше), поэтому общие затраты времени опреде- ляются продолжительностью работы самого длительного потока, а не представляют собой сумму значений времени выполнения отдельно каждого цикла.
1 #!/usr/bin/env python
2 3 import thread
4 from time import sleep, ctime
5 6 def loop0():
7 print 'start loop 0 at:', ctime()
8 sleep(4)
9 print 'loop 0 done at:', ctime()
10 11 def loop1():
12 print 'start loop 1 at:', ctime()
13 sleep(2)
14 print 'loop 1 done at:', ctime()
15 16 def main():
17 print 'starting at:', ctime()
18 thread.start_new_thread(loop0, ())
19 thread.start_new_thread(loop1, ())
20 sleep(6)
21 print 'all DONE at:', ctime()
22 23 if __name__ == '__main__':
24 main()
Для функции start_new_thread() должны быть представлены по крайней мере первые два параметра, поэтому при ее вызове задан пустой кортеж, несмотря на то, что вызываемая на выполнение функция не требует параметров.
Окончание табл. 4.1
06_ch04.indd 190 22.01.2015 22:00:39

191 4.4. Модуль thread
Выполнение этой программы показывает, что данные на выходе существенно из- менились. Вместо полных затрат времени, составлявших 6 или 7 секунд, новый сце- нарий завершается в течение 4 секунд, что представляет собой продолжительность самого длинного цикла с добавлением небольших издержек.
$ mtsleepA.py starting at: Sun Aug 13 05:04:50 2006 start loop 0 at: Sun Aug 13 05:04:50 2006 start loop 1 at: Sun Aug 13 05:04:50 2006 loop 1 done at: Sun Aug 13 05:04:52 2006 loop 0 done at: Sun Aug 13 05:04:54 2006 all DONE at: Sun Aug 13 05:04:56 2006
Фрагменты кода, в которых происходит приостановка на 4 с и на 2 секунды, теперь начинают выполняться одновременно, внося свой вклад в отсчет минимального зна- чения полного времени прогона. Можно даже наблюдать за тем, как цикл 1 заверша- ется перед циклом 0.
Еще одним важным изменением в приложении является добавление вызова sleep(6). С чем связана необходимость такого добавления? Причина этого состоит в том, что если не будет установлен запрет на продолжение основного потока, то в нем произойдет переход к следующей инструкции, появится сообщение “all done” (рабо- та закончена) и работа программы завершится после уничтожения обоих потоков, в которых выполняются функции loop0() и loop1().
В сценарии отсутствует какой-либо код, который бы указывал основному потоку, что следует ожидать завершения дочерних потоков, прежде чем продолжить выпол- нение инструкций. Это — одна из ситуаций, которая показывает, что подразумевает- ся под утверждением, согласно которому для потоков требуется определенная син- хронизация. В данном случае в качестве механизма синхронизации применяется еще один вызов sleep(). При этом используется значение продолжительности приоста- новки, равное 6 секундам, поскольку известно, что оба потока (которые занимают 4 и
2 секунды) должны были завершиться до того, как в основном потоке будет отсчитан интервал времени 6 секунд.
Напрашивается вывод, что должен быть какой-то более удобный способ управле- ния потоками по сравнению с созданием дополнительной задержки в 6 секунд в ос- новном потоке. Дело в том, что из-за этой задержки общее время прогона ненамно- го лучше по сравнению с однопоточной версией. К тому же применение функции sleep() для синхронизации потоков, как в данном примере, не позволяет обеспечить полную надежность. Например, может оказаться, что синхронизируемые потоки яв- ляются независимыми друг от друга, а значения времени их выполнения изменяют- ся. В таком случае выход из основного потока может произойти слишком рано или слишком поздно. Как оказалось, гораздо лучшим способом синхронизации является применение блокировок.
В примере 4.3 показан сценарий mtsleepB.py, полученный в результате следую- щего обновления кода, в котором добавляются блокировки и исключается дополни- тельная функция установки задержки. Выполнение этого сценария показывает, что полученный вывод аналогичен выводу сценария mtsleepA.py. Единственное разли- чие состоит в том, что не пришлось устанавливать дополнительное время ожидания завершения работы, как в сценарии mtsleepA.py. С использованием блокировок мы получили возможность выйти из программы сразу после того, как оба потока завер- шили выполнение. При этом был получен следующий вывод:
06_ch04.indd 191 22.01.2015 22:00:39


Глава 4

Многопоточное программирование
192
$ mtsleepB.py starting at: Sun Aug 13 16:34:41 2006 start loop 0 at: Sun Aug 13 16:34:41 2006 start loop 1 at: Sun Aug 13 16:34:41 2006 loop 1 done at: Sun Aug 13 16:34:43 2006 loop 0 done at: Sun Aug 13 16:34:45 2006 all DONE at: Sun Aug 13 16:34:45 2006
Пример 4.3. Использование блокировок и модуля
thread (mtsleepB.py)
Очевидно, что гораздо удобнее использовать блокировки по сравнению с вызовом sleep() для задержки выполнения основного потока, как в сценарии mtsleepA.py.
1 #!/usr/bin/env python
2 3 import thread
4 from time import sleep, ctime
5 6 loops = [4,2]
7 8 def loop(nloop, nsec, lock):
9 print 'start loop', nloop, 'at:', ctime()
10 sleep(nsec)
11 print 'loop', nloop, 'done at:', ctime()
12 lock.release()
13 14 def main():
15 print 'starting at:', ctime()
16 locks = []
17 nloops = range(len(loops))
18 19 for i in nloops:
20 lock = thread.allocate_lock()
21 lock.acquire()
22 locks.append(lock)
23 24 for i in nloops:
25 thread.start_new_thread(loop,
26 (i, loops[i], locks[i]))
27 28 for i in nloops:
29 while locks[i].locked(): pass
30 31 print 'all DONE at:', ctime()
32 33 if __name__ == '__main__':
34 main()
Рассмотрим, как в данном случае организовано применение блокировок. Обра- тимся к исходному коду.
06_ch04.indd 192 22.01.2015 22:00:39

193 4.4. Модуль thread
Построчное объяснение
Строки 1–6
После начальной строки Unix располагаются инструкции импорта модуля thread и задания нескольких знакомых атрибутов модуля time. Вместо жесткого задания в коде отдельных функций для отсчета 4 и 2 секунд используется единственная функ- ция loop(), а константы, применяемые в ее вызове, задаются в списке loops.
Строки 8–12
Эта функция loop() действует в качестве замены исключенных из кода функций loop*(), которые были предусмотрены в предыдущих примерах. В функцию loop() пришлось внести несколько небольших изменений и дополнений, чтобы обеспечить выполнение этой функцией своего назначения с помощью блокировок. Одно из оче- видных изменений состоит в том, что циклы обозначены номерами, с которыми свя- зана продолжительность приостановки. Еще одним дополнением стало применение самой блокировки. Для каждого потока распределяется и захватывается блокировка.
По истечении времени, установленного функцией sleep(), соответствующая блоки- ровка освобождается, тем самым основному потоку передается указание, что данный дочерний поток завершен.
Строки 14–34
В этом сценарии значительная часть работы выполняется в функции main(), для чего применяются три отдельных цикла for. Вначале создается список блокировок, для получения которых используется функция thread.allocate_lock(), затем про- исходит захват каждой блокировки (отдельно) с помощью метода acquire(). Захват блокировки приводит к тому, что блокировка становится недоступной для дальней- шего манипулирования ею. После того как блокировка становится заблокированной, она добавляется к списку блокировок locks. Операция порождения потоков осу- ществляется в следующем цикле, после чего для каждого потока вызывается функция loop(), потоку присваивается номер цикла, задается продолжительность приоста- новки, и, наконец, для этого потока захватывается блокировка. Почему в данном слу- чае не происходит запуск потоков в цикле захвата блокировок? На это есть две при- чины. Во-первых, необходимо обеспечить синхронизацию потоков, чтобы все наши лошадки выскочили из ворот на беговую дорожку примерно в одно и то же время, и, во-вторых, приходится учитывать, что захват блокировок связано с определенными затратами времени. Если поток выполняет свою задачу слишком быстро, то может завершиться еще до того, как появится шанс захватить блокировку.
Задача разблокирования своего объекта блокировки после завершения выполне- ния возлагается на сам поток. В последнем цикле осуществляются лишь ожидание и возврат в начало (тем самым обеспечивается приостановка основного потока), и это происходит до тех пор, пока не будут освобождены обе блокировки. Затем выполне- ние продолжается. Проверка каждой блокировки происходит последовательно, поэ- тому может оказаться, что при размещении всех продолжительных циклов ближе к началу списка циклов весь ход программы будет определяться задержками в медлен- ных циклах. В таких случаях основная часть времени ожидания будет затрачиваться в первом (или первых) цикле. К моменту освобождения этой блокировки все осталь- ные блокировки могут уже быть разблокированы (иными словами, соответствую- щие потоки могут быть завершены). В результате в основном потоке выполнение
06_ch04.indd 193 22.01.2015 22:00:39