Как потоки работают в Python и каковы общие подводные камни Python-threading?

85

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

Насколько я могу судить, одновременно может работать только один поток, а активный поток переключается каждые 10 инструкций или около того?

Где есть хорошее объяснение или вы можете его предоставить? Также было бы очень хорошо знать об общих проблемах, с которыми вы сталкиваетесь при использовании потоков с Python.

jdd
источник

Ответы:

50

Да, из-за глобальной блокировки интерпретатора (GIL) одновременно может выполняться только один поток. Вот несколько ссылок с некоторыми сведениями об этом:

Из последней ссылки интересная цитата:

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

Если вы хотите использовать многоядерность, pyprocessing определяет API на основе процессов для реального распараллеливания. PEP также включает в себя некоторые интересные тесты.

Питер Хоффманн
источник
1
Действительно комментарий к цитате smoothspan: конечно, потоки Python эффективно ограничивают вас одним ядром, даже если на машине их несколько? Многоядерность может принести пользу, поскольку следующий поток может быть готов к работе без переключения контекста, но ваши потоки Python никогда не могут использовать более одного ядра за раз.
Джеймс Брэди,
2
Правильно, потоки python практически ограничены одним ядром, ЕСЛИ модуль C не взаимодействует с GIL и не запускает собственный собственный поток.
Арафангион,
Фактически, несколько ядер делают потоки менее эффективными, так как возникает много проблем с проверкой, может ли каждый поток получить доступ к GIL. Даже с новым GIL производительность все равно хуже ... dabeaz.com/python/NewGIL.pdf
Basic
2
Обратите внимание, что рекомендации GIL не относятся ко всем переводчикам. Насколько мне известно, и IronPython, и Jython работают без GIL, что позволяет их коду более эффективно использовать многопроцессорное оборудование. Как упоминал Арафангион, интерпретатор CPython также может правильно работать в многопоточном режиме, если код, которому не требуется доступ к элементам данных Python, снимает блокировку, а затем снова получает ее перед возвратом.
holdenweb
Что вызывает переключение контекста между потоками в Python? Это основано на прерываниях таймера? Блокировка или конкретный вызов доходности?
CMCDragonkai
36

Python - это довольно простой язык для внедрения, но с некоторыми оговорками. Самое важное, о чем вам нужно знать, - это Global Interpreter Lock. Это позволяет только одному потоку получить доступ к интерпретатору. Это означает две вещи: 1) вы редко когда-либо используете оператор блокировки в python и 2) если вы хотите воспользоваться преимуществами многопроцессорных систем, вы должны использовать отдельные процессы. РЕДАКТИРОВАТЬ: Я также должен указать, что вы можете поместить часть кода на C / C ++, если хотите также обойти GIL.

Таким образом, вам нужно пересмотреть, почему вы хотите использовать потоки. Если вы хотите распараллелить свое приложение, чтобы воспользоваться преимуществами двухъядерной архитектуры, вам необходимо подумать о том, чтобы разбить приложение на несколько процессов.

Если вы хотите улучшить отзывчивость, вам следует РАССМАТРИВАТЬ использование потоков. Однако есть и другие альтернативы, а именно микронить . Есть также несколько фреймворков, на которые стоит обратить внимание:

Джейсон Бейкер
источник
@JS - Исправлено. В любом случае этот список устарел.
Джейсон Бейкер
Мне просто кажется неправильным, что вам нужно несколько процессов - со всеми вытекающими отсюда накладными расходами - чтобы воспользоваться преимуществами многоядерной системы. У нас есть серверы с 32 логическими ядрами - значит, мне нужно 32 процесса для их эффективного использования? Безумие
Базовое
@Basic - накладные расходы на запуск процесса по сравнению с запуском потока в наши дни минимальны. Я полагаю, вы можете начать видеть проблемы, если мы говорим о тысячах запросов в секунду, но тогда я бы поставил под сомнение выбор Python для такой загруженной службы в первую очередь.
Джейсон Бейкер
20

Ниже приведен базовый образец резьбы. Будет создано 20 потоков; каждый поток выведет свой номер потока. Запустите его и соблюдайте порядок печати.

import threading
class Foo (threading.Thread):
    def __init__(self,x):
        self.__x = x
        threading.Thread.__init__(self)
    def run (self):
          print str(self.__x)

for x in xrange(20):
    Foo(x).start()

Как вы уже намекали, потоки Python реализуются посредством квантования времени. Так получается «параллельный» эффект.

В моем примере мой класс Foo расширяет поток, затем я реализую runметод, в котором находится код, который вы хотите запустить в потоке. Чтобы запустить поток, вы вызываете start()объект потока, который автоматически вызывает runметод ...

Конечно, это только самые основы. Со временем вы захотите узнать о семафорах, мьютексах и блокировках для синхронизации потоков и передачи сообщений.

mmattax
источник
10

Используйте потоки в python, если отдельные воркеры выполняют операции ввода-вывода. Если вы пытаетесь масштабировать несколько ядер на машине, либо найдите хорошую структуру IPC для python, либо выберите другой язык.

Бен МакНил
источник
4

Примечание: где бы я ни упоминал, threadя имею в виду именно потоки в Python, пока явно не указано иное.

В python потоки работают немного по-другому, если вы исходите из C/C++фона. В python только один поток может находиться в состоянии выполнения в данный момент времени. Это означает, что потоки в python не могут по-настоящему использовать мощность нескольких процессорных ядер, поскольку по дизайну потоки не могут выполняться параллельно на нескольких ядрах.

Поскольку управление памятью в Python не является потокобезопасным, каждый поток требует монопольного доступа к структурам данных в интерпретаторе Python. Этот эксклюзивный доступ обеспечивается механизмом, называемым (глобальная блокировка интерпретатора) .GIL

Why does python use GIL?

Чтобы предотвратить одновременный доступ нескольких потоков к состоянию интерпретатора и его повреждение.

Идея состоит в том, что всякий раз, когда поток выполняется (даже если это основной поток) , GIL приобретается и после некоторого предопределенного интервала времени GIL освобождается текущим потоком и повторно запрашивается каким-либо другим потоком (если есть).

Why not simply remove GIL?

Дело не в том, что удалить GIL невозможно, просто при этом мы в конечном итоге устанавливаем несколько блокировок внутри интерпретатора для сериализации доступа, что делает даже однопоточное приложение менее производительным.

поэтому затраты на удаление GIL окупаются снижением производительности однопоточного приложения, что никогда не бывает желательным.

So when does thread switching occurs in python?

Переключение потоков происходит при выпуске GIL. Итак, когда выпускается GIL? Следует учитывать два сценария.

Если поток выполняет операции, связанные с ЦП (например, обработка изображений).

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

В новых версиях вместо использования количества команд в качестве метрики для переключения потока используется настраиваемый временной интервал. Интервал переключения по умолчанию составляет 5 миллисекунд. Вы можете получить текущий интервал переключения, используя sys.getswitchinterval(). Это можно изменить с помощьюsys.setswitchinterval()

Если поток выполняет некоторые операции, связанные с вводом-выводом (доступ к файловой системе Ex или
сетевой ввод-вывод)

GIL освобождается всякий раз, когда поток ожидает завершения некоторой операции ввода-вывода.

Which thread to switch to next?

У интерпретатора нет собственного планировщика. Какой поток становится запланированным в конце интервала, решает операционная система. .

анекикс
источник
3

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

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

будет т
источник
2

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

Я бы пошел так далеко, что предложил несколько родительских процессоров и попытался бы сохранить одинаковые рабочие места на одном ядре (ах).

фреаки
источник