Skip to content

Home Администрирование Потоки выполнения в Python
Потоки выполнения в Python

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

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

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

Знание основ программирования многопоточных приложений может оказаться полезным для системного администратора. Вот несколько примеров, когда потоки выполнения могут пригодиться в повседневной практике системного администратора: исследование локальной сети в автоматическом режиме, извлечение нескольких веб-страниц одновременно, нагрузочное тестирование сервера и вbinолнение сетевых операций.

Сохраняя верность принципу KlsS, рассмотрим один из самых простых примеров использования нескольких потоков выполнения. Следует заметить, что для использования модуля threading необходимо понимание объектно-ориентированного программирования. Если у вас недостаточный опыт объектно-ориентированного программирования (ООП) или вообще его нет, тогда этот пример может оказаться непонятным для вас. В этом случае мы могли бы порекомендовать приобрести книгу Марка Лутца (Mark Lutz) «Learning Python» (O'Reilly) и познакомиться с некоторыми основами ООП. В конечном счете, объектно-ориентированное программирование достойно того, чтобы изучать его.

Давайте перейдем непосредственно к примеру многопоточного приложения, где используются самые простые приемы многопоточного программирования. В этом простом многопоточном сценарии используется модуль threading. В сценарии устанавливается значение глобальной переменной, и затем переопределяется метод run() потока выполнения. Наконец, запускается пять потоков выполнения, каждый из которых выводит свой номер.

Во многих отношениях этот пример чрезмерно упрощен и имеет плохой дизайн, потому что в нем сразу несколько потоков используют одну и ту же глобальную переменную. Часто совместно с потоками лучше использовать очереди, так как они могут принять на себя всю сложность организации доступа к совместно используемым данным. Исходный текст сценария приводится в примере 10.17.

Пример 10.17. Простейший многопоточный сценарий

многозадачность

Sat Apr 19 06:45:57 2008 WAITING FOR clearink.com

Sat Apr 19 06:45:57 2008 clearink.com RETURNED 0

Sat Apr 19 06:45:57 2008 WAITING FOR ironport.com

Sat Apr 19 06:46:58 2008 ironport.com RETURNED 0

В качестве оговорки к следующим примерам многопоточных сценариев следует заметить, что они являются достаточно сложными примерами и те же самые действия могут быть реализованы на основе применения функции subprocess.Popen(). Эта функция является лучшим выбором, когда требуется запустить группу процессов и дождаться их завершения. Если вам необходимо организовать взаимодействие с каждым процессом, то можно использовать функцию subprocess. Popen() в комплексе с потоками выполнения. Основная цель этих примеров - продемонстрировать, что многозадачность нередко требует уступок и компромиссов. Часто бывает очень трудно определить, какая модель лучше отвечает требованиям - потоки выполнения, процессы или асинхронные библиотеки, такие как stackless или twlsted. Ниже приводится пример опроса с помощью утилиты ping большого массива IP-адресов.

Теперь, когда у нас имеется своеобразная программа «Hello World» для потоков выполнения, можно перейти к реализации сценария, который оценит любой системный администратор. Возьмем за основу наш сценарий и изменим его так, чтобы он опрашивал узлы в сети. Это можно считать начальным этапом на пути создания универсального инструмента для работы с сетью. Программный код сценария приводится в примере 10.18.

Пример 10.18. Многопоточная версия утилиты ping

#!/usr/bin/env python

from threading import Thread

import subprocess

from Queue import Queue

num_threads = 3

queue = Queue()

ips = ["10.0.1.1", "10.0.1.3", "10.0.1.11", "10.0.1.51"]

def pinger(i, q):

.... опрос подсети

while True:

ip = q.get()

print "Thread %s: Pinging %s" % (i, ret = subprocess.call("ping -c 1 %s" shell=True,

stdout=open('/dev/nuir, stderr=subprocess.STDOUT) if ret == 0:

print "%s: ls alive" % ip else:

print "%s: did not respond" % ip

Когда мы запустили этот, достаточно простой фрагмент программного кода, мы получили следующий результат:

Этот пример заслуживает того, чтобы разобрать его на понятные части, но сначала - небольшое пояснение. Пример разработки многопоточной версии утилиты ping с целью опроса подсети - это отличный способ продемонстрировать применение потоков. «Обычная» программа на языке Python, не использующая потоки выполнения, потребовала бы времени для своего выполнения N * (среднее время ожидания ответа на каждый запрос ping). Утилита ping может возвращать один из двух вариантов ответа: время отклика хоста и сообщение об истечении предельного времени ожидания. В типичной сети можно столкнуться с обоими вариантами.

Это означает, что на вbinолнение приложения, использующего утилиту ping для опроса хостов сети класса С, состоящей из 254 адресов, может потребоваться до 254 * (~ 3 секунды), что может составить до 12.7 минут. При использовании потоков это время можно уменьшить до нескольких секунд. Именно поэтому потоки имеют важное значение для разработки сетевых приложений. Теперь сделаем еще шаг и подумаем, какие условия могут встретиться в действительности. Сколько подсетей может существовать в типичном центре обработки данных? 20? 30? 50? Очевидно, что программа, вbinолняющая опрос последовательным способом, быстро теряет свою практическую ценность, и многопоточная версия становится идеальным выбором.

Теперь вернемся к нашему простому сценарию и рассмотрим некоторые особенности реализации. Первое, на что следует обратить внимание, - это импортируемые модули, в частности, наибольший интерес для нас представляют модули threading и Queue. Как уже отмечалось выше, разработка многопоточных приложений без использования очередей намного сложнее, и многим оказывается не под силу. Всякий раз, когда вам требуется прибегнуть к использованию потоков, желательно использовать модуль Queue. Почему? Этот модуль снижает потребность в явной реализации защиты данных с помощью мьютексов, потому что внутренние механизмы самих очередей обеспечивают необходимую защиту данных.

Представьте, что вы фермер/ученый, живущий в Средние века, и вы заметили, что вороны, которых часто называют «убийцами» (если интересно узнать, почему, обращайтесь в Википедию), атакуют ваши поля с зерновыми стаями по 20 или более особей.

Это очень умные птицы и их невозможно испугать, бросая камни, так как вы сможете бросать не чаще, чем один камень каждые 3 секунды, а численность стаи может достигать 50 особей. Чтобы отпугнуть всех ворон, может потребоваться до нескольких минут, но за этот промежуток времени урожаю может быть нанесен существенный ущерб. Как ученый, знающий математику, вы понимаете, что эта проблема легко разрешима. Вам нужно лишь набрать кучу камней и затем расставить работников, чтобы они могли одновременно брать камни из кучи и бросать в ворон.

Если следовать этой стратегии, 30 работников, выбирая камни из кучи, могли бы закидать камнями 50 ворон менее чем за 10 секунд. Это основа использования потоков и очередей в сценариях на языке Python. Вы нанимаете группу работников для выполнения какой-либо работы, и когда очередь опустеет, задание можно считать выполненным.

Очереди обеспечивают способ передачи заданий «группе» работников централизованным образом. Один из самых важных элементов нашей простой программы - это вызов метода join(). Описание метода queue. join() гласит следующее:

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

Теперь посмотрим, что выдаст наш замечательный сценарий. Взгляните на пример 10.19.

Пример 10.19. Пример, когда главный поток выполнения программы завершает работу раньше других потоков

Следующая функция вbinолняет основную работу в программе. Она вызывается каждым потоком и извлекает очередной IP-адрес из очереди.

Теперь, ознакомившись с теорией применения в сценариях потоков выполнения и очередей, пройдемся по программному коду шаг за шагом. В самом начале мы жестко определили несколько значений, которые в более универсальных программах обычно передаются в виде аргументов командной строки. Переменная num_threads содержит число рабочих потоков, переменная queue - это экземпляр очереди и, наконец, ips — это список IP-адресов, которые мы должны поместить в очередь:

Примечательно, что адреса выталкиваются из очереди в том же порядке, в каком они находятся в списке. Такая реализация позволяет извлекать элементы, пока очередь не опустеет. В конце цикла while вызывается метод q. task_done() - это имеет важное значение, потому что он сообщает методу join() о том, что был обработан очередной элемент, извлеченный из очереди. Или, говоря простым языком, он сообщает о том, что задание вbinолнено. Посмотрим, что говорится в описании метода Queue. Queue. task_done():

Из описания видно, что между методами q.get() и q. task_done() существует взаимосвязь и в конечном счете они связаны с методом q. join(). Это практически начало, середина и конец истории:

Ниже мы используем простой цикл for, который управляет созданием группы потоков выполнения. Примечательно, что эта группа просто «сидит и ждет», пока что-нибудь не появится в очереди. В программе ничего не происходит, пока управление не достигнет следующего раздела.

В нашей программе кроется одна малозаметная хитрость, которая предохраняет программу от попадания в ловушку. Обратите внимание на вызов метода setDaemon(True). Если этого не сделать перед вызовом метода start() потока, программа зависнет на неопределенный срок.

Причина практически незаметна на первый взгляд и заключается в том, что программа может завершить свою работу, только если потоки вbinолняются в режиме демонов. Вы могли заметить, что в функции pinger() используется бесконечный цикл. Поскольку поток, вызвавший такую функцию, никогда сам не завершится, нам пришлось объявить их потоками-демонами. Чтобы убедиться в справедливости вышесказанного, просто закомментируйте строку worker. setDaemon(True) и запустите программу. Заметим лишь, что без вызова этого метода программа будет крутиться вхолостую неопределенно продолжительное время. Обязательно проверьте это у себя, так как это поможет вам частично снять с процесса покров таинственности:

К этому моменту в нашей программе имеется группа готовых к работе потоков, ожидающих, пока мы дадим им задание. Как только мы поместим элементы в очередь, нашим потокам тут же будет послан сигнал, что можно извлечь задание из очереди, которое в данном случае заключается в том, чтобы опросить указанный IP-адрес:

Наконец, мы достигли критической строки, зажатой между двумя инструкциями print, которая в конечном счете управляет программой. Как уже говорилось ранее, вызвав метод очереди join(), главный поток программы становится в ожидание, пока не опустеет очередь заданий. Именно поэтому потоки и очередь напоминают шоколад с арахисовым маслом. Каждый из них обладает своей прелестью, но вместе они создают особый вкус.

Чтобы лучше понять принципы использования потоков и очередей, нам нужно сделать еще один шаг вперед и добавить в наш пример еще одну группу потоков и еще одну очередь. В первом примере мы опрашивали с помощью утилиты ping список IP-адресов, которые извлекали из очереди. В следующем примере мы заставим первую группу потоков помещать IP-адреса, от которых был получен ответ, во вторую очередь.

После этого вторая группа потоков будет извлекать IP-адреса из второй очереди, производить опрос с помощью утилиты arping и возвращать IP-адреса вместе с МАС-адресами. Исходный текст примера приводится в примере 10.20.

Пример 10.20. Несколько очередей и несколько групп потоков

После запуска этого сценария мы получили следующие результаты:

Для реализации этого решения мы лишь немного расширили предыдущий пример, добавив в него еще одну группу потоков и еще одну очередь. Это достаточно важный прием, чтобы поместить его в свой арсенал, потому что модуль queue делает использование потоков более простым и более безопасным делом. Можно даже сказать, что этот прием относится к разряду обязательных к применению.

Задержка выполнения потоков с помощью threading.Timer

В языке Python имеется еще одна особенность, имеющая отношение к потокам, которая может оказаться удобной при решении задач системного администрирования. Она существенно упрощает запуск функции с задержкой по времени, как показано в примере 10.21.

Пример 10.21. Таймер внутри потока

Если запустить этот фрагмент, можно увидеть, что главный поток программы продолжает работу и при этом происходит отложенный вызов функции:

Обработка событий в потоке

Давайте применим описанную выше методику для решения более практичной задачи. В этом примере мы возьмем на вооружение прием с отложенным запуском и объединим его с циклом ожидания событий, в котором будем проверять наличие расхождений в именах файлов между двумя каталогами. Мы могли бы пойти дальше и проверять время последнего изменения файлов, но, следуя принципу сохранения максимальной простоты примеров, мы посмотрим, как в этом цикле проверяется ожидаемое событие и как, в случае его появления, вызывается метод обработки.

Этот модуль легко можно преобразовать в более универсальный инструмент, но пока в примере 10.22 жестко определены каталоги, которые будут синхронизироваться с задержкой с помощью команды rsync -av --delete, если между ними будут обнаружены различия.

Пример 10.22. Инструмент синхронизации каталогов

Внимательные читатели могут заметить, что строго говоря, задержка здесь не является строго необходимой, и это действительно так. Но как бы то ни было, задержка может дать дополнительное преимущество. Если добавить задержку, скажем, на 5 секунд, можно было бы прервать ввыполнение потока в случае наступления другого события, например, в случае неожиданного удаления основного каталога. Задержка, реализованная в виде потока, представляет собой замечательный механизм создания операций с отложенным ввыполнением, которые можно отменить.

Комментарии (0)

RSS feed Comments

Написать комментарий

smaller | bigger

busy
 

Регистрация




Top