Синхронизация в параллельных системах: от дедлоков до Lock-Free структур

Stanford Online 5,2 тыс. 1 ч 15 мин 5 мин 24.09.2024
Главное

В современной разработке программного обеспечения параллелизм перестал быть экзотикой, превратившись в обязательное требование для производительных систем. Однако простое использование блокировок (locks) часто становится «бутылочным горлышком», ограничивающим масштабируемость. В тринадцатой лекции курса Stanford CS149 рассматриваются фундаментальные проблемы синхронизации, способы реализации эффективных примитивов на уровне железа и переход к парадигме программирования без блокировок (lock-free).

🛑 Три всадника проблем синхронизации: Deadlock, Livelock и Starvation 0:32

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

1. Deadlock (Взаимная блокировка) Это ситуация, когда группа потоков ожидает ресурсы, удерживаемые друг другом, и никто не может сдвинуться с места. Для возникновения дедлока должны одновременно соблюдаться четыре условия:

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

3. Starvation (Голодание) Система в целом работает и выполняет полезные задачи, но конкретный поток или тип задач никогда не получает доступа к ресурсу. Пример: машина, пытающаяся выехать со второстепенной дороги на оживленное шоссе (Highway 1). Если поток машин на шоссе бесконечен и правила требуют уступать, «желтая машина» будет ждать вечно, хотя движение на шоссе идет полным ходом .

🧠 Когерентность кэша и цена блокировки 9:47

Реализация блокировок неразрывно связана с тем, как работают кэши процессоров. Основная задача протоколов когерентности кэша (например, MSI — Modified, Shared, Invalid) — гарантировать, что все ядра видят актуальные данные .

Когда один процессор записывает значение в память (например, меняет флаг блокировки с 0 на 1), протокол обязан аннулировать (invalidate) все копии этой строки кэша в других ядрах . Это создает серьезную проблему для производительности:

С увеличением количества ядер стоимость одной операции захвата/освобождения блокировки растет экспоненциально из-за перегрузки межпроцессорного соединения .

🛠 Реализация эффективных примитивов: от TAS до Ticket Locks 18:07

Для минимизации трафика когерентности инженеры используют различные программные стратегии.

Test-and-Set (TAS)

Самый простой вариант. Поток в цикле вызывает атомарную инструкцию, которая читает значение и устанавливает его в 1.

Test-and-Test-and-Set (TTAS)

Более умный подход. Поток сначала просто читает значение (в обычном цикле), и только если видит, что блокировка свободна (0), пытается выполнить атомарную запись test-and-set .

Ticket Lock (Билетная блокировка)

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

🔄 Атомарные операции и Compare-and-Swap (CAS) 36:28

Современные процессоры предоставляют богатый набор атомарных инструкций. Одной из самых важных является Compare-and-Swap (CAS) . Логика CAS: «Если значение по адресу равно X, замени его на Y, иначе ничего не делай».

На базе CAS можно реализовать любой сложный атомарный примитив, например, atomic_min :

  1. Считать текущее значение из памяти.
  2. Вычислить локально новое минимальное значение.
  3. Вызвать CAS. Если за время вычислений значение в памяти изменилось, CAS вернет ошибку.
  4. В случае неудачи — повторить цикл заново .

По мнению лектора, понимание CAS критически важно, так как это фундамент для создания всех lock-free структур данных .

⛓ Тонкая синхронизация в структурах данных 52:41

Простое «грубое» решение (одна блокировка на весь связный список) убивает параллелизм, превращая выполнение программы в последовательное . Альтернатива — тонкая синхронизация (fine-grained synchronization).

Метод Hand-over-hand Locking

Аналогия — прохождение рукохода на полосе препятствий Ninja Warrior: вы не отпускаете одну перекладину, пока не схватитесь за следующую .

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

🚀 Введение в Lock-Free программирование 1:11:10

Lock-free алгоритмы позволяют потокам работать с данными без использования мьютексов, что исключает риск блокировки всей системы, если один поток будет прерван ОС .

Ключевая идея: Вместо того чтобы запрещать другим вход (блокировка), мы выполняем работу спекулятивно. В самый последний момент, перед записью результата, мы проверяем с помощью CAS, не изменился ли мир за это время. Если изменился — мы просто отбрасываем результат и пробуем снова .

Такой подход особенно эффективен в высоконагруженных системах (базы данных, веб-серверы), где контекстное переключение потока, удерживающего блокировку, может привести к простою сотен других потоков . Большинство современных конкурентных коллекций в Java и C++ реализованы именно на базе lock-free алгоритмов .


💬 Цитаты

«Если вы никогда не держитесь хотя бы за одну перекладину, гравитация победит.»

Кейвон Фатахалиан 1:04:42

«Иметь блокировку и иметь строку кэша — это две разные вещи.»

Кейвон Фатахалиан 23:38
👥 Спикер
🔗 Упомянутые сайты и проекты
📖 Термины
Deadlock
Состояние, при котором два или более потока бесконечно ждут друг друга, удерживая необходимые ресурсы.
CAS (Compare-and-Swap)
Атомарная инструкция процессора, которая обновляет значение переменной только в том случае, если текущее значение совпадает с ожидаемым.
MSI Protocol
Базовый протокол обеспечения когерентности кэшей, где строка может быть в состоянии Modified, Shared или Invalid.
Lock-Free
Алгоритмы, гарантирующие прогресс хотя бы одного потока в системе без использования блокирующих примитивов.
📊 Цифры
⚖️ Другая сторона
Технологии и IT Stanford Online Cache Coherence Compare-and-Swap Deadlock Lock-Free