Промышленное производство
Промышленный Интернет вещей | Промышленные материалы | Техническое обслуживание и ремонт оборудования | Промышленное программирование |
home  MfgRobots >> Промышленное производство >  >> Industrial programming >> Python

Мастерство многопоточности в Python: GIL объяснён с практическими примерами

Python предлагает и multiprocessing, и multithreading для ускорения рабочих нагрузок. Это руководство проведёт вас через создание надежных потоковых приложений, нюансы Global Interpreter Lock (GIL) и как избегать частых подводных камней: дедлоков и состояний гонки.

Что такое поток (Thread)?

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

Что такое процесс (Process)?

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

Основы многопоточности в Python

Модуль threading предоставляет высокоуровневый API для управления потоками, в то время как устаревший _thread сохранён только для обратной совместимости. Потоки идеальны для I/O-bound задач, а CPU-bound работу лучше выполнять через модуль multiprocessing для обхода GIL.

Когда использовать многопоточность

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

Ключевые модули Python для треддинга

  1. _thread — низкоуровневый, устарел.
  2. threading — современный, богатый функционалом, рекомендуемый выбор.

Использование модуля threading

Модуль threading предоставляет класс Thread, примитивы синхронизации (Lock, RLock, Semaphore, Condition, Event, Barrier) и утилиты вроде activeCount() и enumerate(). Ниже — минимальный пример демонстрации создания потоков и синхронизации.

import time
import _thread
def thread_test(name, wait):
    for i in range(4):
        time.sleep(wait)
        print(f"Running {name}")
    print(f"{name} has finished execution")
if __name__ == "__main__":
    _thread.start_new_thread(thread_test, ("First Thread", 1))
    _thread.start_new_thread(thread_test, ("Second Thread", 2))
    _thread.start_new_thread(thread_test, ("Third Thread", 3))

Запуск этого кода даёт перемежающийся вывод, демонстрирующий конкурентное выполнение.

Мастерство многопоточности в Python: GIL объяснён с практическими примерами

Пояснение

  1. Импорт time и legacy _thread.
  2. Определение thread_test для выполнения работы и логирования прогресса.
  3. Запуск трёх независимых потоков через start_new_thread.

Современный треддинг с threading.Thread

На практике вы наследуетесь от threading.Thread и переопределяете метод run(). Следующий пример повторяет предыдущее поведение, но использует высокоуровневый API.

import time
import threading
class ThreadTester(threading.Thread):
    def __init__(self, name, delay):
        super().__init__()
        self.name = name
        self.delay = delay
    def run(self):
        for _ in range(4):
            time.sleep(self.delay)
            print(f"Running {self.name}")
        print(f"{self.name} has finished execution")
if __name__ == "__main__":
    threads = [
        ThreadTester("First Thread", 1),
        ThreadTester("Second Thread", 2),
        ThreadTester("Third Thread", 3),
    ]
    for t in threads:
        t.start()
    for t in threads:
        t.join()

Вывод:

Мастерство многопоточности в Python: GIL объяснён с практическими примерами

Пояснение

  1. Наследуемся от Thread для инкапсуляции поведения потока.
  2. Переопределяем __init__ для хранения поток-специфичных данных.
  3. Переопределяем run для определения задачи.
  4. Создаём, стартуем и ждём (join) каждый поток.

Частые подводные камни треддинга

Дедлоки (Deadlocks)

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

Состояния гонки (Race Conditions)

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

i = 0
for _ in range(100):
    print(i)
    i += 1

Запуск этого цикла в параллельных потоках может дать перемешанную последовательность, потому что инкременты перекрываются.

Синхронизация потоков

Модуль threading поставляет несколько примитивов синхронизации. Фундаментальный — Lock, гарантирующий эксклюзивный доступ к общему ресурсу.

import threading
lock = threading.Lock()
def worker(name):
    for _ in range(5):
        lock.acquire()
        print(f"{name} acquired lock")
        # критическая секция
        lock.release()
if __name__ == "__main__":
    t1 = threading.Thread(target=worker, args=("Thread A",))
    t2 = threading.Thread(target=worker, args=("Thread B",))
    t1.start(); t2.start()
    t1.join(); t2.join()

Вывод демонстрирует сериализованный доступ к критической секции.

Мастерство многопоточности в Python: GIL объяснён с практическими примерами

Другие инструменты синхронизации

Понимание Global Interpreter Lock (GIL)

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

  1. Когда поток стартует, он захватывает GIL.
  2. Если другому потоку нужен GIL, он блокируется до освобождения первым.
  3. I/O-bound потоки отпускают GIL во время блокирующих вызовов, позволяя другим выполняться.
  4. CPU-bound потоки удерживают GIL, мешая другим потокам выполнять Python-код.

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

Почему существует GIL

Сборщик мусора CPython полагается на подсчёт ссылок. Без глобальной блокировки конкурентные обновления счётчиков ссылок привели бы к гонкам и повреждению памяти. Пообъектная блокировка ввела бы значительные накладные расходы, поэтому GIL был принят как простое, эффективное решение.

Практические выводы

Python

  1. Освоение функции free() в C: практическое руководство и пример
  2. Мастерство метода strip() в Python: подробное руководство и практические примеры
  3. Мастерство метода str.count() в Python: как подсчитывать символы и подстроки с примерами
  4. Функция round() в Python: подробное объяснение с практическими примерами
  5. Освоение функции map() в Python: синтаксис, примеры и лучшие практики
  6. Python timeit(): измерение времени выполнения с практическими примерами
  7. Counter в collections Python: эффективное подсчёт, обновление и арифметические операции
  8. Python list.count(): экспертное руководство с практическими примерами
  9. Python list.index(): как находить позиции элементов — практические примеры
  10. Многопоточность в C#: Повышение производительности и эффективности