Учимся писать многопоточные и многопроцессные приложения на Python

Эта статья не для матёрых укротителей Python’а, для которых распутать этот клубок змей — детская забава, а скорее поверхностный обзор многопоточных возможностей для недавно подсевших на питон.

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


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

Многопоточные приложения


В Python есть модуль threading, и в нем есть все, что нужно для многопоточного программирования: тут есть и различного вида локи, и семафор, и механизм событий. Один словом — все, что нужно для подавляющего большинства многопоточных программ. Причем пользоваться всем этим инструментарием достаточно просто. Рассмотрим пример программы, которая запускает 2 потока. Один поток пишет десять “0”, другой — десять “1”, причем строго по-очереди.

import threading

def writer(x, event_for_wait, event_for_set):
    for i in xrange(10):
        event_for_wait.wait() # wait for event
        event_for_wait.clear() # clean event for future
        print x
        event_for_set.set() # set event for neighbor thread

# init events
e1 = threading.Event()
e2 = threading.Event()

# init threads
t1 = threading.Thread(target=writer, args=(0, e1, e2))
t2 = threading.Thread(target=writer, args=(1, e2, e1))

# start threads
t1.start()
t2.start()

e1.set() # initiate the first event

# join threads to the main thread
t1.join()
t2.join()

Никакой магии и voodoo-кода. Код четкий и последовательный. Причем, как можно заметить, мы создали поток из функции. Для небольших задач это очень удобно. Этот код еще и достаточно гибкий. Допустим у нас появился 3-й процесс, который пишет “2”, тогда код будет выглядеть так:

import threading

def writer(x, event_for_wait, event_for_set):
    for i in xrange(10):
        event_for_wait.wait() # wait for event
        event_for_wait.clear() # clean event for future
        print x
        event_for_set.set() # set event for neighbor thread

# init events
e1 = threading.Event()
e2 = threading.Event()
e3 = threading.Event()

# init threads
t1 = threading.Thread(target=writer, args=(0, e1, e2))
t2 = threading.Thread(target=writer, args=(1, e2, e3))
t3 = threading.Thread(target=writer, args=(2, e3, e1))

# start threads
t1.start()
t2.start()
t3.start()

e1.set() # initiate the first event

# join threads to the main thread
t1.join()
t2.join()
t3.join()

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

Global Interpreter Lock


Существуют две самые распространенные причины использовать потоки: во-первых, для увеличения эффективности использования многоядерной архитектуры cоврменных процессоров, а значит, и производительности программы;
во-вторых, если нам нужно разделить логику работы программы на параллельные полностью или частично асинхронные секции (например, иметь возможность пинговать несколько серверов одновременно).

В первом случае мы сталкиваемся с таким ограничением Python (а точнее основной его реализации CPython), как Global Interpreter Lock (или сокращенно GIL). Концепция GIL заключается в том, что в каждый момент времени только один поток может исполняться процессором. Это сделано для того, чтобы между потоками не было борьбы за отдельные переменные. Исполняемый поток получает доступ по всему окружению. Такая особенность реализации потоков в Python значительно упрощает работу с потоками и дает определенную потокобезопасность (thread safety).

Но тут есть тонкий момент: может показаться, что многопоточное приложение будет работать ровно столько же времени, сколько и однопоточное, делающее то же самое, или за сумму времени исполнения каждого потока на CPU. Но тут нас поджидает один неприятный эффект. Рассмотрим программу:

with open('test1.txt', 'w') as fout:
    for i in xrange(1000000):
        print >> fout, 1

Эта программа просто пишет в файл миллион строк “1” и делает это за ~0.35 секунды на моем компьютере.

Рассмотрим другую программу:

from threading import Thread

def writer(filename, n):
    with open(filename, 'w') as fout:
        for i in xrange(n):
            print >> fout, 1

t1 = Thread(target=writer, args=('test2.txt', 500000,))
t2 = Thread(target=writer, args=('test3.txt', 500000,))

t1.start()
t2.start()
t1.join()
t2.join()

Эта программа создает 2 потока. В каждом потоке она пишет в отдельный файлик по пол миллиона строк “1”. По-сути объем работы такой же, как и у предыдущей программы. А вот со временем работы тут получается интересный эффект. Программа может работать от 0.7 секунды до аж 7 секунд. Почему же так происходит?

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

UPD: на данный момент в Python 3.2 существует улучшенная реализация GIL, в которой эта проблема частично решается, в частности, за счет того, что каждый поток после потери управления ждет небольшой промежуток времени до того, как сможет опять захватить GIL (на эту тему есть хорошая презентация на английском)

«Выходит на Python нельзя писать эффективные многопоточные программы?», — спросите вы. Нет, конечно, выход есть и даже несколько.

Многопроцессные приложения


Для того, чтобы в некотором смысле решить проблему, описанную в предыдущем параграфе, в Python есть модуль subprocess. Мы можем написать программу, которую хотим исполнять в параллельном потоке (на самом деле уже процессе). И запускать ее в одном или нескольких потоках в другой программе. Такой способ действительно ускорил бы работу нашей программы, потому, что потоки, созданные в запускающей программе GIL не забирают, а только ждут завершения запущенного процесса. Однако, в этом способе есть масса проблем. Основная проблема заключается в том, что передавать данные между процессами становится трудно. Пришлось бы как-то сериализовать объекты, налаживать связь через PIPE или друге инструменты, а ведь все это несет неизбежно накладные расходы и код становится сложным для понимания.

Здесь нам может помочь другой подход. В Python есть модуль multiprocessing. По функциональности этот модуль напоминает threading. Например, процессы можно создавать точно так же из обычных функций. Методы работы с процессами почти все те же самые, что и для потоков из модуля threading. А вот для синхронизации процессов и обмена данными принято использовать другие инструменты. Речь идет об очередях (Queue) и каналах (Pipe). Впрочем, аналоги локов, событий и семафоров, которые были в threading, здесь тоже есть.

Кроме того в модуле multiprocessing есть механизм работы с общей памятью. Для этого в модуле есть классы переменной (Value) и массива (Array), которые можно “обобщать” (share) между процессами. Для удобства работы с общими переменными можно использовать классы-менеджеры (Manager). Они более гибкие и удобные в обращении, однако более медленные. Нельзя не отметить приятную возможность делать общими типы из модуля ctypes с помощью модуля multiprocessing.sharedctypes.

Еще в модуле multiprocessing есть механизм создания пулов процессов. Этот механизм очень удобно использовать для реализации шаблона Master-Worker или для реализации параллельного Map (который в некотором смысле является частным случаем Master-Worker).

Из основных проблем работы с модулем multiprocessing стоит отметить относительную платформозависимость этого модуля. Поскольку в разных ОС работа с процессами организована по-разному, то на код накладываются некоторые ограничения. Например, в ОС Windows нет механизма fork, поэтому точку разделения процессов надо оборачивать в:

if __name__ =='__main__':

Впрочем, эта конструкция и так является хорошим тоном.

Что еще ...


Для написания параллельных приложений на Python существуют и другие библиотеки и подходы. Например, можно использовать Hadoop+Python или различные реализации MPI на Python (pyMPI, mpi4py). Можно даже использовать обертки существующих библиотек на С++ или Fortran. Здесь можно было упомянуть про такие фреймфорки/библиотеки, как Pyro, Twisted, Tornado и многие другие. Но это все уже выходит за пределы этой статьи.

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

Подробнее
Реклама
Комментарии 85
  • 0
    Не проще mpich2 использовать?
    • +4
      За старание плюсик. А так — слишком много слов для того, чтобы показать как создавать и запускать поток.
      Судя по оговоркам, автор знает гораздо больше чем пишет :)
      • +2
        Да, пожалуй, но хотелось всего по чуть-чуть ухватить.
        • 0
          Присоединяюсь к предыдущему комментатору. Очень хороший обзор, мне, как начинающему питонисту, пригодится на будущее, думаю. Но примеров на multiprocessing и subprocess (хотя там непонятно, что там писать) для полноты картины не хватает.
          • +1
            Я, честно говоря, не ожидал, что эта тема стоит настолько остро. В следующих статьях я постараюсь подробнее рассказать о каждом виде параллелизма стандартными средствами питона.
            • +1
              Уже жду с нетерпением.
              • 0
                Спасибо за статью. Прошло 2 года. Мы все еще ждем продолжения.
                • 0
                  Привет из 2015 года. Все еще ждем.
                  • –1
                    Привет из 2016 года. Видимо, вы хер положили на эту тему, но многие все еще ждут.
                    • 0
                      Надеюсь к 2017 вы смиритесь или найдете другие источники.
                • –1
                  А мы все еще ждем
          • НЛО прилетело и опубликовало эту надпись здесь
            • –3
              1) Да, это не совсем гонка, но эффект похожий.
              2) Точно оценивать производительность задачи не стояло. Хотелось придумать показательный пример, но при этом максимально простой. Как мне кажется для академических целей вполне пойдет.
              • НЛО прилетело и опубликовало эту надпись здесь
                • 0
                  Момент спорный, но вы точно правы в том, что такой пример может только запутать.
                  Упоминание про гонку я убрал из статьи.
            • НЛО прилетело и опубликовало эту надпись здесь
              • +1
                Пожалуй я неправильно выразил мысль. Это не GIL лучше, а без GIL пока не получается лучше.

                Еще тогда Гвидо сказал, что с радостью уберет GIL, если кто-то предложит реализацию без GIL, но такую, чтобы она не ухудшала производительность однопоточных программ. С тех пор ситуация, вроде бы, не сильно изменилась.
                • 0
                  Если честно — то GIL таки довольно серьёзно улучшился в 3.2
                  • 0
                    Да, я так поглядываю мельком. Там вроде бы появилась хитрая оптимизация с задержками на 5 миллисекунд, но пока Python 3 для многих не актуален и приходится работать с тем что есть сейчас =)

                    Но с другой стороны я к GIL отношусь философски: если производительность сильно критична, то я могу написать критические места на Си, но пока на моих задачах куда важнее надежность и предсказуемость кода.
                  • НЛО прилетело и опубликовало эту надпись здесь
                    • +5
                      1. Global Lock — необходимое зло. Пока от него не удается избавится.
                      2. В 3.2 GIL переписали. Он никуда не делся, но стало гораздо меньше холостых захватов в eval loop:
                      когда поток захватывает GIL, понимает что вообще-то он не должен работать потому что в блокировке и отпускает GIL. Раньше подобное случалось часто, теперь GIL не дергается если переключать поток не нужно. Вот здесь asvetlov.blogspot.com/2011/07/gil.html я описывал как оно сейчас работает, а здесь перевод Beazley habrahabr.ru/post/84629/ — почему раньше работало хуже.

                      Вопль «какого чёрта» улыбнул.
                      1. Если что, даже в glibc есть global state, защищенный блокировками. Пример — менеджер памяти, все эти malloc() и free().
                      2. Глобальное состояние интерпретатора имеет пару дюжин разных локов. import, zlib и io — у каждого свои блокировки помимо GIL.
                      3. Программы, не требующие явной синхронизации, возможны. Правда, в любом нетривиальном случае их приходится писать или очень аккуратно, или выбирая необычные методики (я имею в виду Erlang в первую очередь). С Питоном так не получится.
                      4. Новый GIL работает на CAS'ах если что.

                      Итого. GIL — не манна небесная. Решает часть проблем, выдвигает новые. Питон от версии к версии старается сделать работу с потоками лучше. Есть над чем потрудиться.
                      Быстрее с GIL не выйдет при всём желании. Вопрос лишь в том, насколько получается медленней при разных реализациях.
                      • НЛО прилетело и опубликовало эту надпись здесь
                        • +1
                          Используйте поменьше пафоса, пожалуйста. И приводите примеры.
                          Мне трудно обсуждать анонимную нормальную платформу. Готов заранее признать, что эта невидимка не имеет ни одной проблемы и замечательно подходит для всех мыслимых случаев сейчас и в необозримом будущем.
                          • НЛО прилетело и опубликовало эту надпись здесь
                            • НЛО прилетело и опубликовало эту надпись здесь
                              • –3
                                Жесть. При чем тут Java? Она тоже является интерпретатором?
                                • НЛО прилетело и опубликовало эту надпись здесь
                                  • –4
                                    Вы считаете Гвидо и разработчики Питона тупее вас?
                                    • НЛО прилетело и опубликовало эту надпись здесь
                                      • 0
                                        А кто говорит что GIL ускоряет однопоточные программы?

                                        Просто сравнивать Jav-у, С++ с Питоном нет смысла — это разные вещи. Питон — четкий однопоточный интерпретатор, по-крайней мере в классической реализации.

                                        GIL — это просто решение, позволяющее разработчику абстрагироваться от потокобезопасности нативного кода, стоящего за инструкциями Питона. Для интерпретируемого языка для общих задач — вполне нормальное решение.
                                        • НЛО прилетело и опубликовало эту надпись здесь
                                          • 0
                                            В Питоне принято для максимальной загрузки CPU масштабировать на процессы. Это такая альтернатива. Посмотрите на Google Chrome как наиболее доступный пример масштабирования процессов.
                                            • НЛО прилетело и опубликовало эту надпись здесь
                                              • 0
                                                Что вы подразумеваете под «вертикальной масштабируемостью»?
                                                • НЛО прилетело и опубликовало эту надпись здесь
                                                  • –2
                                                    Потоки тяжелее в обслуживании чем процессы, потому что требуют затрат на синхронизацию адресного пространства процесса, в котором они работают.
                                                    • 0
                                                      Интересное высказывание, если учесть, что потоки обычно определяют как легковесные процессы.

                                                      Что значит «синхронизация адресного пространства»? Если я правильно понял, то скорее всего эта синхронизация нужна для данных, общих для нескольких потоков. А если есть общие данные, то странно использовать для скалирования отдельные процессы: что-то мне подсказывает, что IPC окажется дороже, чем «синхронизация адресного пространства».
                                                      • 0
                                                        IPC, конечно же дороже. Процессы в этом случае не используют. Процессы используют, например, при организации архитектуры сервера веб-приложений для параллельной обработки потока запросов.

                                                        Я просто о том, что использование нельзя использовать нативные потоки для реализации параллельных алгоритмов — когда вы пытаетесь запустить их несколько тысяч, затраты процессорного времени на их запуск и обслуживание нивелируют весь профит от их использования. А если их ещё и синхронизировать надо, то тут вообще всё плохо: синхронизация потоков — «тяжёлая» операция. Для таких вещей используют CUDA, OpenCV или OpenMP, которые позволяют запускать процедуры в чисто параллельном режиме. Но и там куча своих ограничений.
                                                        • 0
                                                          когда вы пытаетесь запустить их несколько тысяч, затраты процессорного времени на их запуск и обслуживание нивелируют весь профит от их использования

                                                          … А для отдельных процессов этот оверхэд будет куда больше, особенно на создание.

                                                          Я не очень понял, при чём тут CUDA и Ко: мы, вроде бы, обсуждали производительность параллельных вычислений при использовании потоков или процессов.

                                                          И да, что за операция такая — «синхронизация потоков»? И почему она дорогая?
                                                • 0
                                                  Честно сказать, мне потоки тоже кажутся костылем.
                                                  Структурно стройное решение — акторы, взаимодействие на передаче сообщений на не на разделяемой памяти.
                                                  Только не доросли мы до массового применения такого подхода.
                                                  • НЛО прилетело и опубликовало эту надпись здесь
                                                    • 0
                                                      Реальное железо ничего не знает о переменных, классах и байткоде, который крутится внутри виртуальной машины. Так что не вижу большого противоречия.
                                                      • 0
                                                        Хм, представьте себе одноядерную однопроцессорную машину. Что будет быстрее выполняться на этой машине: программа которая исполняет параллельный алгоритм в многопоточном режиме или программа которая исполняет этот же алгоритм в последовательном режиме.
                                                • 0
                                                  Да что ж вы всё потоки да потоки :)

                                                  Процессами всё масштабируется. В веб-приложениях это прекрасно работает.
                                                  • 0
                                                    О том и статья: потоки в питоне не про производительность на мультиядерных системах.
                                                    • 0
                                                      Ну а производительные многопоточные решения на мультиядерных системах никто на Питоне и не пишет: для это есть C, Go и Java, CUD'ы всякие, OpenMP и прочее.
                                              • 0
                                                А вто это уже правильное замечание.
                                                И проблема не только в том чтобы убрать GIL. Нужно еще на поломать существующие библиотеки — иначе этот язык будет называться уже не Питоном.
                                                • НЛО прилетело и опубликовало эту надпись здесь
                                                  • 0
                                                    Есть библиотеки, написанные на чистом Питоне — и там более или менее всё хорошо. А есть использующие C Extensions. Таких очень много, а стоимость их сопровождения гораздо выше чем pure python.
                                                    И C Extensions на Jython или IronPython подключать нельзя.
                                                    В pypy появилась экспериментальная поддержка. Работает почти всегда, но ужасно медленно.
                                        • –1
                                          Завидую вашей непосредственности.
                                          Вы очень непринужденно смешиваете виртуальные машины с языками программирования, произвольно применяете термин «многопоточность» там, где подразумевается более общий «параллелизм».
                                          Многопоточные PHP, PERL и Erlang — мне понравилось.
                                          Какое виртуозное жонглирование терминами!
                                          • 0
                                            А в чем вы здесь видите некорректность? Вопрос был поставлен так: почему в 2012 году, когда космические корабли даже в моем нетбуке 4 ядра, в RI реализации одного из популярнейших языков программирования до сих пор живет такой ужас царизма как GIL. И не просто живет, а еще и считается чуть ли не священной коровой, без которой этот язык жить не будет.

                                            В ответ идет какой-то не очень текст, что, мол, иначе нельзя — не получится написать интерпретатор без GIL. Оппонент приводит вам пример: вот есть интерпретаторы, как JVM, CLR. Они уже давно очень даже неплохо обходятся без GIL. Позволяют писать действительно параллельный код. То, что JVM интерпретирует не саму яву, а байт-код — это ни разу не принципиально в данном контексте: вы легко можете дописать javac на самой яве, и запустить в той же JVM. В свое время контейнеры сервлетов так делали для исполнения JSP (может, и сейчас делают — не знаю) — компилировали JSP в сервлет на яве, компилировали его в байт-код, и загружали получаемый класс. Многие интерпретируемые языки поверх JVM опускают (позволяют опускать) явный этап компиляции в байт-код: я могу навскидку вспомнить BeanShell и JavaScript (rhino). Да и тот же самый Jython, кстати.

                                            В этом смысле мне лично не очень понятно, что именно вы имеете против сравнения Python RI с JVM, CLR PHP/Perl runtime. По-моему, это вполне легальное сравнение, которое наглядно показывает, что интерпретатор без GIL более чем реален, и очень даже производительный (если сомневаетесь — можете сравнить Python RI с JVM :)
                                            • 0
                                              Вот так — правильно. Сравнивать нужно VM CPython, JVM и CLR.
                                              Говорить о С/С++ не следовало. Совсем другая область. Даже не из-за байткода и VM а из-за отсутствия garbage collector.
                                              PHP, PERL и Erlang тоже не следовало приводить в качестве примера «правильных» и «нормальных» реализаций — они несколько вольно работают с потоками. Если брать за идеал JVM или CLR — Erlang и PERL убоги.

                                              Если говорить, что VM CPython хуже JVM потому что не умеет делать честную правильную многопоточность — соглашусь. Другой вопрос, что это ограничение можно обойти, что и делают если нужно особо не напрягаясь.
                                              • 0
                                                Я не думаю что GIL считается «священной коровой»: просто была бы возможность сделать pure-параллельный Питон, без GIL, нативно написанный на C (а не «слоёный», как JPython и IronPython), то её реализуют. Просто сейчас наличие GIL не считается вселенским злом, которое мешает жить Питон-программистам. Ну есть GIL, и есть. Язык отличный, приложения на нём пишутся и прекрасно работают. А если надо реализовать какое-нибудь более производительное решение — берётся C и реализуется.
                                                • 0
                                                  Насколько я понимаю, просто GIL был в питоне так долго, что теперь его не так-то просто выковырять. Т.е. это беда RI, да, но а) решить ее не так-то просто б) за долгое время питонисты научились с этим жить, поэтому особо и не жалуются. Как следствие — решать эту проблему особо и не торопятся. Ну и, все-таки, у опен-сорс коммьюнити питона нет таких ресурсов, как у Sun/MS, чтобы потратить десятки человеко-лет на написание высокопроизводительной VM.
                                                  • 0
                                                    Ну кто его знает. Может Гвидо и Ко уже вынашивают в своих головах планы на 4-й Питон: с блэкджеком без GIL'а и с потоками :)
                                                    • 0
                                                      Абсолютно верно по всем пунктам.
                                              • 0
                                                Ну, справедливости ради, в JVM тоже куча важного делается только на savepoint-ах, в режиме «вселенная подождет».

                                                Вообще, доколе Azul будет хвастаться полностью параллельным GC, а Sun, создатель явы, будет допиливать state-of-art G1, который все еще требует stop-the-world?

                                                Не надо обижать маленьких :) Кто-то писал, что «concurrent programming is hard» — вот в питоне без него и обходятся, так проще :)
                                                • 0
                                                  savepoint
                                                  Когда-когда, говорите, ваша секретарша принимает обращения по подобным вопросам? :P
                                        • +1
                                          насколько я понял GIL присутствует не во всех реализациях питона, но там, где он есть, полноценной многопоточности не будет, как бы быстр не был GIL.

                                          http://lua-users.org/wiki/LuaVersusPython
                                          • 0
                                            Верно. Если уж упомянут lua, но в нём можно запустить несколько потоков, но я бы не назвал это мультипоточной моделью исполнения. Lua запускает новый интерпретатор в каждом потоке, по одной независимой виртуальной машине на поток (с возможностью кроссинтерпретаторного взаимодействия). Так же работает и Perl.
                                            Это другая модель параллельных вычислений, имеющая как достоинства так и недостатки.
                                            • +1
                                              глубоко я не копал, но ничего страшного нет в этом, луа сравнительно легкий интерпретатор, а кроссинтерпретаторное взаимодействие другими словами — это взаимодействие между потоками, при чем синхронизация будет только там, где надо (например, защита данных). Кроме этого, есть выбор: запускать интерпретатор связав с уже существующими(передав lua_State) или без связи(независимо). В питоне же один GIL на весь процесс.
                                    • 0
                                      Спасибо за статью, совсем недавно столкнулся с этой проблемой, как-то сам дошел, что нужно использовать multiprocessing, но шел долго, теперь знаю почему так
                                      • 0
                                        Спасибо большое за статью. Стоит заметить, что многопоточности питона хватает выше крыши, для того чтобы распараллелить эмуляцию действий различных пользователей на сайте, а также тестирование любого массового сетевого взаимодействия. Мало того, тут GIL даже помогает, мы видим в каком потоке остановились во время отладки и видим стек. Насколько я помню ограничения GIL позволяют сериализовать состояние потока, включая его имя, а также позволяет спокойно отлаживаться в одном потоке, так что остальные потоки в этот момент не мешают.
                                        • 0
                                          Спасибо за статью, жаль только, что нет примеров использования multiprocessing.
                                          • 0
                                            Программа с тредами запущенная руками из консоли не обрабатывает ctrl+c
                                            Как ее научить этому?
                                          • 0
                                            Писал многопоточный скрипт на python, экспериментальным путем было установлено, что потоки не раскидываются на ядра. Вы уверены, что за это отвечает ОС?
                                            • 0
                                              из ссылки выше:
                                              When any one specific thread is running in Python code though it will acquire the GIL, in doing this you lock out any other threads which need to have the GIL at that time when running. Thus, although you can have multiple threads at the same time, for Python bound code you can't effectively run them concurrently. This means for example that two Python threads can't at the same time make use of two distinct CPUs or cores in the system.
                                              • 0
                                                Доки я читал, спасибо. Вообще, не раз встречал мнение программистов на питоне, что потоки реализованы «чтобы было». Типа как «go to» в ЯП. Меня скорость работы моей программы, разбитой на потоки, совсем не порадовало. И, вы не поверите, задача была решена на perl.
                                                • 0
                                                  1) Вы так говорите, как будто я раскаленными вилами вас пытаю и заставляю писать на Python, причем абсолютно все.

                                                  2) В Python потоки изначально не создавались ради производительности. Они для создания асинхронных I/O и вызовов внешних процессов. Если проблема в этом, то в питоне есть куча методов решения вопросов производительности. Если просто не нравится питон, то хозяин барин.
                                                  Лично я критичное по производительности приложение буду писать на си/с++.
                                                  Если мне просто надо написать тупой мониторинг 10 серверов, то можно писать хоть на брейнфаке (я кстати иногда такие вещи пишу на Прологе, работает медленно, зато пишется в 2-3 строки то, что даже на перле может занять строк 20).

                                                  3) Перл я уважаю, хороший язык писал на нем лет 10 назад. Питон изучал параллельно. Сложно сказать, почему я в итоге выбрал питон. Сейчас-то комьюнити у питона просто огромно, а тогда перл был круче и считался чуть ли не совершенным языком (ну или может быть я идеализирую, мне тогда было 14 лет :)
                                                  • 0
                                                    Первый человек, которому нужна производительность многопоточного приложения и выбирающий между Python и Perl. При желании можно создать неуправляемый поток ОС напрямую через API платформы. Причём я бы посоветовал писать C-extension, если уж нужна и производительность и функционал питона.
                                                    • 0
                                                      Нет, в том проекте мне нужна не производительность, а одновременность выполнения некоторых длительных действий. Т.е. чтобы несколько сотен потоков крутились асинхронно, что, как мы знаем, в силу GIL невозможно.
                                                      • 0
                                                        Возможно
                                                        • 0
                                                          Как?
                                                          • 0
                                                            Потоки в Питоне реализованы асинхронно. Просто из-за того что интерпретатор работает в один поток, ему приходится переключаться между потоками в ходе работы. Вот для этого и реализован GIL — инструкция в коде потоке захватывает GIL, выполняется, отпускает GIL. А сами потоки при этом параллельны.

                                                            Это похоже на то, как работает операционная система на одноядерной однопроцессорной машине: там ведь тоже потоки не работают в настоящем параллельном режиме: процессор переключается между ними по очереди согласно их приоритету.
                                                            • 0
                                                              Ну, собственно, я это и имел виду. Нам-то от такой асинхронности ни жарко, ни холодно.
                                                              • 0
                                                                А вы думаете что даже если на 8-ядерной машине запустите 100 потоков они у вас будут в чисто параллельно работать?
                                                                • 0
                                                                  Конечно, не «чисто», но гораздо быстрее. Повторюсь, одна и та же задача решалась на питоне и перле. Перл вин :)
                                                          • 0
                                                            Еще можно, например, реализовать внутренность потоков на с/с++ и спилить их в модуль (если использовать SWIG, то это делается очень просто).

                                                            А потом запустить что-то такое:
                                                            from threading import Thread
                                                            from test_loop import loop
                                                            
                                                            threads = [Thread(target=loop) for _ in xrange(1000)]
                                                            map(lambda x: x.start(), threads)
                                                            map(lambda x: x.join(), threads)
                                                            


                                                            Внутри loop у меня что-то типа алгоритма Флоида крутится. 1000 потоков весьма бодро грузят ядра.
                                                            В каждом потоке по 8'000'000 итераций цикла. Код отрабатывает за 18-19 сек.

                                                            Вообще лично мой рецепт такой:

                                                            Да, возможно на Perl вы выиграете на тридах производительности на 10% (хотя даже в это я не поверю) по сравнению с многопроцессной реализацией на Python (в которой нет GIL).

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

                                                            Да, может быть этот подход не достаточно гибкий, но хорошо писать на 2-3 языках уже сложно, а знать больше вообще тяжело (я думаю понятно, что речь не о Hello world). Мой джентльменский набор C/C++/Python/JS/Bash.
                                                            Да, я знаю, что инструмент подбирается под задачу, но переход на другой инструмент это время и деньги. Куда проще и дешевле просто купить лишний сервер и написать изначально гибкое и расширяемое решение (в чем питону вообще мало равных).

                                                            И да, все что я тут написал — мое ИМХО, основанное на личном опыте. Если кто-то думает иначе — я с удовольствием выслушаю.
                                              • 0
                                                Спасибо за интересную заметку. До недавнего времени я не знал, что в CPython есть настоящая многопоточность. Если бы я писал книжки по Python, то говорил бы о multiprocessing еще в введении и выделял это жирным шрифтом. Еще мне всегда было интересно, как программисты на Python ищут документацию. Вся документация по Perl к примеру находится в одном месте — metacpan.org/ Если же посмотреть на PyPI, то у половины модулей никакой документации нет вообще (даже если установить модуль и посмотреть в pydoc). Может быть, есть какой-то специальное место для поиска документации по модулям, доступных через PyPI?

                                                Хорошо пишите, продолжайте в том же духе.
                                                • НЛО прилетело и опубликовало эту надпись здесь
                                                • 0
                                                  Жаль что примеры кода есть только по самым простым и «плохим» реализациям мультипоточности. Неплохо было бы добавить примеров использования модулей subprocess и multiprocessing в соответствующих параграфах статьи.

                                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.