Потоки выполнения представляют еще один способ запуска операций, выполняемых одновременно. В двух словах, механизм потоков выполнения позволяет запустить функцию (или вызываемый объект другого типа) параллельно основной программе. Иногда их называют «облегченными процессами», потому что они работают параллельно, подобно дочерним процессам, но выполняются в рамках одного и того же процесса. Процессы обычно используются для запуска независимых программ, а потоки выполнения — для решения таких задач, как неблокирующий ввод, и для выполнения продолжительных заданий в программах с графическим интерфейсом. Они также представляют естественную модель реализации алгоритмов, которые можно выразить в терминах независимых заданий. В приложениях, которые выигрывают от параллельной обработки, потоки дают программистам большие выгоды:
Производительность
Поскольку все потоки выполняются в пределах одного процесса, их запуск не сопряжен с высокими накладными расходами, как при копировании процесса в целом. Издержки, связанные с копированием порождаемых дочерних процессов и запуском потоков, могут быть различными в зависимости от платформы, но обычно считается, что потоки обходятся дешевле в смысле производительности.
Простота
Потоки выполнения заметно проще в обращении, особенно если на сцену выходят более сложные аспекты процессов (например, завершение процессов, обмен информацией между процессами и процессы-«зомби», о которых рассказывается в главе 12).
Совместно используемая глобальная память
Кроме того, поскольку потоки выполняются в одном процессе, они используют общую глобальную память процесса. Благодаря этому потоки могут просто и естественно взаимодействовать друг с другом путем чтения и записи данных в глобальной памяти, доступной всем потокам выполнения. Для программиста на языке Python это означает, что глобальные переменные, объекты и их атрибуты и такие компоненты, как импортированные модули, совместно используются всеми потоками выполнения в программе — если, например, в одном потоке выполнения присваивается значение глобальной переменной, ее новое значение увидят все другие потоки выполнения. При обращении к совместно используемым глобальным объектам необходимо проявлять некоторую осторожность, но все равно это обычно проще, чем те средства организации взаимодействий, которые применяются для обмена данными с дочерними процессами и с которыми мы познакомимся ниже в этой главе (например, каналы, потоки ввода-вывода, сигналы, сокеты и так далее). Как и многое в программировании, все вышеизложенное не является универсальной и общепринятой истиной, поэтому вам самим придется взвесить и оценить различия с позиции своих программ и платформ.
Переносимость
Возможно, важнее всего, что приемы работы с потоками выполнения лучше переносятся на другие платформы, чем приемы работы с процессами. На момент написания данной книги функция os.fork вообще не поддерживается стандартной версией Python для Windows, тогда как потоки выполнения поддерживаются. Если вам необходимо обеспечить параллельное выполнение заданий в сценариях на языке Python переносимым способом, и вы не желаете или не можете установить в Windows Unix-подобную библиотеку, такую как Cygwin, потоки выполнения окажутся, скорее всего, лучшим решением. Инструменты для работы с потоками выполнения в Python автоматически учитывают специфические для каждой платформы различия в потоках выполнения и предоставляют единообразный интерфейс для всех операционных систем. Следует отметить, что относительно новый пакет multiprocessing, описываемый далее в этой главе, предлагает еще одно решение проблемы переносимости, которое может использоваться в некоторых случаях.
Так в чем же подвох? Существует три основных потенциальных недостатка, о которых следует знать, прежде чем нырять в свои потоки выполнения:
Вызовы функций и запуск программ
Прежде всего, потоки выполнения не являются способом, по крайней мере, не самым простым способом, запуска других программ. Потоки выполнения предназначены для запуска функций (точнее, любого вызываемого объекта, включая связанные и несвязанные методы), выполняющихся параллельно с основной программой. Как мы видели в предыдущем разделе, после выполнения операции ветвления дочерние процессы могут вызывать функции или запускать новые программы. Естественно, функция, запущенная в отдельном потоке выполнения, также способна запускать другие сценарии с помощью встроенной функции exec и новые программы с помощью таких инструментов, как функции os.system, os.popen и модуль subprocess, особенно если они производят продолжительные вычисления. Но вообще, потоки выполнения предназначены для запуска функций внутри программы.
С практической точки зрения это обычно не рассматривается, как недостаток. Для многих приложений возможность параллельного выполнения функций сама по себе является достаточно мощным приобретением. Например, если вам необходимо реализовать неблокирующий ввод и вывод или избежать «подвисания» графического интерфейса из-за выполнения продолжительной операции, с этим прекрасно справятся потоки выполнения — просто создайте поток выполнения. который запустит функцию, производящую продолжительные вычисления, а основная программа продолжит выполняться независимо.
Синхронизация потоков выполнения и очереди
Во-вторых, тот факт, что потоки выполнения совместно используют объекты и переменные в глобальной памяти процесса, имеет свои положительные и отрицательные стороны — это упрощает организацию взаимодействий, но при этом нам необходимо синхронизировать выполнение различных операций. Как мы увидим далее, даже такие операции, как вывод, могут стать источником конфликтов, потому что они пользуются одним потоком вывода sys.stdout процесса.
К счастью, модуль queue из стандартной библиотеки, описываемый в этом разделе, упрощает решение этой проблемы: на практике многопоточные программы обычно создают один или несколько потоков производителей (рабочих потоков), которые добавляют данные в очередь, и один или более потоков потребителей, которые извлекают данные из очереди и обрабатывают их. Например, в типичной реализации графического интерфейса производители могут загружать или вычислять данные и помещать их в очередь, а потребитель — главный поток выполнения в программе — периодически проверять наличие данных в очереди по событиям от таймера и отображать их в графическом интерфейсе. Поскольку стандартная реализация очередей уже предусматривает возможность работы с несколькими потоками выполнения, программы, структурированные таким способом, автоматически обеспечивают синхронизацию доступа к данным из нескольких потоков выполнения.
Глобальная блокировка интерпретатора (Global Interpreter Lock, GIL)
Наконец, как мы узнаем далее в этом разделе, реализация механизма потоков выполнения в Python допускает выполнение виртуальной машиной только одного потока в каждый конкретный момент времени. Потоки выполнения в Python являются настоящими потоками выполнения операционной системы, но каждый поток должен приобрести единственную общедоступную блокировку, когда будет готов к запуску, и каждый поток выполнения может быть вытеснен через короткий промежуток времени (в настоящее время — после выполнения виртуальной машиной некоторого количества инструкций, хотя такой порядок может измениться в Python 3.2).
Вследствие этого потоки выполнения в языке Python не могут выполняться одновременно на нескольких процессорах в многопроцессорных системах. Чтобы воспользоваться преимуществами многопроцессорных систем, можно вместо потоков выполнения воспользоваться механизмом ветвления процессов (объем и сложность программного кода в обоих случаях остаются примерно одинаковыми). Кроме того, части потоков выполнения, реализованные как расширения на языке C, могут выполняться по-настоящему независимо, если они освобождают GIL, чтобы обеспечить возможность выполнения программного кода Python в других потоках. Однако программный код на языке Python не может выполняться одновременно в нескольких потоках.
Преимущество реализации механизма потоков выполнения в Python — высокая производительность. Первые попытки внедрить механизм поддержки потоков выполнения в виртуальную машину привели к двукратному снижению скорости выполнения программ в Windows, и еще большее снижение наблюдалось в Linux. Даже однопоточные программы работали в два раза медленнее.
Даже при том, что наличие GIL снижает практическую пользу потоков выполнения в языке Python, не позволяя использовать преимущества многопроцессорных систем, — потоки выполнения остаются полезным инструментом реализации неблокирующих операций, особенно в приложениях с графическим интерфейсом. Кроме того, новый пакет multiprocessing, с которым мы познакомимся далее, предлагает другое решение этой проблемы — он предоставляет переносимый прикладной интерфейс, похожий на интерфейс механизма потоков выполнения, но основанный на процессах, благодаря чему программы получают простоту обращения с потоками выполнения и преимущества выполнения независимых процессов в многопроцессорных системах.
Несмотря на то, что после прочтения этого обзора у вас могло сложиться иное мнение, я утверждаю, что потоки выполнения в языке Python удивительно просты в использовании. Фактически когда запускается программа, она уже выполняется в потоке, который обычно называется «главным потоком» процесса. Для запуска новых, независимых потоков выполнения в рамках одного и того же процесса в программах на языке Python обычно используется либо низкоуровневый модуль _thread, позволяющий запускать функции в порожденных потоках выполнения, либо высокоуровневый модуль threading, предоставляющий возмож-
ность управления потоками выполнения с помощью объектов высокого уровня, созданных на основе классов. Оба модуля также предусматривают инструменты синхронизации доступа к совместно используемым объектам с помощью блокировок.
В данной книге будут исследоваться оба модуля, _thread и threading, и в примерах они будут использоваться взаимозаменяемо. Некоторые программисты на языке Python могли бы порекомендовать всегда использовать модуль threading и оставить модуль _thread в покое. Последний из них ранее назывался thread и в версии 3.X получил название _thread, которое предполагает менее высокий статус модуля. Лично я считаю, что это крайность (это одна из причин, почему в некоторых примерах в данной книге используется конструкция as thread в инструкциях импортирования, позволяющая использовать оригинальное имя модуля в программном коде). Если только вам не требуются мощные инструменты из модуля threading, выбор между этими двумя модулями является вопросом личных предпочтений, при этом дополнительные требования модуля threading могут считаться ничем не оправданными.
В базовом модуле _thread не используются приемы объектноориентированного программирования, и он очень прост в использовании, как будет показано в примерах этого раздела. Модуль threading лучше подходит для решения более сложных задач, которые требуют сохранения информации в контексте потоков или наблюдения за потоками, но не все многопоточные программы требуют применения дополнительных инструментов, и во многих из них используется достаточно ограниченный набор возможностей многопоточной модели. Фактически сравнение этих модулей напоминает сравнение функции os.walk с классами, реализующими обход дерева, с которыми мы встретимся в главе 6, — оба приема имеют своих сторонников и область применения. Как всегда, не забывайте основное правило Python: не добавляйте сложностей, когда сложности не нужны.
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, I том, 2011