Многопоточное программированиеВ этой главе
Скачать 0.74 Mb.
|
Многопоточное программирование В этой главе... • Введение/общее назначение • Потоки и процессы • Поддержка потоков в языке Python • Модуль thread • Модуль threading • Сравнение однопоточного и многопоточного выполнения • Практическое применение многопоточной обработки • Проблема “производитель–потребитель” и модуль Queue/queue • Дополнительные сведения об использовании потоков • Связанные модули 4 Глава 06_ch04.indd 181 22.01.2015 22:00:35 Глава 4 Многопоточное программирование 182 > С помощью Python можно запустить поток, но нельзя его остановить. > Вернее, приходится ожидать, пока он не достигнет конца выполнения. > Это означает, что все происходит, как в группе новостей [comp.lang.python]? Обмен сообщениями между Клиффом Уэллсом (Cliff Wells) и Стивом Холденом (Steve Holden) с участием Тимоти Делани (Timothy Delaney), февраль 2002 г. В настоящей главе рассматриваются различные способы обеспечения параллель- ного выполнения в коде. В первых нескольких разделах этой главы показано, в чем состоят различия между процессами и потоками. После этого будет дано определе- ние понятия многопоточного программирования и представлены некоторые средства многопоточного программирования, предусмотренные в языке Python. (Читатели, уже знакомые с многопоточным программированием, могут перейти непосредствен- но к разделу 4.3.5.) В заключительных разделах этой главы приведены некоторые при- меры того, как использовать модули threading и Queue для реализации многопоточ- ного программирования с помощью языка Python. 4.1. Введение/общее назначение До появления средств многопоточного (multithreaded — MT) программирования выполнение компьютерных программ состояло из единой последовательности шагов, которые выполнялись процессором компьютера от начала до конца, т.е. синхронно. Такая организация выполнения применялась независимо от того, требовала ли сама задача последовательного упорядочения шагов или допускала разбиение на подзада- чи и отдельное их выполнение в программе. В последнем случае подзадачи вообще могли быть независимыми, не связанными никакими причинно-следственными от- ношениями (а это означает, что результаты одних подзадач не влияют на выполне- ние других подзадач). Из этого следует вывод, что такие независимые задачи могут выполняться не последовательно, а одновременно. Подобная параллельная органи- зация работы позволяет существенно повысить эффективность решения всей задачи. Изложенные выше соображения лежат в основе многопоточного программирования. Многопоточное программирование идеально подходит для решения задач, асин- хронных по своему характеру (т.е. допускающих прерывание работы), требующих выполнения нескольких параллельных действий, в которых реализация каждого дей- ствия может быть недетерминированной, иными словами, происходящей в случайные и непредсказуемые моменты времени. Такие задачи программирования могут быть организованы в виде нескольких потоков выполнения или разделены на несколько потоков, в каждом из которых осуществляется конкретная подзадача. В зависимости от приложения в этих подзадачах могут вычисляться промежуточные результаты для последующего слияния и формирования заключительной части вывода. Задачи, выполнение которых ограничено пропускной способностью процессора, довольно легко разделить на подзадачи, выполняемые в последовательном или мно- гопоточном режиме. С другой стороны, организовать выполнение однопоточного процесса с несколькими внешними источниками ввода не столь просто. Для решения этой задачи без применения многопоточного режима в последовательной программе необходимо предусмотреть один или несколько таймеров и реализовать схему муль- типлексирования. 06_ch04.indd 182 22.01.2015 22:00:35 183 4.1. Введение/общее назначение В последовательной программе потребуется опрашивать каждый терминальный канал ввода-вывода для проверки наличия данных, введенных пользователем. При этом необходимо добиться того, чтобы в программе не происходило блокирование при чтении из терминального канала ввода-вывода, поскольку сам характер посту- пления введенных пользователем данных является недетерминированным, а блоки- ровка привела бы к нарушению обработки данных из других каналов ввода-вывода. В такой последовательной программе приходится использовать неблокирующий ввод-вывод или блокирующий ввод-вывод с таймером (чтобы блокировка устанавли- валась лишь на время). Последовательная программа представляет собой единый поток выполнения, по- этому ей приходится манипулировать отдельными подзадачами, чтобы на любую от- дельно взятую подзадачу не затрачивалось слишком много времени, а также следить за тем, чтобы длительность формирования ответов пользователям соответствовала установленным критериям. Применение последовательной программы для решения задачи такого типа часто требует организации сложной системы передачи управле- ния, которую трудно понять и сопровождать. Если же для решения подобной задачи программирования применяется много- поточная программа с общей структурой данных, такой как Queue (многопоточная структура данных очереди, рассматриваемая ниже в этой главе), то весь ход работы можно организовать с помощью нескольких потоков, каждый из которых выполняет конкретные функции, например, как показано ниже. • UserRequestThread. Обеспечивает чтение данных, введенных пользователем, возможно, из канала ввода-вывода. В программе может быть создан целый ряд потоков, по одному для каждого из одновременно работающих клиентов, за- просы которых могут быть поставлены в очередь. • RequestProcessor. Поток, который отвечает за выборку запросов из очереди и их обработку с предоставлением полученных выходных данных для еще одного потока. • ReplyThread. Поток, обеспечивающий получение выходных данных, предназна- ченных для пользователя, и их отправку в ответ на запрос (если приложение является сетевым) или запись данных в локальной файловой системе или базе данных. Если для решения подобной задачи программирования применяется несколько потоков, то сложность программы сокращается и появляется возможность обеспе- чить простую, эффективную и хорошо организованную реализацию. Программ- ная реализация каждого потока, как правило, становится проще, поскольку поток предназначен для выполнения не всего задания, а лишь его части. Например, поток UserRequestThread просто считывает данные, введенные пользователем, и помещает их в очередь для дальнейшей обработки другим потоком и т.д. Каждый поток реша- ет собственную подзадачу; программисту остается лишь тщательно спроектировать потоки каждого из применяемых типов, чтобы они выполняли то, что от них требу- ется, наилучшим образом. Принцип использования потоков для решения конкрет- ных задач мало чем отличается от предложенной Генри Фордом модели сборочной линии для производства автомобилей. 06_ch04.indd 183 22.01.2015 22:00:36 Глава 4 Многопоточное программирование 184 4.2. Потоки и процессы 4.2.1. Общее определение понятия процесса Компьютерные программы — это просто исполняемые объекты в двоичной (или другой) форме, которые находятся на диске. Программы начинают действовать лишь после их загрузки в память и вызова операционной системой. Процесс — это про- грамма в ходе ее выполнения (в такой форме процессы иногда называют тяжеловес- ными процессами). Каждый процесс имеет собственное адресное пространство, память и стек данных, а также может использовать другие вспомогательные данные для конт- роля над выполнением. Операционная система управляет выполнением всех процес- сов в системе, выделяя каждому процессу процессорное время по определенному принципу. В ходе выполнения процесса может также происходить ветвление или по- рождение новых процессов для осуществления других задач, но каждый новый про- цесс имеет собственную память, стек данных и т.д. Вообще говоря, отдельные процес- сы не могут иметь доступ к общей информации, если не реализовано межпроцессное взаимодействие (interprocess communication — IPC) в той или иной форме. 4.2.2. Общее определение понятия потока Потоки (иногда называемые легковесными процессами) подобны процессам, за ис- ключением того, что все они выполняются в пределах одного и того же процесса, следовательно, используют один и тот же контекст. Потоки можно рассматривать как своего рода “мини-процессы”, работающие параллельно в рамках основного процес- са или основного потока. Поток запускается, проходит определенную последовательность выполнения и завершается. В потоке ведется указатель команд, позволяющий следить за тем, где в настоящее время происходит его выполнение в текущем контексте. Поток может быть прерван и переведен на время в состояние ожидания (это состояние принято также называть приостановкой (sleeping)), в то время как другие потоки продолжают работать. Такая операция называется возвратом управления (yielding). Все потоки, организованные в одном процессе, используют общее пространство данных с основным потоком, поэтому могут обмениваться информацией или взаи- модействовать друг с другом с меньшими сложностями по сравнению с отдельными процессами. Потоки, как правило, выполняются параллельно. Именно распаралле- ливание и совместное использование данных становятся предпосылками обеспече- ния координации выполнения нескольких задач. Вполне естественно, что в системе с одним процессором невозможно в полном смысле слова организовать параллельное выполнение, поэтому планирование потоков происходит таким образом, чтобы каж- дый из них выполнялся в течение какого-то короткого промежутка времени, а затем возвращал управление другим потокам (образно говоря, снова становился в очередь на получение следующей порции процессорного времени). В ходе выполнения всего процесса каждый поток осуществляет свои собственные, отдельные задачи и передает полученные результаты другим потокам по мере необходимости. Разумеется, переход от последовательной организации работы к параллельной связан с возникновением дополнительных сложностей. В частности, если два или не- сколько потоков получают доступ к одному и тому же фрагменту данных, то в за- висимости от того, в какой последовательности происходит доступ, могут возникать несогласованные результаты. Неопределенность в отношении последовательности 06_ch04.indd 184 22.01.2015 22:00:36 185 4.3. Поддержка потоков в языке Python доступа принято называть состоянием состязания (race condition). К счастью, в боль- шинстве библиотек поддержки потоков предусмотрены примитивы синхронизации того или иного типа, которые позволяют диспетчеру потоков управлять выполнени- ем и доступом. Еще одна сложность обусловлена тем, что невозможно предоставлять всем пото- кам равную и справедливую долю времени выполнения. Это связано с тем, что неко- торые функции устанавливают блокировки и снимают их только после завершения своего выполнения. Если функция не разработана специально для использования в потоке, то ее применение может привести к перераспределению процессорного вре- мени в ее пользу. Такие функции принято называть жадными (greedy). 4.3. Поддержка потоков в языке Python В разделе описывается использование потоков в программе на языке Python. В частности, рассматриваются ограничения потоков, обусловленные применением глобальной блокировки интерпретатора, и приводится небольшой демонстрацион- ный сценарий. 4.3.1. Глобальная блокировка интерпретатора Выполнением кода Python управляет виртуальная машина Python (называемая так- же главным циклом интерпретатора). Язык Python разработан таким способом, чтобы в этом главном цикле мог выполняться только один поток управления по аналогии с тем, как организовано совместное использование одного процессора несколькими процессами в системе. В памяти может находиться много программ, но в любой конкретный момент времени процессор занимает только одна из них. Аналогичным образом, притом что в интерпретаторе Python могут эксплуатироваться несколько потоков, в любой момент времени интерпретатором выполняется только один поток. Для управления доступом к виртуальной машине Python применяется глобальная блокировка интерпретатора (global interpreter lock — GIL). Именно эта блокировка обеспечивает то, что выполняется один и только один поток. Виртуальная машина Python функционирует в многопоточной среде следующим образом. 1. Задание глобальной блокировки интерпретатора. 2. Переключение на поток для его выполнения. 3. Должно быть выполнено одно из следующего: а) заданное количество команд в байт-коде; б) проверка способности потока самостоятельно возвращать управление (для чего может служить вызов функции time.sleep(0)). 4. Перевести поток назад в приостановленное состояние (выйти из потока). 5. Разблокировать глобальную блокировку интерпретатора. 6. Снова проделать все эти действия (lather, rinse, repeat). Если сделан вызов внешнего кода (допустим, любой встроенной функции расши- рения C/C++), то глобальная блокировка интерпретатора будет заблокирована до за- вершения этого вызова (поскольку в языке Python невозможно задать интервал с по- мощью байт-кода). Тем не менее при программировании расширений не исключена 06_ch04.indd 185 22.01.2015 22:00:36 Глава 4 Многопоточное программирование 186 возможность разблокирования глобальной блокировки интерпретатора, что позволя- ет избавить разработчика Python от необходимости брать на себя управление блоки- ровками в коде Python в подобных ситуациях. Например, в любых процедурах Python, основанных на использовании ввода-выво- да (в которых вызывается встроенный код C операционной системы), предусмотрено освобождение глобальной блокировки интерпретатора до вызова функции ввода-вы- вода, что позволяет продолжить выполнение других потоков, в то время как проис- ходит ввод-вывод. Если же в коде не осуществляется большой объем ввода-вывода, то, как правило, процессор (и глобальная блокировка интерпретатора) блокируется на полный интервал времени, предоставленный потоку, пока он не вернет управление. Иными словами, больше шансов воспользоваться преимуществами многопоточной среды имеют программы Python, ограничиваемые пропускной способностью вво- да-вывода, чем программы, ограничиваемые пропускной способностью процессора. Читатели, желающие ознакомиться с исходным кодом, а также изучить организа- цию главного цикла интерпретатора и глобальной блокировки интерпретатора, мо- гут просмотреть файл Python/ceval.c. 4.3.2. Выход из потока После того как поток завершает выполнение задачи, для которой он был создан, происходит выход из потока. Выход из потока может осуществляться путем вызова одной из функций выхода, такой как thread.exit(), с применением любого из стан- дартных способов выхода из процесса Python, например sys.exit(), или с помощью генерирования исключения SystemExit. Однако возможность непосредственно унич- тожить поток отсутствует. В следующем разделе будут рассматриваться два модуля Python, применяемых для работы с потоками, но один из них, модуль thread, не рекомендуется для ис- пользования. Для этого есть много причин, но наиболее важной из них является то, что применение этого модуля приводит к завершению работы всех прочих потоков после выхода из основного потока, и при этом очистка памяти не осуществляется должным образом. Второй модуль, threading, гарантирует, что весь процесс будет оставаться действующим до тех пор, пока не произойдет выход из всех важных до- черних потоков. (Чтобы ознакомиться со сведениями о том, почему это так важно, прочитайте врезку “Избегайте использования модуля thread”.) Тем не менее в основные потоки всегда следует закладывать такие алгоритмы, чтобы они качественно выполняли функции диспетчера и при осуществлении этой задачи учитывали, какое назначение имеют отдельные потоки, какие данные или па- раметры требуются для каждого из порожденных потоков, когда эти потоки завер- шат выполнение и какие результаты предоставят. В ходе выполнения этой работы основные потоки могут дополнительно формировать отдельные результаты в виде окончательного, значимого вывода. 4.3.3. Доступ к потокам из программы Python Язык Python поддерживает многопоточное программирование с учетом особенно- стей операционной системы, под управлением которой он функционирует. Он под- держивается на большинстве платформ на основе Unix, таких как Linux, Solaris, Mac OS X, *BSD, а также на персональных компьютерах под управлением Windows. В язы- ке Python используются потоки, совместимые со стандартом POSIX, которые иногда называют пи-потоками (pthreads). 06_ch04.indd 186 22.01.2015 22:00:36 187 4.3. Поддержка потоков в языке Python По умолчанию поддержка потоков включается при построении интерпретатора Python из исходного кода (начиная с версии Python 2.0) или при установке исполняе- мой программы интерпретатора в среде Win32. Чтобы определить, предусмотрено ли применение потоков на конкретном установленном интерпретаторе, достаточно про- сто попытаться импортировать модуль thread из интерактивного интерпретатора, как показано ниже (если потоки доступны, то не появляется сообщение об ошибке). >>> import thread >>> Если интерпретатор Python не был откомпилирован с включенными потоками, то попытка импорта модуля оканчивается неудачей: >>> import thread Traceback (innermost last): File " 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 секунд. |