Модели конкурентности: потоки, события, акторы, CSP

Потоки и async — не единственные способы думать о конкурентности. Есть и другие модели.

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

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

Четыре распространённые модели

МодельКак общаютсяПример
Потоки + общая памятьчерез общие переменные под блокировкамиPython threading, Java
Событийная (event loop)колбэки/корутины в одном потокеasyncio, Node.js
Акторысообщения в «почтовый ящик» актораErlang, Akka
CSP / каналыпередача по каналам между процессамиGo (goroutines + channels)

Акторы

В модели акторов каждый актор — изолированная сущность со своим состоянием и почтовым ящиком. Снаружи к состоянию не подобраться; можно лишь прислать сообщение. Актор обрабатывает сообщения по одному, поэтому внутри него гонок нет «по построению». Так устроены телеком-системы на Erlang.

CSP и каналы

CSP (Communicating Sequential Processes) — основа конкурентности в Go. Задачи (горутины) общаются через каналы: один пишет в канал, другой читает. Девиз — «не общайтесь через общую память; делите память, общаясь». Канал сам синхронизирует передачу.

import collections

# смоделируем канал как очередь: producer кладёт, consumer берёт
channel = collections.deque()

channel.append("msg-1")   # producer -> канал
channel.append("msg-2")

while channel:
    msg = channel.popleft()   # канал -> consumer
    print("получено:", msg)

Вывод:

получено: msg-1
получено: msg-2

Как работает под капотом

Модели с передачей сообщений прячут синхронизацию внутри почтовых ящиков и каналов: разработчик не пишет блокировки руками. Это снижает класс ошибок (гонки, забытые release), но добавляет другие заботы — переполнение очередей, дедлоки на каналах (читатель ждёт сообщение, которое никто не пришлёт), сериализацию сообщений. В Python чистой модели акторов в стандартной библиотеке нет, но идею каналов хорошо передаёт queue.Queue и связка процессов с очередями в multiprocessing.

Частые ошибки

  • Считать одну модель универсальной. Для CPU лучше процессы, для тысяч соединений — события, для устойчивых распределённых систем — акторы.
  • Смешивать общую память и сообщения бессистемно. Тогда теряются преимущества обеих моделей.
  • Игнорировать переполнение каналов/ящиков. Без ограничения размера producer может «завалить» consumer.

Итог

  • Два лагеря: общая память с блокировками и передача сообщений.
  • Передача сообщений безопаснее — нет общего изменяемого состояния.
  • Акторы изолируют состояние, CSP/каналы синхронизируют передачу.
  • Выбор модели зависит от задачи; в Python каналы моделируются очередями.
Проверьте себя
1. Чем модель передачи сообщений (акторы, CSP) безопаснее модели общей памяти?
AОна быстрее на любом железе
BВ ней нет общего изменяемого состояния, поэтому гонок «по построению» нет
CОна не использует процессор
DОна не требует кода
2. Какой девиз отражает модель CSP/каналов (как в Go)?
AДелите всё через глобальные переменные
BНе общайтесь через общую память — делите память, общаясь
CСоздавайте поток на каждую переменную
DБлокируйте всё одной большой блокировкой