Лайфхаки

Маленькие, полезные хитрости

Многопоточный python на примерах. Глава 3: Multiprocessing

25.05.2023 в 03:12

Многопоточный python на примерах. Глава 3: Multiprocessing


Шли годы, слава о добром волшебнике росла, а вместе с ней росла и зависть одного нелицеприятного темного волшебника (Саруморта? Или может Воландемана?). Вооруженной немеряной хитростью и движимый завистью, темный волшебник наложил на Дамблдальфа страшное проклятие. Когда проклятие настигло его, Дамблдальф понял, что у него есть всего несколько мгновений, чтобы отразить его. В отчаянии он рылся в своих книгах заклинаний и быстро нашел одно контрзаклятие, которое должно было сработать. Единственная проблема заключалась в том, что волшебнику нужно было вычислить сумму всех простых чисел меньше 1 000 000. Странное, конечно, заклятье, но что имеем.Волшебник знал, что вычисление значения будет тривиальным, если у него будет достаточно времени, но такой роскоши у него не было. Несмотря на то, что он великий волшебник, все же и он ограничен своей человечностью и может проверять на простоту всего одно число за раз. Если бы он решил просто просуммировать простые числа друг за другом, времени ушло бы слишком много. Когда до того, чтобы применить контрзаклятье остались считанные секунды, он вдруг вспомнил заклятье multiprocessing , которое узнал из магического свитка много лет назад. Это заклинание позволит ему копировать себя, чтобы распределить числа между своими копиями и проверять несколько одновременно. И в итоге все, что ему нужно будет сделать – это просто сложить числа, которые он и его копии обнаружат.

Многопоточность python gil. Thread-Safe Code

In a thread-safe program, threads can access the same data structures securely because a synchronization mechanism always keeps data structures in a consistent state. The mechanism Python internally uses to support this synchronization for multithreaded programs is the global interpreter lock (GIL). The GIL’s protection occurs at the interpreter-state level. With the GIL in place, for instance, the integration of non-thread-safe C extension is easier because you can explicitly acquire and release the GIL from the C code, thus making your extension thread-safe at the Python level.

Similarly, the GIL also offers protection for internal C data structures that are heavily used in Python’s memory management . The memory management strategy used in Python requires protection against race conditions, memory leaks, and incorrect release objects. This protection is guaranteed through a mutex (a component of the GIL), which prevents threads from modifying shared data structures incorrectly.

The GIL’s job is to keep internal data structures synced and consistent across all threads sharing the same memory space. The presence of the GIL does not make your Python code thread-safe per se. , like shared instances of a database connection. Neither does it guarantee consistency for instructions like a compound assignment:

x = x + 1

The line above is not atomic, thus, it could be interrupted midway if the thread running it drops the GIL (or if it is forced to do so). If another thread with the GIL modifies the x variable, you will likely get a race condition. In general, only atomic instructions are guaranteed to be thread-safe . For non-atomic instructions, you will need to use a ( or any other synchronization mechanism ), which makes a thread have exclusive access to shared resources at the Python level, making other threads wait until the lock is released.

Python многопоточность и асинхронность. Почему так сложно понять asyncio

Асинхронное программирование традиционно относят к темам для "продвинутых". Действительно, у новичков часто возникают сложности с практическим освоением асинхронности. В случае python на то есть весьма веские причины:

    Асинхронность в python была стандартизирована сравнительно недавно. Библиотекаasyncioпоявилась впервые в версии 3.5 (то есть в 2015 году), хотя возможность костыльно писать асинхронные приложения и даже фреймворки, конечно, была и раньше. Соответственно у Лутца она не описана, а, как всем известно, "чего у Лутца нет, того и знать не надо" .

    Рекомендуемый синтаксис асинхронных команд неоднократно менялся уже и после первого появленияasyncio. В сети бродит огромное количество статей и роликов, использующих архаичный код различной степени давности, только сбивающий новичков с толку.

    Официальная документацияasyncio(разумеется, исчерпывающая и прекрасно организованная) рассчитана скорее на создателей фреймворков, чем на разработчиков пользовательских приложений. Там столько всего — глаза разбегаются. А между тем: "Вам нужно знать всего около семи функций для использования asyncio" (c) Юрий Селиванов, автор PEP 492, в которой были добавлены инструкции async и await

На самом деле наша повседневная жизнь буквально наполнена асинхронностью.

Утром меня поднимает с кровати будильник в телефоне. Я когда-то давно поставил его на 8:30 и с тех пор он исправно выполняет свою работу. Чтобы понять когда вставать, мне не нужно таращиться на часы всю ночь напролет. Нет нужды и периодически на них посматривать (скажем, с интервалом в 5 минут). Да я вообще не думаю по ночам о времени, мой мозг занят более интересными задачами — просмотром снов, например. Асинхронная функция "подъем" находится в режиме ожидания. Как только произойдет событие "на часах 8:30", она сама даст о себе знать омерзительным Jingle Bells.

Иногда по выходным мы с собакой выезжаем на рыбалку. Добравшись до берега, я снаряжаю и забрасываю несколько донок с колокольчиками. И… Переключаюсь на другие задачи: разговариваю с собакой, любуюсь красотами природы, истребляю на себе комаров. Я не думаю о рыбе. Задачи "поймать рыбу удочкой N" находятся в режиме ожидания. Когда рыба будет готова к общению, одна из удочек сама даст о себе знать звонком колокольчика.

Будь я автором самого толстого в мире учебника по python, я бы рассказывал читателям про асинхронное программирование уже с первых страниц. Вот только написали "Hello, world!" и тут же приступили к созданию "Hello, asynchronous world!". А уже потом циклы, условия и все такое.

Но при написании этой статьи я все же облегчил себе задачу, предположив, что читатели уже знакомы с основами python и им не придется втолковывать что такое генераторы или менеджеры контекста. А если кто-то не знаком, тогда сейчас самое время ознакомиться.

Пара слов о терминологии

В настоящем руководстве я стараюсь придерживаться не академических, а сленговых терминов, принятых в русскоязычных командах, в которых мне довелось работать. То есть "корутина", а не "сопрограмма", "футура", а не "фьючерс" и т. д. При всем при том, я еще не столь низко пал, чтобы, скажем, задачу именовать "таской". Если в вашем проекте приняты другие названия, прошу отнестись с пониманием и не устраивать терминологический холивар.

Внимание! Все примеры отлажены в консольном python 3.10. Вероятно в ближайших последующих версиях также работать будут. Однако обратной совместимости со старыми версиями не гарантирую. Если у вас что-то пошло не так, попробуйте, установить 3.10 и/или не пользоваться Jupyter.

Python многопоточность пример. threading — Thread-based parallelism ¶

Source code: Lib/threading.py

This module constructs higher-level threading interfaces on top of the lower levelmodule. See also themodule.

Changed in version 3.7: This module used to be optional, it is now always available.

Note

While they are not listed below, the

Return the currentobject, corresponding to the caller’s thread of control. If the caller’s thread of control was not created through themodule, a dummy thread object with limited functionality is returned.

threading.get_ident ( )

Return the ‘thread identifier’ of the current thread. This is a nonzero integer. Its value has no direct meaning; it is intended as a magic cookie to be used e.g. to index a dictionary of thread-specific data. Thread identifiers may be recycled when a thread exits and another thread is created.

New in version 3.3.

threading.enumerate ( )

Return a list of allobjects currently alive. The list includes daemonic threads, dummy thread objects created by, and the main thread. It excludes terminated threads and threads that have not yet been started.

Return the thread stack size used when creating new threads. The optional size argument specifies the stack size to be used for subsequently created threads, and must be 0 (use platform or configured default) or a positive integer value of at least 32,768 (32 KiB). If size is not specified, 0 is used. If changing the thread stack size is unsupported, ais raised. If the specified stack size is invalid, ais raised and the stack size is unmodified. 32 KiB is currently the minimum supported stack size value to guarantee sufficient stack space for the interpreter itself. Note that some platforms may have particular restrictions on values for the stack size, such as requiring a minimum stack size > 32 KiB or requiring allocation in multiples of the system memory page size - platform documentation should be referred to for more information (4 KiB pages are common; using multiples of 4096 for the stack size is the suggested approach in the absence of more specific information). Availability: Windows, systems with POSIX threads.

Threading python примеры. Python в три ручья: работаем с потоками (часть 1)

В каких случаях вам нужна многопоточность, как реализовать её на Python и что нужно знать о глобальной блокировке GIL.

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

Небольшое предупреждение для тех, кто впервые слышит о параллельных вычислениях. Что такое поток и чем он отличается от процесса, мы выяснили в статье. Тогда мы приводили примеры на Java, но теоретические основы многопоточности верны и для Python. Совпадают, в том числе, механизмы синхронизации потоков: семафоры, взаимные исключения (mutex), условия, события. Поэтому сегодня сделаем акцент на особенностях Python, его механизмах и инструментах, связанных с многопоточностью.

Организовать параллельные вычисления в Python без внешних библиотек можно с помощью модулей:

  • threading — для управления потоками.
  • queue — для организации очередей.
  • multiprocessing — для управления процессами.

Пока нас интересует только первый пункт списка.

Как создавать потоки в Python

Метод 1 — «функциональный»

Для работы с потоками из модуля threading импортируем класс Thread. В начале кода пишем:

from threading import Thread

После этого нам будет доступна функция Thread() — с ней легко создавать потоки. Синтаксис такой:

Первый параметр target — это «целевая» функция, которая определяет поведение потока и создаётся заранее. Следом идёт список аргументов. Если судьбу аргументов (например, кто будет делимым, а кто делителем в уравнении) определяет их позиция, их записывают как args=(x,y). Если же вам нужны аргументы в виде пар «ключ-значение», используйте запись вида kwargs={‘prop’:120}.

Ради удобства отладки можно также дать новому потоку имя. Для этого среди параметров функции прописывают name=«Имя потока». По умолчанию name хранит значение null. А ещё потоки можно группировать с помощью параметра group, который по умолчанию — None.

За дело! Пусть два потока параллельно выводят каждый в свой файл заданное число строк. Для начала нам понадобится функция, которая выполнит задуманный нами сценарий. Аргументами целевой функции будут число строк и имя текстового файла для записи.

Давайте попробуем:

#coding: UTF-8 from threading import Thread def prescript ( thefile , num ): with open (thefile, 'w' ) as f: for i in range (num): if num > 500 : f.write( 'МногоБукв \n ' ) else : f.write( 'МалоБукв \n ' ) thread1Thread(prescript,(,,)) thread2Thread(prescript,(,,)) thread1.start() thread2.start() thread1.join() thread2.join()

Что start() запускает ранее созданный поток, вы уже догадались. Метод join() останавливает поток, когда тот выполнит свои задачи. Ведь нужно закрыть открытые файлы и освободить занятые ресурсы. Это называется «Уходя, гасите свет». Завершать потоки в предсказуемый момент и явно — надёжнее, чем снаружи и неизвестно когда. Меньше риск, что вмешаются случайные факторы. В качестве параметра в скобках можно указать, на сколько секунд блокировать поток перед продолжением его работы.

Метод 2 — «классовый»

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

#coding: UTF-8 import threading class MyThread ( threading.Thread ): def __init__ ( self , num ): super ().__init__( self , name = "threddy" + num) self .numnum():(,.num), thread1MyThread() thread2MyThread() thread1.start() thread2.start() thread1.join() thread2.join()

Стандартные методы работы с потоками

Чтобы управлять потоками, нужно следить, как они себя ведут. И для этого в threading есть специальные методы:

current_thread() — смотрим, какой поток вызвал функцию;

active_count() — считаем работающие в данный момент экземпляры класса Thread;

enumerate() — получаем список работающих потоков.

Ещё можно управлять потоком через методы класса:

is_alive() — спрашиваем поток: «Жив ещё, курилка?» — получаем true или false;

getName() — узнаём имя потока;

setName(any_name) — даём потоку имя;

У каждого потока, пока он работает, есть уникальный идентификационный номер, который хранится в переменной ident .

thread1.start() print (thread1.ident)

Отсрочить операции в вызываемых потоком функциях можно с помощью таймера. В инициализаторе объектов класса Timer всего два аргумента — время ожидания в секундах и функция, которую нужно в итоге выполнить:

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

Многопоточность python multiprocessing. В чём отличие между конкурентным и параллельным выполнением кода?

Начнём с определений:

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

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

Самое главное в этих определениях, их ключевое отличие друг от друга, заключается в словах «в процессе выполнения».

The Art of Concurrency

А теперь перейдём к нашей истории про еду.

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

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

В «Конкурентных бургерах» работает дама среднего возраста. На её руке — татуировка «Python», она во время работы от души хохочет. Она выполняет следующие действия:

    Принимает заказы.

    Переворачивает жарящиеся котлеты для бургеров.

    Накладывает на булочки овощи и котлеты, поливает всё это соусом, выдаёт готовые заказы.

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

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

Главные различия этих двух заведений заключаются в количестве работников и в том, как именно в них решаются различные задачи:

    В «Конкурентных бургерах» одновременно (но не в точности в одно и то же время) выполняется несколько задач. Там имеется единственный работник, который переключается между задачами.

    В «Параллельных салатах» несколько задач решается одновременно, в точности в одно и то же время. Здесь имеется несколько работников, каждый из которых в некий момент времени решает лишь одну какую-то задачу.

Вы замечаете, что и там и там клиентов обслуживают с одинаковой скоростью. Женщина в «Конкурентных бургерах» ограничена скоростью, с которой её небольшой гриль способен жарить котлеты. А в «Параллельных салатах» используется труд нескольких мужчин, каждый из которых занят на одном салате и ограничен временем, необходимым на приготовление салата.

Вам становится понятно, что «Конкурентные бургеры» (Concurrent Burgers) ограничены скоростью подсистемы ввода/вывода (I/O bound), а «Параллельные салаты» (Parallel Salads) ограничены возможностями центрального процессора (CPU bound).

    Ограничения, связанные с подсистемой ввода/вывода — это когда скорость программы зависит от того, насколько быстро происходит чтение данных с диска или выполнение сетевых запросов. «Подсистемой ввода/вывода» в «Конкурентных бургерах» является процесс приготовления котлет.

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

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

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

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

Python многопоточность threading. Основная терминология

Думая над многопоточностью в Python, стоит сначала запомнить ключевую терминологию ЯП. Без нее изучить Питон и участвовать в создании threads не получится. Поэтому сначала необходимо выучить следующие понятия:

  1. Алгоритм – представлен набором инструкций, принципов и правил, которые помогают решать определенные проблемы.
  2. Аргумент – значение, передаваемое в имеющиеся в кодификации команды/функции.
  3. Баг – непредвиденная ошибка, обнаруженная во время работы ПО. Приводит к неполадкам и сбоям в работе контента.
  4. Символы – элементарные единицы отображения информации. Равны одной цифирной/буквенной записи.
  5. Объекты – комбинации связанных констант, переменных, структур информации, которые обрабатываются и выбираются совместно.
  6. Объектно-ориентированное программирование – один из основных принципов создания программ. В его основе лежит программирование через объекты. Логика и абстракции здесь не столь важны.
  7. Класс – набор связанных объектов. Последние обладают одинаковыми (общими) свойствами.
  8. Код – письменный набор инструкций, которые написаны посредством применения протоколов ЯП.
  9. Компиляция – процесс создания исполняемого софта через кодификации.
  10. Массив – списки/группы похожих между собой типов значений данных. Они группируются и образовывают множество.
  11. Итерация – один проход через набор операций, работающих с программным кодом.
  12. Ключевое слово – зарезервированное слово в ЯП. Используется для выполнения определенных задач. Слова из ключевиков не могут служить именами для переменных.
  13. Переменные – места хранения временных данных в приложении. Их можно сохранять, корректировать, а также отображаться при необходимости.
  14. Указатель – переменная, содержащая адрес места в памяти. Местоположение – это начальная точка элемента (массива, целого числа).
  15. Исходные данные – ключевое местоположение, откуда берется и используется электронный материал в пределах утилиты.

Python однопоточный или многопоточный. Coroutines и asyncio

Coroutines (сопрограммы) – альтернативный способ одновременного выполнения функции посредством специальных конструкций, а не системных потоков.

Сопрограмма – общая структура управления, в которой управление потоком совместно передается между двумя подпрограммами без возврата. Получаем однопоточную однопроцессорную конструкцию, использующую кооперативную многозадачность.

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

Реализуем это на Python с помощью библиотекиasyncio, предоставляющей основу и API для запуска и управления сопрограммами с ключевыми словамиasyncиawait.