Главная страница
Навигация по странице:

  • 4.3.5. Многопоточные модули Python

  • Избегайте использования модуля thread

  • 4.4. Модуль thread

  • Таблица 4.1.

  • Методы объекта LockType Lock

  • Функция/метод Описание

  • Пример 4.2. Использование модуля thread (mtsleepA.py)

  • Пример 4.3. Использование блокировок и модуля thread (mtsleepB.py)

  • Построчное объяснение Строки 1–6

  • Строки 14–34

  • 4.5. Модуль threading

  • Таблица 4.2.

  • Многопоточное программированиеВ этой главе


    Скачать 0.74 Mb.
    НазваниеМногопоточное программированиеВ этой главе
    Дата24.04.2023
    Размер0.74 Mb.
    Формат файлаpdf
    Имя файлаpart.pdf
    ТипДокументы
    #1086381
    страница2 из 8
    1   2   3   4   5   6   7   8
    Пример 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

    Глава 4

    Многопоточное программирование
    194
    операций проверки остальных, освобожденных блокировок произойдет мгновенно, без пауз. Наконец, необходимо полностью гарантировать, чтобы в заключительной паре строк выполнение функции main() происходило лишь при непосредственном вызове этого сценария.
    Как было указано в предыдущем основном примечании, в данной главе модуль thread представлен исключительно для того, чтобы ознакомить читателя с началь- ными сведениями о многопоточном программировании. В многопоточном приложе- нии, как правило, следует использовать высокоуровневые модули, такие как модуль threading, который рассматривается в следующем разделе.
    4.5. Модуль
    threading
    Перейдем к описанию высокоуровневого модуля threading, который не только предоставляет класс Thread, но и дает возможность воспользоваться широким раз- нообразием механизмов синхронизации, позволяющих успешно решать многие важные задачи. В табл. 4.2 представлен список всех объектов, имеющихся в модуле threading.
    Таблица 4.2. Объекты модуля
    threading
    Объект
    Описание
    Thread
    Объект, который представляет отдельный поток выполнения
    Lock
    Примитивный объект блокировки (такая же блокировка, как и в модуле thread)
    Rlock
    Реентерабельный объект блокировки предоставляет возможность в отдельном потоке (повторно) захватывать уже захваченную блокировку (это рекурсивная блокировка)
    Condition
    Объект условной переменной вынуждает один поток ожидать, пока определен- ное условие не будет выполнено другим потоком. Таким условием может быть изменение состояния или задание определенного значения данных
    Event
    Обобщенная версия условных переменных, которая позволяет обеспечить ожидание некоторого события любым количеством потоков, так что после об- наружения этого события происходит активизация всех потоков
    Semaphore
    Предоставляет счетчик конечных ресурсов, совместно используемый потоками; если ни один из ресурсов не доступен, происходит блокировка
    BoundedSemaphore Аналогично Semaphore, но гарантируется, что превышение начального значе- ния никогда не произойдет
    Timer
    Аналогично
    Thread, за исключением того, что происходит ожидание в течение выделенного промежутка времени перед выполнением
    Barrier a
    Создает барьер, которого должно достичь определенное количество потоков, прежде чем всем этим потокам будет разрешено продолжить работу a
    Новое в Python 3.2.
    В этом разделе описано, как использовать класс Thread для реализации многопо- точного режима работы. Выше в данной главе уже были приведены основные сведе- ния о блокировках, поэтому в данном разделе не будут рассматриваться примитивы
    06_ch04.indd 194 22.01.2015 22:00:40

    195 4.5. Модуль threading блокировки. Кроме того, сам класс Thread() позволяет решать некоторые задачи синхронизации, поэтому нет необходимости явно использовать примитивы блоки- ровки.
    1   2   3   4   5   6   7   8


    написать администратору сайта