Python — Многозадачность, конкурентность и асинхронность

Table of Contents

Подборка материалов для освоения темы многозадачности в Python

YouTube ролики

YouTube English:

Статьи

Введение в Python

Исходный глоссарий

Виртуальное адресное пространство

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

Структура виртуального адресного пространства

Python не управляет адресным пространством напрямую — он запрашивает память у ОС через malloc, mmap, brk.

Heap & Stack

Стек(stack) и куча(heap) – области в оперативной памяти (ОЗУ, RAM), в которых хранятся данные приложения во время его выполнения. Управление оперативной памятью для приложения Python осуществляется с помощью Python memory manager.

В управлении памятью (Python memory management) существует механизм учёта ссылок (reference counting), который ведет внутренний журнал того, как много ссылок ссылается на объект в куче. Когда на объект не ссылается ни одна ссылка сборщик мусора (Garbage collector) автоматически освобождает память выделенную ранее для этого объекта.

Heap — область памяти процесса, предназначенная для динамического выделения памяти во время выполнения.

В Python:

  • все объекты Python живут в heap
  • int, list, dict, class, function — всё heap

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

Python stack — это логическая абстракция, а не «настоящий» stack ОС.

Дескрипторы ресурсов (File Descriptors)

File descriptor — это целое число, которое операционная система даёт твоей программе, когда она открывает файл или другое устройство ввода-вывода (например, сокет, pipe). Это как минимальный идентификатор ресурса: Python использует его для низкоуровневых операций с файлами.

  • Это не объект Python, это число, под которым ОС видит открытый файл/ресурс.
  • С помощью FD можно делать низкоуровневые операции (чтение, запись, дупликация, перемещение позиции и т.п.).
  • Отличие от обычного open() в том, что FD используют функции модуля os, а не методы объекта файла.

Каждый FD — ограниченный ресурс. Если ты открыл много файлов или сокетов и не закрыл их, система закончится и новые операции упадут с ошибками вроде Too many open files. Это особенно критично для серверов, которые держат много соединений одновременно.

В Unix-системах всё представляется как файл. Стандартные дескрипторы:

  • 0 — stdin
  • 1 — stdout
  • 2 — stderr

Ты можешь перенаправлять их (например, в скриптах bash или в приложениях), и это тоже работает через FD.

Примеры ресурсов: файлы, сокеты, pipe, eventfd, epoll/kqueue

Глобальные переменные в Python

Глобальные переменные — это имена, привязанные в namespace модуля.

В реальности:

  • имя x → указатель
  • объект 10 → heap
  • namespace модуля → dict в heap

Регистры CPU

Регистры CPU — сверхбыстрая память внутри процессора.

Хранят: указатель инструкции (IP), указатель стека (SP), флаги, временные значения.

Python не управляет регистрами напрямую. Но при context switch ОС сохраняет регистры, при переключении потоков Python → регистры меняются. Это основная стоимость context switch.

User Space

User space — режим выполнения с ограниченными правами.

Python-код выполняется исключительно в user space.

Запрещено:

  • прямой доступ к устройствам
  • управление памятью
  • прерывания

Kernel Space

Kernel space — привилегированный режим выполнения.

Ядро:

  • управляет памятью
  • планирует процессы
  • обрабатывает I/O
  • управляет сетевым стеком

Что такое процесс, поток, системный вызов и context switch?

Процесс — это изолированное выполняемое окружение, предоставляемое ОС. Каждый процесс имеет собственное виртуальное адресное пространство, heap, stack, дескрипторы ресурсов (файлы, сокеты).

Поток (Thread) — это единица выполнения внутри процесса. Потоки разделяют одно адресное пространство процесса, каждый поток имеет собственный stack, выполняются псевдопараллельно внутри 1 процесса. Общее у потоков heap, глобальные переменные, объекты Python. Раздельное — stack, регистры CPU.

Системный вызов — это контролируемый переход из user space в kernel space.

Python-код не может напрямую:

  • читать диск
  • писать в сокет
  • создавать процесс
  • спать

Что происходит при системном вызове:

  • Python вызывает C-функцию
  • C-функция делает syscall
  • ОС выполняет операцию
  • Поток блокируется, пока ОС не закончит

В этот момент:

  • GIL может быть освобождён
  • другой поток может выполняться

Context switch — это переключение CPU с одной задачи на другую.

Бывает:

  • между потоками
  • между процессами

Что сохраняется:

  • регистры CPU
  • указатель стека
  • состояние планировщика

Архитектура CPython

CPython — это эталонная реализация языка программирования Python. Это версия Python по умолчанию, наиболее широко используемая и оригинальная реализация, написанная преимущественно на языке C.

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

  1. Исходный код (Source code) – mymodule.py преобразуется в байт-код с помощью компилятора (compiler) Python
  2. Байт-код (Byte code) сохраняется в определенном формате (.pyc, .pyo, .pyd) – mymodule.pyc
  3. Виртуальная машина Python (или PVM) получает байт-код и с помощью интерпретатора преобразует его в бинарный код.
  4. Бинарный или машинный код (Binary code)
  5. Компьютер читает бинарный код и выполняет программу

Важно понимать разницу между языком Python и интерпретатором CPython. Язык Python — это набор правил и синтаксиса (описанных в документации), а CPython конкретная программа, исполняющая код на этом языке.


Python — язык, а CPython — его основной движок.


Что такое PVM?

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

Компилятор Python выполняет ту же задачу, но несколько иным образом. Он преобразует исходный код программы в другой вид кода, называемый байт-кодом. Каждая инструкция программы на Python преобразуется в набор инструкций байт-кода.

Виртуальная машина Python (Python Virtual Machine, PVM) принимает этот байт-код и преобразует его в машинный код, чтобы компьютер мог выполнить соответствующие инструкции и вывести итоговый результат. Для выполнения этого преобразования PVM оснащена интерпретатором. Интерпретатор преобразует байт-код в машинный код и передаёт этот машинный код процессору компьютера для выполнения. Поскольку именно интерпретатор играет ключевую роль, виртуальную машину Python часто также называют интерпретатором.

Альтернативные реализации

Хотя CPython является стандартной реализацией, существуют и другие реализации Python, созданные для конкретных задач, таких как повышение производительности или интеграция с другими платформами:

  • PyPy — использует компиляцию Just-In-Time (JIT), что позволяет во многих случаях выполнять Python-код значительно быстрее, чем в CPython.
  • Jython — написан на Java и компилирует Python-код в байткод Java, что позволяет запускать Python на виртуальной машине Java (JVM) и взаимодействовать с библиотеками Java.
  • IronPython — реализован для Common Language Infrastructure (CLI), благодаря чему может работать на платформе .NET.
  • MicroPython / CircuitPython — оптимизированные реализации, предназначенные для микроконтроллеров и встраиваемых систем.

Производительность

Те, кто имеют опыт работы с компилирующими языками программирования, такими как C и C++, могут заметить несколько отличий в модели выполнения Python.

  • Первое, что бросается в глаза, – это отсутствие этапа сборки, или вызова утилиты «make»: программный код может запускаться сразу же, как только будет написан.
  • Второе отличие: байт код не является двоичным машинным кодом (например, инструкциями для микропроцессора Intel). Байт код – это внутреннее представление программ на языке Python.

По этой причине программный код на языке Python не может выполняться так же быстро, как программный код на языке C или C++. Обход инструкций выполняет виртуальная машина, а не микропроцессор, и чтобы выполнить байт код, необходима дополнительная интерпретация, инструкции которого требуют на выполнение больше времени, чем машинные инструкции микропроцессора. С другой стороны, в отличие от классических интерпретаторов, здесь присутствует дополнительный этап компиляции – интерпретатору не требуется всякий раз снова и снова анализировать инструкции исходного текста. В результате Python способен обеспечить скорость выполнения где то между традиционными компилирующими и традиционными интерпретирующими языками программирования.

GIL (Global Interpreter Lock)

GIL (Global Interpreter Lock) — интерпретатор Python однопоточный в том смысле, что в каждый момент времени может выполняться только один участок байт-кода, даже если в процессе работает несколько потоков. Глобальная блокировка интерпретатора не позволяет выполнять несколько потоков одновременно.

Python может освободить GIL на время выполнения операций ввода-вывода (I/O Bound), потому что для выполнения ввода-вывода вызывается низкоуровневая функция операционной системы. Эти функции работают за пределами интерпретатора, т. е. никак не могут повредить его внутренние структуры, от чего и призвана защитить GIL.

GIL был введён для упрощения управления памятью в Python, поскольку многие внутренние операции, такие как создание объектов, по умолчанию не являются потокобезопасными. Без GIL нескольким потокам, одновременно обращающимся к общим ресурсам, потребовались бы сложные механизмы блокировок или синхронизации для предотвращения гонок данных и повреждения состояния.

Когда GIL становится узким местом?

  • В однопоточных программах GIL не имеет значения, так как поток обладает эксклюзивным доступом к интерпретатору Python.
  • В многопоточных I/O-bound программах влияние GIL менее заметно, поскольку потоки освобождают GIL во время ожидания операций ввода-вывода.
  • В многопоточных CPU-bound задачах GIL становится серьёзным узким местом. Несколько потоков, конкурируя за GIL, вынуждены по очереди выполнять байткод Python.

Интересный случай, на который стоит обратить внимание, — использование time.sleep. Python фактически рассматривает time.sleep как I/O-операцию. Функция time.sleep не является CPU-bound, поскольку во время сна не происходит активных вычислений или выполнения байткода Python. Вместо этого ответственность за отслеживание прошедшего времени передаётся операционной системе. В течение этого времени поток освобождает GIL, позволяя другим потокам выполняться и использовать интерпретатор.

CPU-bound vs I/O-bound задачи

I/O-bound

I/O-bound задача — это задача, выполнение которой блокируется ожиданием операций ввода-вывода (I/O), например сетевых запросов, чтения/записи на диск или работы с внешними устройствами, и поэтому большая часть времени тратится не на вычисления, а на ожидание завершения этих операций.

CPU-bound

CPU-bound задача — это задача, выполнение которой ограничено мощностью центрального процессора (CPU), а не ожиданием ввода-вывода. Время её выполнения определяется главным образом количеством вычислительных операций, которые нужно выполнить CPU, а не тем, сколько времени тратится на ожидание данных из внешних источников.

Многозадачность в Python

Concurrency vs Parallelism

  • Concurrency — это управление несколькими задачами в одно и то же время, но не обязательно их одновременное выполнение. Задачи могут выполняться по очереди, создавая иллюзию многозадачности.
  • Parallelism — это одновременное выполнение нескольких задач, как правило за счёт использования нескольких ядер CPU.

Критерии выбора подхода — Multithreading, Multiprocessing или Asyncio

Multiprocessing (многопроцессность)

  • Лучше всего подходит для CPU-bound задач, требующих интенсивных вычислений.
  • Используется, когда необходимо обойти GIL — каждый процесс имеет собственный интерпретатор Python, что позволяет достичь настоящего параллелизма.

Multithreading (многопоточность)

  • Лучше всего подходит для быстрых I/O-bound задач, так как уменьшается частота переключений контекста, и интерпретатор Python дольше остаётся в одном потоке.
  • Не подходит для CPU-bound задач из-за ограничений GIL.

Asyncio (асинхронность)

  • Идеально подходит для медленных I/O-bound задач, таких как длительные сетевые запросы или обращения к базе данных, поскольку эффективно обрабатывает ожидание и хорошо масштабируется.
  • Не подходит для CPU-bound задач, если вычисления не выносятся в другие процессы.

threading

Начальный пример Threading

Модуль threading предоставляет способ запуска нескольких потоков (меньших единиц процесса) конкурентно внутри одного процесса. Он позволяет создавать и управлять потоками, делая возможным параллельное выполнение задач с разделяемым адресным пространством памяти. Потоки особенно полезны, когда задачи являются I/O-bound, например при работе с файлами или выполнении сетевых запросов, где значительная часть времени тратится на ожидание внешних ресурсов.

Типичный сценарий использования threading — управление пулом рабочих потоков, которые могут конкурентно обрабатывать несколько задач. Ниже приведён базовый пример создания и запуска потоков с использованием Thread:

Результат:

Результат второго запуска:

Общая схема start и join в threading:

Деталь реализации CPython

В CPython из-за глобальной блокировки интерпретатора (GIL) только один поток может выполнять Python-код в каждый момент времени (хотя некоторые ориентированные на производительность библиотеки могут обходить это ограничение). Если требуется более эффективно использовать вычислительные ресурсы многоядерных машин, рекомендуется использовать multiprocessing или concurrent.futures.ProcessPoolExecutor. Тем не менее, threading остаётся подходящей моделью, если нужно одновременно выполнять несколько I/O-bound задач.

GIL и вопросы производительности

В отличие от модуля multiprocessing, который использует отдельные процессы для обхода GIL, модуль threading работает внутри одного процесса, а значит все потоки разделяют одно и то же адресное пространство памяти. Однако GIL ограничивает прирост производительности при работе с CPU-bound задачами, поскольку только один поток может выполнять байткод Python одновременно. Несмотря на это, потоки остаются полезным инструментом для достижения конкурентности во многих сценариях.

Начиная с Python 3.13, существуют free-threaded сборки, в которых GIL может быть отключён, что позволяет добиться настоящего параллельного выполнения потоков. Однако по умолчанию эта возможность недоступна (см. PEP 703).

Жизненный цикл потока

Жизненным циклом потоков можно управлять с помощью следующих методов:

  • start() — Дает потоку жизнь.
  • run() — Этот метод представляет действия, которые должны быть выполнены в
    потоке.
  • join([timeout]) — Поток, который вызывает этот метод, приостанавливается, ожидая завершения потока, чей метод вызван. Параметр timeout (число с плавающей точкой) позволяет указать время ожидания (в секундах), по истечении которого приостановленный поток продолжает свою работу независимо от завершения потока, чей метод join был вызван. Вызывать join() некоторого потока можно много раз. Поток не может вызвать метод join() самого себя. Также нельзя ожидать завершения еще не запущенного потока. Слово «join» в переводе с английского означает «присоединить», то есть, метод, вызвавший join(), желает, чтобы поток по завершении присоединился к вызывающему метод потоку.
  • getName() — Возвращает имя потока. Для главного потока это «MainThread«.
  • setName(name) — Присваивает потоку имя name.
  • isAlive() — Возвращает истину, если поток работает (метод run() уже вызван, но еще не завершился).
  • isDaemon() — Возвращает истину, если поток имеет признак демона. Программа на Python завершается по завершении всех потоков, не являющихся демонами. Главный поток демоном не является.
  • setDaemon(daemonic) — Устанавливает признак daemonic того, что поток является демоном. Начальное значение этого признака заимствуется у потока, запустившего данный. Признак можно изменять только для потоков, которые еще не запущены.

Атрибуты потока:

  • t.name — имя потока
  • t.ident — Уникальный идентификатор потока (ID) — None, если поток ещё не запущен
  • threading.current_thread() — Возвращает объект текущего потока
  • t.daemon = True — Демон-потоки убиваются при завершении главного потока. Используются для фоновых задач, логирования, heartbeat-потоков. daemon нужно задавать до start()
  • threading.active_count() — Количество активных потоков
  • threading.enumerate() — Список всех живых потоков

Реализация потокобезопасной записи результатов с Lock, чтобы избежать race condition

Что выполняется в коде:

  • t.start() — Создаёт реальный системный поток. Вызывает crawl(…) в новом потоке. Нельзя вызывать start() дважды для одного и того же объекта.
  • t.join() — Блокирует главный поток и ждёт, пока поток t завершится. Гарантирует, что все данные собраны.
  • threading.current_thread().name — Позволяет узнать, какой поток сейчас выполняется. Используется для логирования и отладки
Команда Где используется Назначение
Thread(...) создание потоков описание задачи
start() запуск старт выполнения
join() ожидание синхронизация
current_thread() внутри crawl диагностика
Lock() защита results потокобезопасность

Результат выполнения скрипта:

Основы threading

Создание потока через конструктор threading.Thread

Основные параметры:

Параметр Описание
target Функция, которая будет выполнена в потоке
args Кортеж позиционных аргументов
kwargs Именованные аргументы
name Имя потока
daemon Демон-поток (True/False)

Запуск и управление потоками t.start()

  • Запускает поток
  • Внутри вызывает run()
  • Нельзя вызвать повторно

  • Содержит код потока
  • Не запускает новый поток, если вызвать напрямую
  • Обычно не вызывается вручную

  • Ждёт завершения потока
  • timeout — максимальное время ожидания (в секундах)

  • Возвращает True, если поток ещё работает

Примитивы синхронизации: Lock, RLock, Semaphore, Event, Condition

Подробнее: Python. Урок 23. Потоки и процессы в Python. Часть 2. Синхронизация потоков


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

Диспетчеры контекста предусмотрены для всех объектов модуля threading, таких как Lock, RLock, Condition, Semaphore и BoundedSemaphore, то есть для работы с этими объектами может применяться инструкция with.

Начнём с Lock и RLock, так как они лежат в основе почти всех сценариев синхронизации.

Lock

Lock — это обычный мьютекс, который может быть захвачен только одним потоком в конкретный момент времени. Когда поток вызывает acquire(), он либо сразу получает доступ к критической секции, либо блокируется до тех пор, пока другой поток не освободит lock. После выполнения защищённого участка кода поток обязан вызвать release(). В реальном коде Lock почти всегда используется через контекстный менеджер with, потому что это гарантирует освобождение блокировки даже при исключении. Lock подходит для защиты простых структур данных, таких как словари, списки или счётчики, и для коротких критических секций. Важно понимать, что один и тот же поток не может захватить Lock повторно: попытка сделать это приведёт к deadlock, когда поток будет ждать самого себя.

Проблема (без Lock)

Решение с Lock

Что гарантирует Lock

  • Только один поток изменяет общее состояние
  • Нет гонок данных

RLock

Эта проблема решается с помощью RLock, или reentrant lock. По сути это мьютекс с учётом владельца. Поток, который уже владеет RLock, может захватить его ещё раз, и Python просто увеличит внутренний счётчик захватов. Освобождать такой lock нужно столько же раз, сколько он был захвачен. RLock необходим в более сложных архитектурах, когда функции с защитой lock вызывают друг друга, либо когда публичный метод и внутренний метод используют одну и ту же блокировку. Без RLock такой код почти неизбежно приводит к взаимной блокировке.

Проблема с Lock

Решение с RLock

Что даёт RLock

  • Один поток может захватывать блокировку несколько раз
  • Важно для рекурсии и вложенных вызовов

Semaphore

Следующий важный примитив — Semaphore. В отличие от Lock, который допускает ровно одного владельца, semaphore разрешает одновременно находиться в критической секции ограниченному числу потоков. При создании семафора задаётся счётчик, который уменьшается при acquire() и увеличивается при release(). Пока счётчик положительный, потоки могут входить без ожидания, а когда он становится равным нулю, все последующие вызовы acquire() блокируются. Семантически семафор описывает не владение ресурсом, а количество доступных слотов. Это делает его удобным для ограничения параллельного доступа к внешним системам, таким как база данных, пул соединений или сторонний API. В отличие от Lock, семафор не привязан к конкретному потоку, поэтому важно строго соблюдать баланс acquire() и release(), иначе система либо «утечёт» в блокировку, либо начнёт пускать больше потоков, чем предполагалось.

Пример: максимум 2 потока одновременно

Что гарантирует Semaphore

  • Не более N потоков внутри секции
  • Остальные ждут освобождения ресурса

BoundedSemaphore

BoundedSemaphore — это вариант семафора из модуля threading, который предназначен для строгого контроля количества одновременных доступов к ресурсу и дополнительно защищает от логических ошибок в коде.

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

Ключевое отличие BoundedSemaphore от Semaphore заключается в том, что он не позволяет превысить начальное значение счётчика. Если вызвать release() больше раз, чем было успешных acquire(), BoundedSemaphore выбросит исключение ValueError. Обычный Semaphore такого не делает и молча увеличивает счётчик, что может привести к незаметным ошибкам и нарушению инвариантов программы.

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

Пример использования BoundedSemaphore для ограничения числа одновременных работников:

Если в этом примере по ошибке вызвать pool.release() дважды в одном потоке, программа сразу упадёт с ValueError, что явно укажет на ошибку управления ресурсом. Именно это поведение и является главным практическим отличием BoundedSemaphore от обычного Semaphore.

Event

Event решает другую задачу и не предназначен для защиты критических секций. Это потокобезопасный флаг, который может быть установлен или сброшен, и который другие потоки могут проверять или ожидать. Внутренне Event хранит состояние «установлен» или «не установлен». Когда поток вызывает wait(), он блокируется до тех пор, пока другой поток не вызовет set(). Если событие уже установлено, wait() возвращается сразу. В отличие от lock-ов, событие не «потребляется» при ожидании, и все потоки, ожидающие одного и того же события, будут разбужены одновременно. На практике Event чаще всего используется для управления жизненным циклом потоков, например для корректной остановки воркеров или для сигнализации о готовности системы к работе. Это более выразительная и безопасная альтернатива общим флагам и бесконечным циклам с sleep.

Пример: ожидание старта

Event идеально подходит для

  • старт / стоп сигналов
  • graceful shutdown — (плавное или корректное завершение работы) — это процесс остановки компьютерной системы (приложения, сервера, контейнера), при котором она успевает выполнить необходимые задачи по очистке и сохранению данных перед полным выключением.
  • ожидания готовности ресурса

Condition

Condition является самым сложным и одновременно самым гибким примитивом синхронизации. Он объединяет в себе мьютекс и механизм ожидания уведомлений. Идея Condition заключается в том, что поток может ждать не просто сигнала, а выполнения определённого логического условия, связанного с состоянием программы. Поток захватывает условие, проверяет состояние, и если оно не удовлетворяет требованиям, вызывает wait(). При этом lock временно освобождается, чтобы другие потоки могли изменить состояние. Когда другой поток вызывает notify() или notify_all(), ожидающие потоки пробуждаются и снова проверяют условие. Именно повторная проверка условия является ключевым моментом, так как пробуждение не гарантирует, что состояние действительно изменилось нужным образом. Condition активно используется в классических паттернах producer–consumer, очередях задач и системах, где потоки должны реагировать на изменение общего состояния, а не просто на факт события.

Если смотреть на эти примитивы как на систему, то Lock и RLock отвечают за эксклюзивный доступ, Semaphore ограничивает параллелизм, Event передаёт сигналы между потоками, а Condition позволяет потокам координироваться на основе сложных условий. В продакшене выбор примитива почти всегда диктуется смыслом задачи, а не техническими деталями. Хорошая синхронизация делает код не только корректным, но и читаемым, потому что по выбранному примитиву сразу понятно, как именно потоки должны взаимодействовать друг с другом.

Producer / Consumer с Condition

Condition = Lock + Event

  • wait() → отпускает lock и ждёт
  • notify() → будит ожидающий поток
  • Позволяет ждать логических условий, а не просто блокировки

Barrier

threading.Barrier — это примитив синхронизации в Python, который позволяет группе потоков одновременно ожидать друг друга в определенной точке выполнения (контрольной точке) перед тем, как продолжить работу.

todo

Итоговая таблица

Примитив Для чего
Lock Простая защита общего состояния
RLock Вложенные / рекурсивные блокировки
Semaphore Ограничение количества потоков
Event Сигналы между потоками
Condition Сложная координация и ожидание условий

concurrent.futures

todo

multiprocessing

Теория
Разница между:
fork / spawn / forkserver

IPC (межпроцессное взаимодействие):
Queue
Pipe
Manager
Стоимость сериализации (pickle)
Copy-on-write (Linux)

Практика
Параллельная обработка данных
Использование multiprocessing.Pool

Бенчмарк:
threading vs multiprocessing
Поймать баг с pickling’ом

📌 Продакшн insight: multiprocessing часто убивает latency, если использовать бездумно.

asyncio

Ключевая тема для highload backend.

Теория
Event loop
Coroutine
Awaitable
Task vs Future
Cooperative multitasking
Почему async ≠ threading

Практика
Переписать синхронный код в async
Одновременные HTTP-запросы (aiohttp)
Ограничение параллелизма (Semaphore)

Ошибки:
blocking call внутри async
забытый await

Критически важно: понимание, почему один blocking вызов убивает весь сервис.

0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x