Статьи

Concurrency в реальных системах: от потоков до асинхронности

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

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

Именно поэтому понимание concurrency — важная часть инженерного мышления. Это не просто инструмент оптимизации, а фундамент архитектуры многих современных систем.

Concurrency и Parallelism: в чём разница

Эти два понятия часто используют как взаимозаменяемые, хотя они описывают разные вещи.

Concurrency — архитектурный принцип организации задач
Parallelism — физическое выполнение нескольких задач одновременно

Иначе говоря, concurrency — это про структуру программы, а parallelism — про аппаратные возможности процессора.

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

Где используется concurrency

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

Она используется в:

• Веб-серверах
• Базах данных
• Распределённых системах
• Системах очередей
• Обработке потоковых данных
• Пользовательских интерфейсах

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

Основные модели concurrency

Существует несколько способов организовать конкурентное выполнение задач. Они отличаются по сложности, производительности и архитектурным ограничениям.
Подход
Как работает
Где используется
Потоки (threads)
несколько потоков делят одну память
Java, C++, backend-сервисы
Асинхронность
задачи не блокируют поток
Python asyncio, Node.js
Event loop
события обрабатываются через очередь
Node.js, UI-фреймворки
Message passing
обмен сообщениями вместо общей памяти
Go, Erlang
Каждый из этих подходов решает одну и ту же задачу — эффективно управлять большим количеством одновременно выполняющихся операций.

Потоки: классическая модель concurrency

Самый традиционный способ работы с concurrency — использование потоков.

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

Простой пример на Java:
Здесь создаётся новый поток выполнения. Он запускает метод run() независимо от основного потока программы.

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

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

Асинхронная модель

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

Пример на Python:
Здесь задачи выполняются конкурентно внутри одного потока благодаря event loop. Когда одна задача ждёт завершения операции (например, сетевого запроса), управление передаётся другой.
Такой подход особенно эффективен для сетевых приложений.

Event loop

Event loop — это механизм, который управляет выполнением асинхронных задач.

Он работает по простой схеме:

1. Принимает события
2. Помещает их в очередь
3. Выполняет обработчики

Эта модель активно используется в системах, где много операций ввода-вывода: сетевых запросов, чтения файлов, взаимодействия с API.
Именно поэтому такие платформы, как Node.js, способны обслуживать огромное количество соединений, используя всего несколько потоков.

Основные проблемы concurrency

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

Race condition

Гонка данных возникает, когда несколько потоков одновременно изменяют одно и то же состояние.

Рассмотрим простую операцию:

counter++;

На уровне процессора она состоит из трёх действий:

1. Чтение значения
2. Увеличение
3. Запись обратно

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


Deadlock

Deadlock — это ситуация, когда два потока ждут друг друга бесконечно.

Поток A: держит Lock1 и ждёт Lock2
Поток B: держит Lock2 и ждёт Lock1

В результате ни один поток не может продолжить работу.

Deadlock — одна из самых известных проблем многопоточного программирования. В сложных системах она может возникать из-за неправильного порядка захвата блокировок.


Livelock

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

Синхронизация доступа к данным

Чтобы избежать проблем concurrency, используются механизмы синхронизации.
Инструмент
Назначение
Mutex / Lock
ограничивает доступ к ресурсу одним потоком
Semaphore
контролирует количество потоков
Atomic операции
выполняются неделимо
Immutable данные
состояние нельзя изменить
На практике чаще всего используются блокировки.

Пример на Python:
Блокировка гарантирует, что только один поток выполняет критическую секцию.
Однако чрезмерное использование locks может ухудшить производительность и усложнить код.

Альтернатива: обмен сообщениями

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

Пример на Go:
Этот подход лежит в основе акторных систем и хорошо масштабируется в распределённых архитектурах.

Concurrency в базах данных

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

Для этого используются различные механизмы.
Механизм
Как работает
Locking
строки или таблицы блокируются
MVCC
хранение нескольких версий данных
Isolation levels
управление видимостью изменений
Например, PostgreSQL применяет MVCC (Multi Version Concurrency Control), позволяя транзакциям читать старые версии строк, пока другие транзакции их изменяют.
Это снижает количество блокировок и повышает масштабируемость.

Concurrency в веб-серверах

Разные серверы используют разные архитектурные модели.
Модель
Пример серверов
Thread per request
Apache
Event-driven
Node.js
Worker pool
Nginx, многие backend-фреймворки
Все они решают одну задачу — эффективно обрабатывать большое количество запросов.
Выбор модели зависит от типа нагрузки: CPU-интенсивной или I/O-интенсивной.

Практические принципы работы с concurrency

Несмотря на разнообразие инструментов, в разработке есть несколько проверенных принципов, которые помогают избегать проблем:

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

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

Почему concurrency остаётся сложной

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

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

Concurrency — фундаментальный инструмент современной разработки. Благодаря ему системы могут обрабатывать тысячи задач одновременно и эффективно использовать ресурсы процессора.
Но вместе с преимуществами приходит и новая сложность: управление состоянием, синхронизация потоков и поиск трудноуловимых ошибок.
Современные языки и фреймворки постепенно упрощают работу с concurrency. Появляются async/await, actor-модели, каналы и другие абстракции. Однако понимание базовых принципов остаётся ключевым навыком разработчика.
Именно эти принципы позволяют ориентироваться в любой технологии — от backend-разработки до распределённых систем и инфраструктуры.


Хотите узнать больше? Изучите другие статьи из разделов:
Основы и старт в IT Java Python DevOps