Конкурентность и параллелизм

Материал из llmdocs — справочник разработчика
Портал: Основы CS
Конкурентность и параллелизм
Конкурентностьработа с несколькими задачами, чередующимися во времени
Параллелизмодновременное выполнение на нескольких ядрах или машинах
Механизмыпроцессы, потоки, корутины (async/await), зелёные потоки, акторы
Задачи с ожиданием ввода-выводаасинхронность, потоки
Вычислительные задачипроцессы, потоки без глобальной блокировки, GPU
Ограничение ускорениязакон Амдала (1967)
Проблемыгонки данных, взаимоблокировки, голодание

Конкурентность (англ. concurrency) — свойство программы состоять из нескольких независимых задач, выполнение которых перекрывается во времени: пока одна ждёт ответа сети, другая обрабатывает данные. Параллелизм (parallelism) — физически одновременное выполнение вычислений на нескольких ядрах процессора, нескольких машинах или GPU. Роб Пайк сформулировал разницу так: конкурентность — это работа со многими вещами сразу, параллелизм — выполнение многих вещей сразу.[1]

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

Зачем это нужно

С середины 2000-х тактовая частота процессоров почти перестала расти, а количество ядер — нет. Герб Саттер назвал это концом «бесплатного обеда»: программы перестали ускоряться сами собой с каждым новым поколением процессоров, и для использования всех ядер их приходится писать параллельными.[2] Одновременно серверные приложения обслуживают тысячи соединений, большую часть времени ожидающих сети или диска, — это задача конкурентности.

Два класса задач

Ограничены вводом-выводом (I/O-bound) Ограничены процессором (CPU-bound)
Пример веб-сервер, парсер сайтов, вызовы API, запросы к БД обработка изображений, численные расчёты, сжатие, обучение моделей
Узкое место ожидание сети и диска вычисления
Что помогает конкурентность: async/await, потоки параллелизм: процессы, потоки на разных ядрах, векторизация, GPU

Механизмы

  • Процессы — изолированная память, связь через каналы, сокеты или общую память. Надёжно, но дороже по ресурсам.
  • Потоки (threads) — общая память внутри процесса; управляет ими операционная система. Дешевле процессов, но общий доступ к данным требует синхронизации.
  • Корутины и async/await — задачи сами уступают управление в точках ожидания, а цикл событий переключается между ними в одном потоке. Тысячи одновременных соединений почти без накладных расходов — так работают asyncio в Python, JavaScript и асинхронный Rust.
  • Зелёные потоки — лёгкие потоки, которыми управляет среда выполнения языка: горутины в Go, виртуальные потоки в Java. Совмещают простоту обычного кода с масштабом асинхронности.
  • Акторы и передача сообщений — задачи не делят память, а обмениваются сообщениями (Erlang, Akka). Принцип Go: «не общайтесь через общую память — делитесь памятью через общение».[6]

Пример в Python

import asyncio, httpx
from concurrent.futures import ProcessPoolExecutor

# I/O-bound: 100 запросов конкурентно в одном потоке
async def fetch_all(urls):
    async with httpx.AsyncClient() as client:
        return await asyncio.gather(*(client.get(u) for u in urls))

# CPU-bound: вычисления параллельно на всех ядрах
def heavy(n: int) -> int:
    return sum(i * i for i in range(n))

if __name__ == "__main__":
    with ProcessPoolExecutor() as pool:
        results = list(pool.map(heavy, [10**7] * 8))

В CPython глобальная блокировка интерпретатора (GIL) не даёт потокам выполнять Python-код параллельно, поэтому для вычислений используют процессы.[3] PEP 703 сделал GIL необязательным: начиная с Python 3.13 существует экспериментальная сборка без GIL, а в 3.14 она получила официальную поддержку (см. GIL и free-threading (PEP 703)).[4]

Проблемы

  • Состояние гонки. Результат зависит от того, в каком порядке потоки обратились к общим данным: два потока одновременно читают счётчик, прибавляют единицу и записывают — одно увеличение теряется (см. Состояние гонки и блокировки).
  • Взаимоблокировка (deadlock). Два потока ждут блокировки, захваченные друг другом, и стоят вечно.
  • Голодание и инверсия приоритетов. Задача бесконечно не получает ресурс.
  • Недетерминированность. Ошибки проявляются редко и не воспроизводятся в отладчике.

Средства защиты — мьютексы, атомарные операции, неизменяемые данные, очереди и каналы вместо общей памяти. Rust проверяет отсутствие гонок данных на этапе компиляции с помощью системы владения и типажей Send и Sync.[5]

Закон Амдала

Ускорение от параллелизма ограничено долей программы, которая выполняется последовательно. Если 10% работы нельзя распараллелить, то даже на бесконечном числе ядер программа ускорится не больше чем в 10 раз. Отсюда практический вывод: прежде чем добавлять ядра, найдите и сократите последовательную часть — блокировки, общий ресурс, однопоточный этап.

Параллелизм за пределами одной машины

На уровне систем те же идеи масштабируются: очереди задач и пулы обработчиков, горизонтальное масштабирование сервисов в Kubernetes, распределённые вычисления. Там к гонкам добавляются сетевые задержки и частичные отказы — это предмет распределённых систем.

См. также

Источники

  1. Rob Pike — Concurrency is not Parallelism (2012)
  2. Herb Sutter — The Free Lunch Is Over (2005)
  3. Python — concurrent.futures
  4. PEP 703 — Making the Global Interpreter Lock Optional in CPython
  5. The Rust Programming Language — Fearless Concurrency
  6. Go — Effective Go: Concurrency