2.17 Конкурентность и параллелизм
Обзор и мотивация
Конкурентность это искусство структурирования программы как независимых задач, которые могут продвигаться, не дожидаясь друг друга. Параллелизм это фактическое выполнение этих задач в один и тот же момент на нескольких процессорах. Различие не педантичное. Конкурентность это способ организовать код так, чтобы медленный сетевой вызов не замораживал всю программу; параллелизм это способ завершить большое вычисление быстрее, распределив его по ядрам. Путаница между ними приводит к тому, что команды добавляют потоки в надежде на скорость и получают одни ошибки.
Для больших команд эта тема важна, потому что конкурентность это место, где корректность тихо умирает. Один автор, пишущий однопоточный код, может рассуждать о нём строка за строкой, но в момент, когда многие авторы делят память между потоками, число возможных чередований взрывается, а программа, проходящая все тесты, всё равно может упасть один раз на миллион под эксплуатационной нагрузкой, не громким крахом, а испорченными данными, зависшими запросами и инцидентами, которые никто не может воспроизвести. Эта глава опирается на фокус на уровне кода главы 2.16 (инженерия производительности) и вычислительные основы главы 2.13 и питает проблемы координации главы 3.3 (распределённые системы), то есть конкурентности между машинами с добавленной жестокостью ненадёжной сети.
Для корпораций ошибки конкурентности это ошибки пропускной способности. Высоконагруженные сервисы живут или умирают по способности обрабатывать тысячи одновременных запросов без гонок за общее состояние, а один несинхронизированный счётчик может испортить главную книгу под нагрузкой. Для государства ставки это корректность и проверяемость в системах, работающих десятилетиями и затрагивающих безопасность, пособия или публичные записи. Гонка в налоговой или медицинской системе не неудобство; это неверный ответ, который кто-то позже должен объяснять надзорному органу. В обоих случаях цель одна: сделать безопасный путь умолчанием, чтобы многие люди, трогающие код, не должны были каждый быть экспертом по конкурентности.
Ключевые принципы
- Конкурентность это структура; параллелизм это выполнение. Решите, что вам на самом деле нужно, прежде чем тянуться к потокам.
- Общее изменяемое состояние враг. Почти каждая ошибка конкурентности восходит к двум задачам, трогающим одни и те же изменяемые данные.
- Предпочитайте неизменяемость и передачу сообщений. Данные, которые нельзя изменить, нельзя «гонять», а сообщения безопаснее общей памяти.
- Недетерминизм основная трудность. Ошибка, проявляющаяся раз в тысячу запусков, вся проблема, а не крайний случай.
- Ограничивайте всё. Неограниченные очереди, числа потоков и работа в полёте превращают всплеск в сбой.
- Модели высокого уровня побеждают сырые блокировки. Акторы, каналы и структурированная конкурентность дают многим авторам безопасное умолчание, а у каждой блокировки есть цена.
- Тестируйте чередования, а не только счастливый путь. Детерминированные тесты не могут поймать ошибку, которую раскрывает только редкий порядок.
Рекомендации
Решите, нужна ли вам конкурентность или параллелизм
Начните с названия проблемы. Если ваш сервис тратит большую часть времени на ожидание (баз данных, сетевых вызовов или диска), у вас нагрузка, ограниченная вводом-выводом, и ответ конкурентность: структурируйте код так, чтобы пока один запрос ждёт, другие продвигались. Один поток с async/await или небольшой пул может обслуживать тысячи ожидающих запросов. Если же ваша программа ограничена ЦП, перемалывает вычисления почти без ожидания, то скорость покупает параллелизм по ядрам, и здесь потолок задаёт закон Амдала (см. главу 2.16): последовательная доля ограничивает ваше ускорение, сколько бы ядер вы ни добавили. Измерьте, в каком режиме вы находитесь, прежде чем проектировать.
Воспринимайте общее изменяемое состояние как врага
Почти каждый дефект конкурентности сводится к одной форме: две задачи читают и пишут одни и те же изменяемые данные, не договорившись о порядке. Это состояние гонки, порождающее потерянные обновления, наполовину записанные объекты и значения, нарушающие инварианты, которые код считал безопасными. Самая надёжная защита иметь меньше общего изменяемого состояния. Давайте каждой задаче собственные данные, передавайте копии, а не ссылки, и ограничивайте изменяемое состояние единственным владельцем, до которого остальные добираются через сообщения. Когда вам действительно нужно делиться, делайте разделение явным и малым, чтобы рецензент мог видеть каждое место, где состояние затрагивается.
Предпочитайте неизменяемость и передачу сообщений как умолчания
Самые безопасные общие данные это данные, которые нельзя изменить. Неизменяемый объект после создания может читаться любым числом потоков без синхронизации, потому что не за что гоняться. Сделайте неизменяемость умолчанием, а изменяемость намеренным исключением. Когда задачам нужно координироваться, предпочитайте передачу сообщений общей памяти: вместо общей переменной пусть одна задача отправляет значение другой, и в этом философия пословицы Go «не общайтесь, разделяя память; разделяйте память, общаясь». Передача сообщений превращает невидимые, зависящие от порядка ошибки в явный проверяемый поток данных, и эта ясность почти всегда стоит затрат на сообщение в коде, который сопровождают многие.
Обращайтесь к моделям высокого уровня раньше сырых блокировок
Блокировки, написанные вручную, корректны в принципе и губительны на практике, потому что люди плохо рассуждают обо всех чередованиях. Предпочитайте модели, делающие безопасную конкурентность умолчанием. Модель акторов даёт каждому актору закрытое состояние и почтовый ящик: акторы никогда не делят память и только отправляют сообщения, поэтому целые классы гонок исчезают. Взаимодействующие последовательные процессы (CSP), модель, стоящая за каналами в языках вроде Go, предполагают, что независимые процессы передают значения по типизированным каналам. Структурированная конкурентность привязывает время жизни конкурентных задач к лексической области, так что задачи не могут пережить блок, породивший их, а ошибки распространяются, а не исчезают. Async/await позволяет писать конкурентный код, ограниченный вводом-выводом, в последовательном стиле. Каждое из них поднимает нижнюю планку для среднего автора, а это то, что нужно большой команде.
Понимайте модель памяти, атомарность и видимость
Когда вы всё же делите память, кусаются два свойства. Атомарность означает, что операция происходит целиком или не происходит вовсе; простое приращение (x = x + 1) не атомарно, потому что читает, прибавляет и записывает тремя шагами, которые другой поток может прервать, и так счётчики теряют обновления. Видимость означает, что запись одного потока становится наблюдаемой для другого; без правильной синхронизации значение, записанное на одном ядре, может сидеть в кэше, невидимое для другого, так что поток может бесконечно крутиться на флаге, уже установленном. Модель памяти вашего языка определяет, когда записи становятся видимыми и какие порядки компилятор и ЦП могут переставлять, поэтому нельзя предполагать, что код выполняется в порядке, в котором вы его написали. Используйте атомарные типы и примитивы синхронизации языка, а не изобретайте свою схему без блокировок.
Используйте примитивы синхронизации намеренно и проектируйте против взаимной блокировки
Когда разделение неизбежно, выбирайте правильный примитив и уважайте его цену. Блокировка или мьютекс (взаимное исключение) пропускает в критическую секцию по одному потоку, но сериализует доступ, так что горячая блокировка становится узким местом, стирающим пользу многих ядер. Семафор ограничивает, сколько задач может продолжать одновременно, и так вы ограничиваете пул. Атомарные операции дают обновления без блокировок для простых значений вроде счётчиков, дешевле блокировки, но легко применяются неверно для чего-либо составного. Блокировки несут три классических режима отказа. Взаимная блокировка (deadlock) это когда задачи ждут друг друга по циклу и ни одна не может продолжать, хрестоматийный случай два потока, каждый из которых держит одну блокировку и хочет другую. Livelock это когда задачи продолжают реагировать друг на друга, но не продвигаются. Голодание это когда задача никогда не получает ресурс, потому что другие постоянно вырываются вперёд. Дисциплины, их предотвращающие, конкретны: введите глобальный порядок блокировок, держите блокировки кратко, добавляйте тайм-ауты, чтобы застрявшая задача отказывала громко, никогда не вызывайте неизвестный код, держа блокировку, и используйте справедливое планирование там, где голодание риск. Записывайте эти правила, потому что новый автор не может заново открыть их только по коду.
Ограничивайте очереди, пулы и работу в полёте обратным давлением
Неограниченная очередь бомба замедленного действия. При всплеске трафика работа приходит быстрее, чем уходит, очередь растёт без предела, память заполняется, и сервис умирает так, что это выглядит как таинственный сбой из-за нехватки памяти, а не перегрузка, которой он является. Ограничивайте каждую очередь, ставьте предел каждому пулу потоков и применяйте обратное давление (backpressure): когда система полна, сигнализируйте выше по течению замедлиться или быстро отклоняйте работу, а не принимайте бесконечную работу, которую не сможете завершить. Подбирайте размеры пулов под нагрузку (примерно число ядер для работы, ограниченной ЦП, больше для работы, ограниченной вводом-выводом, где потоки в основном ждут) и воспринимайте границу как намеренное решение о мощности. Это связано с паттернами устойчивости главы 3.3.
Используйте параллелизм данных там, где работа тривиально параллельна
Некоторые задачи делятся чисто: применить одну операцию к каждому элементу большого набора данных, причём ни один элемент не зависит от другого. Этот параллелизм данных самый дружелюбный, потому что общего состояния для гонок мало, а ускорение может приближаться к числу ядер, как показывают конвейеры map-reduce, параллельные операции над массивами и векторизованный числовой код. Даже здесь уважайте закон Амдала: шаг слияния или свёртки часто последователен и ограничивает ваш выигрыш, а накладные расходы разбиения могут доминировать для малых вводов. Обращайтесь к нему, когда работа на элемент существенна, а элементы действительно независимы; иначе простейшая последовательная версия часто и достаточно быстра, и гораздо легче в поддержании корректности, что подкрепляют практики конструирования главы 2.9.
Тестируйте и отлаживайте недетерминированный код намеренно
Ошибки конкурентности недетерминированы, поэтому обычные тесты, запускающие одно чередование, в основном их пропускают. Атакуйте проблему намеренно стресс- и фаззинг-тестами, запускающими много задач с рандомизированным временем, чтобы вытряхнуть редкие порядки. Обращайтесь к детекторам гонок и санитайзерам потоков, инструментам, инструментирующим доступ к памяти, чтобы ловить гонки данных, даже когда ошибочное чередование в этот запуск не произошло. Там, где ваша платформа это предлагает, используйте детерминированную симуляцию или управляемые планировщики, воспроизводящие конкретное чередование и превращающие гейзенбаг в воспроизводимый, и проектируйте так, чтобы при зависании в эксплуатации можно было захватить состояния потоков и владение блокировками, что связано с дисциплиной отладки главы 2.15. Прежде всего предпочитайте проекты (неизменяемость, передача сообщений, единоличное владение), делающие целые категории этих ошибок невозможными, потому что ошибку, которую нельзя создать, никогда не придётся отлаживать.
Компромиссы: плюсы и минусы
| Подход | Плюсы | Минусы |
|---|---|---|
| Общая память с блокировками | Быстро на операцию; знакомо | Гонки, взаимные блокировки и ошибки видимости; трудно сохранять корректность многим авторам |
| Неизменяемость | Не нужна синхронизация; тривиально потокобезопасные чтения | Стоимость копирования; неудобно для больших изменяемых структур |
| Передача сообщений (акторы, каналы) | Явный поток данных; целые классы ошибок исчезают | Накладные расходы на сообщение; может скрывать обратное давление при неограниченных очередях |
| Async/await | Дешёвая конкурентность для работы, ограниченной вводом-выводом; код выглядит последовательным | Нет параллелизма для работы ЦП; блокировка задачи останавливает другие |
| Структурированная конкурентность | Ясные времена жизни задач; ошибки распространяются; нет утёкших задач | Новее, менее доступна в некоторых экосистемах |
| Параллелизм данных | Почти линейное ускорение на независимой работе | Потолок Амдала; накладные расходы доминируют на малых вводах |
| Атомарные операции / без блокировок | Нет состязания за блокировки для простых значений | Крайне легко ошибиться тонко; трудно рецензировать |
Основное напряжение это безопасность против сырой скорости, и разрешение в том, чтобы сначала купить корректность и тратить производительность только там, где измерение доказывает необходимость. Сырые блокировки общей памяти самые быстрые на операцию и самые опасные на строку кода; модели высокого уровня стоят немного пропускной способности и возвращают много безопасности и ясности, и для кода, сопровождаемого многими руками, этот размен решительно стоит делать. Резервируйте вручную настроенную конкурентность без блокировок для малых горячих точек, где профилировщик (глава 2.16) доказывает, что накладные расходы координации важны, и даже их держите за хорошо протестированной границей.
Вопросы для обсуждения с командой
Для вашего самого загруженного сервиса нагрузка ограничена вводом-выводом или ЦП и соответствует ли ей ваш проект конкурентности? Команды регулярно добавляют пулы потоков в сервисы, проводящие 95% времени в ожидании базы данных, получая состязание, но не пропускную способность, или пытаются распараллелить вычисление, чья последовательная доля ограничивает любое ускорение. Правильный проект следует из режима: async или малый пул для работы, где много ожидания, настоящий параллелизм по ядрам для работы, где много вычислений. Принесите профиль, показывающий, куда на самом деле уходит время, а не допущение, и если большая часть времени уходит на вычисления, измерьте последовательную долю и позвольте закону Амдала назвать потолок. Ответ определит, обратитесь ли вы к async, ограниченному пулу или параллелизму данных.
Каково умолчание вашей команды для разделения состояния между задачами и безопасно ли оно по построению? В большой команде умолчание важнее исключений, потому что большая часть кода пишется людьми, не являющимися специалистами по конкурентности, которые копируют любой шаблон, уже имеющийся. Если умолчание общие изменяемые объекты под стихийными блокировками, вы в одной забытой блокировке от гонки, всплывающей месяцы спустя в эксплуатации. Если умолчание неизменяемость и передача сообщений, целые категории ошибок никогда не возникают, а редкое место, действительно нуждающееся в общей памяти, выделяется для тщательного ревью. Обсудите, к чему сегодня потянулся бы новый инженер, поймали бы ваши ревью несинхронизированную запись и как сделать безопасный путь лёгким.
Как бы вы нашли, воспроизвели и исправили ошибку конкурентности, проявляющуюся раз на миллион запросов в эксплуатации? Честный ответ для многих команд: не смогли бы, потому что ошибка исчезает, когда на неё смотришь, а их тесты запускают лишь одно безобидное чередование. Это должно тревожить, потому что такие ошибки молча портят данные и подтачивают доверие. Поговорите о том, запускаете ли вы детекторы гонок и санитайзеры потоков в непрерывной интеграции, проводите ли стресс-тесты с рандомизированным временем и фиксирует ли ваша эксплуатационная наблюдаемость состояние потоков и блокировок в момент зависания. Лучшие команды отвечают тем, что делают большинство таких ошибок невозможными выбором модели, так что остаточные немногие редки и локализованы.
Где в вашей системе всё ещё существует неограниченная очередь или пул потоков без предела и что с ним происходит при внезапном десятикратном всплеске? Это важно, потому что неограниченная работа в полёте это отказ, маскирующийся под таинственный сбой из-за нехватки памяти: работа приходит быстрее, чем уходит, память заполняется, а сервис умирает, выглядя как аппаратная неисправность, а не перегрузка, которой он является. Противоположные соображения реальны, потому что слишком низкая граница отклоняет законный трафик, а слишком высокая откладывает сбой вместо его предотвращения, так что число это решение о мощности, а не догадка. Принесите опись каждой очереди и пула, её текущую границу (или признание, что её нет), поведение обратного давления при заполнении и свидетельства нагрузочных тестов о том, как система деградирует на краю. Для корпоративного парка одна неограниченная очередь может каскадом привести к сбою всего парка, а для государственной платформы, обязанной оставаться доступной гражданам, изящный отказ с ясной ошибкой обязательство перед сервисом, поэтому граница и путь отказа принадлежат плану мощностей и ранбуку, а не памяти одного инженера.
Какова политика вашей команды по использованию моделей конкурентности высокого уровня против написанных вручную блокировок и где вы допустили исключения? Модель по умолчанию определяет, насколько безопасно среднее изменение, потому что большинство авторов не специалисты по конкурентности и будут копировать любой уже существующий шаблон: акторы, каналы и структурированная конкурентность поднимают нижнюю планку для всех, тогда как сырые блокировки корректны в теории и источник взаимных блокировок на практике. Напряжение в том, что модели высокого уровня стоят небольших накладных расходов на сообщение или задачу, а профилировщик иногда докажет, что горячему пути нужен вручную настроенный код без блокировок, так что общий запрет так же неверен, как и вседозволенность. Принесите список мест, где вы опустились ниже безопасного умолчания, свидетельства профилирования, оправдавшие каждое, и как каждое исключение огорожено протестированной границей и документированным порядком блокировок. В большой корпорации эта политика не даёт тысячам участников изобретать каждый небезопасную схему, а в долгоживущей государственной системе позволяет рецензенту годы спустя понять, почему опасный шаблон был разрешён, и подтвердить, что он по-прежнему оправдан.
Когда вы решаете распараллелить вычисление, как вы измеряете последовательную долю и кто отвечает за подтверждение того, что ускорение реально? Команды регулярно распределяют вычисление по ядрам и празднуют число, которое профилировщик никогда бы не подтвердил, потому что закон Амдала ограничивает выигрыш обратной величиной последовательной доли, сколько бы ядер вы ни добавили, а накладные расходы разбиения и слияния могут полностью стереть пользу на малых вводах. Противоположное тяготение в том, что параллелизм добавляет реальную сложность и новую поверхность для гонок, поэтому вопрос в том, оправдывает ли измеренное ускорение принимаемый риск корректности. Принесите профиль, изолирующий последовательную часть, размеры ввода, где параллелизм действительно выигрывает, и бенчмарк «до и после» на репрезентативном оборудовании, а не обнадёживающую оценку. Для корпорации, платящей за большой парк вычислений, честный анализ последовательной доли превращается в сэкономленные или потраченные впустую расходы на оборудование, а для государственного органа, отвечающего за стоимость публичной системы, тот, кто одобрил параллельный проект, должен уметь показать измерение, оправдавшее его, при аудите.
Взгляд по секторам
Стартап. С крошечной командой и без запаса времени покупайте корректность структурой, а не специалистом по конкурентности, которого вы не можете нанять. Обращайтесь к единственному безопасному умолчанию, которое даёт ваш язык: async/await для работы, ограниченной вводом-выводом, одна владеющая задача или актор для любого общего состояния, и полностью пропустите настроенные вручную блокировки. Гонка потерянного обновления в платёжном пути может потопить вас быстрее пропущенной функции, поэтому потратьте небольшое дополнительное количество кода, чтобы сделать этот класс ошибок невозможным, и двигайтесь дальше.
Малый бизнес. У вас нет никого, чья работа конкурентность, поэтому отдавайте предпочтение платформам и управляемым сервисам, которые делают это за вас: транзакция базы данных, хостинговая очередь или модель запросов фреймворка лучше потоков, которые вы поддерживаете вручную. Оценивая инструмент, воспринимайте «делает ли он конкурентность безопасной по умолчанию» как вопрос «купить или построить» и предпочитайте вариант, где неверное чередование не может молча испортить запись клиента. Держите общее изменяемое состояние вне собственного кода везде, где его может хранить ограниченный управляемый сервис.
Крупная организация. Между многими командами цель домашнее умолчание, удерживающее тысячи участников в безопасности: неизменяемость и передача сообщений как норма, модели высокого уровня вместо сырых блокировок, ограниченные очереди и пулы с обратным давлением и документированный глобальный порядок блокировок. Закодируйте это в инженерных стандартах, обеспечивайте детекторами гонок и стресс-тестами в CI и управляйте исключениями там, где профилировщик оправдал код без блокировок, чтобы каждое оставалось за протестированной рецензируемой границей. Управляйте мощностью конкурентности как заботой всего парка с границами очередей и размерами пулов, привязанными к измеренной нагрузке.
Государственный сектор. Корректность и проверяемость в системах, работающих десятилетиями, важнее сырой пропускной способности. Требуйте, чтобы каждый переход состояния записывался и воспроизводился, чтобы предполагаемую гонку можно было воспроизвести, а исправление доказать надзорному органу, и держите детерминированные пути без ИИ для решений, касающихся пособий, безопасности или публичных записей. Закупки должны требовать от поставщиков раскрытия их модели конкурентности и свидетельств охвата детекторами гонок и стресс-тестами, потому что неверный ответ под нагрузкой в публичной системе не неудобство, а то, что подотчётное должностное лицо должно будет позже объяснять.
Примеры
Стартап. Небольшая команда выпускает платёжную функцию и замечает, что балансы счетов иногда дрейфуют на несколько копеек под нагрузкой. Причина простое «прочитать-изменить-записать» поля баланса из параллельных обработчиков запросов, гонка потерянного обновления. Вместо россыпи блокировок они переносят баланс каждого счёта за одну владеющую задачу, обрабатывающую дебеты и кредиты как сообщения по одному. Дрейф исчезает, код становится легко осмыслить, и они добавляют стресс-тест, запускающий тысячи параллельных переводов для охраны исправления. Одно структурное изменение, целый класс ошибок устранён.
Крупная организация. Высоконагруженный сервис заказов, обрабатывающий десятки тысяч запросов в секунду, страдает периодическими скачками задержки и иногда сбоями из-за нехватки памяти при всплесках трафика. Расследование находит неограниченную рабочую очередь за пулом потоков, растущую без предела, как только спрос превышает мощность. Команда ограничивает очередь, ставит предел пулу по числу ядер и добавляет обратное давление, быстро отклоняющее избыточную нагрузку с ясной ошибкой. Пропускная способность становится предсказуемой, сбои прекращаются, а горячая блокировка на общем кэше заменяется структурой без блокировок только после того, как профилировщик доказывает, что состязание реально. Безопасные умолчания для многих авторов, настроенная конкурентность только там, где измерено.
Государственный сектор. Национальная платформа пособий работает десятилетиями и должна давать проверяемые корректные результаты даже при параллельных обновлениях дел. Команда выбирает неизменяемость и передачу сообщений как домашнее умолчание, ограничивает каждую единицу изменяемого состояния единственным владельцем и вводит глобальный порядок блокировок везде, где блокировки остаются, всё записано в инженерные стандарты. Они запускают санитайзеры потоков и рандомизированные стресс-тесты в конвейере и проектируют так, чтобы каждый переход состояния записывался и воспроизводился для надзора, что позволяет воспроизвести и доказать исправление, когда подозревается редкое чередование. Корректность и проверяемость воспринимаются как первоклассные требования, а не запоздалые мысли о производительности.
Бизнес-обоснование: мотивы, ROI и TCO
Отдача от дисциплинированной конкурентности проявляется в инцидентах, которых не происходит. Одна гонка в эксплуатации может испортить данные по тысячам записей, а стоимость включает и инженерные часы на поиск ошибки, прячущейся при наблюдении, и гораздо большую стоимость сверки плохих данных, уведомления пострадавших пользователей и восстановления доверия. Это одни из самых дорогих для диагностики дефектов именно потому, что они недетерминированы, поэтому погоня за одним гейзенбагом может затмить усилия по выбору безопасной модели заранее.
Выгода проявляется и в пропускной способности и стоимости. Правильный размер конкурентности позволяет сервису обрабатывать гораздо больше нагрузки на том же оборудовании, повторяющаяся экономия для большого парка, тогда как обратное давление и ограниченные очереди предотвращают каскадные сбои, превращающие всплеск трафика в публичный инцидент. Совокупная стоимость владения скромна и в основном культурна: вы вкладываетесь в домашний стиль (неизменяемость, передача сообщений, структурированная конкурентность), в инструменты (детекторы гонок, санитайзеры потоков, стресс-стенды в CI) и в стандарты, кодирующие порядок блокировок и ограничения. Альтернатива кодовая база, где корректность зависит от того, что каждый автор вечно эксперт, чего никакая растущая команда выдержать не может. Обосновывайте руководству в его единицах: переводите предотвращённую гонку в избегаемые инциденты порчи данных, обратное давление в предотвращённые сбои, а безопасное умолчание в сэкономленное время адаптации.
Антипаттерны и ловушки
- Добавление потоков ради скорости на работе, ограниченной вводом-выводом. Больше потоков в сервисе с большим ожиданием покупает состязание, а не пропускную способность.
- Общее изменяемое состояние везде. Любой поток, изменяющий любой объект, делает корректность делом удачи, которую никакой рецензент не может проверить.
- Неограниченные очереди и пулы. Всплеск растит очередь, пока не умрёт память; сбой выглядит таинственным, но это простая перегрузка.
- Стихийные блокировки без глобального порядка. Блокировки, берущиеся в разном порядке по кодовой базе, взаимно блокируются под нагрузкой.
- Предположение, что код выполняется в написанном порядке. Игнорирование модели памяти, из-за чего ошибка видимости оставляет поток крутиться на устаревшем значении.
- Самодельная хитрость без блокировок. Самодельные схемы без блокировок почти всегда тонко неверны и почти невозможны для рецензирования.
- Тестирование только счастливого чередования. Детерминированные тесты проходят, пока чередование один на миллион портит эксплуатацию.
- Вызов неизвестного кода при удержании блокировки. Обратный вызов, блокирующийся или входящий повторно, превращает критическую секцию во взаимную блокировку.
Модель зрелости
- Уровень 1, Начальный: Конкурентность стихийна и реактивна. Потоки и блокировки добавляются по инстинкту, общее изменяемое состояние повсюду, а очереди неограниченны. Состояния гонки проявляются как невоспроизводимые инциденты в эксплуатации, которые никто не может диагностировать, и нет инструментов для их ловли.
- Уровень 2, Развитие: Некоторые команды освоили базовые практики: используют блокировки с большей осторожностью и ограничивают самые очевидные очереди. Есть неформальная осведомлённость о гонках и взаимных блокировках, и несколько критических путей получают дополнительную проверку. Практика непоследовательна между командами, тестирование по-прежнему в основном одночередовое, а безопасные шаблоны живут в отдельных людях, а не на письме.
- Уровень 3, Стандартизация: У организации есть документированный домашний стиль, соблюдаемый по всей организации: неизменяемость и передача сообщений как умолчания, модели высокого уровня вместо сырых блокировок, ограниченные очереди и пулы с обратным давлением и документированный глобальный порядок блокировок. Детекторы гонок и стресс-тесты работают в CI, а выбор конкурентности следует из того, ограничена ли работа вводом-выводом или ЦП.
- Уровень 4, Управление: Организация измеряет и контролирует свою позицию по конкурентности относительно базовых значений. Она отслеживает охват детекторами гонок и санитайзерами потоков по сервисам, записывает глубину очередей, время ожидания блокировок, насыщение пулов и долю отклонений как отслеживаемые метрики и нагрузочно тестирует кривую деградации, так что каждая граница это решение о мощности на основе данных. Инциденты конкурентности подсчитываются и анализируются по трендам, последовательные доли распараллеленных нагрузок измеряются относительно реально достигнутого ускорения, а решения «идти или не идти» по новым проектам опираются на эти свидетельства, а не на инстинкт.
- Уровень 5, Оркестрация: Безопасная конкурентность путь наименьшего сопротивления для каждого автора, а практика непрерывно улучшается и интегрирована по организации. Целые классы ошибок невозможны по построению, горячие точки настраиваются только там, где профилирование это доказывает, а детерминированное воспроизведение делает редкую остаточную ошибку воспроизводимой. Корректность и проверяемость непрерывно защищаемые свойства, границы мощности адаптируются к наблюдаемой нагрузке, а стандарты эволюционируют по мере смещения платформы и нагрузки.
Идеи для обсуждения
- Если бы вы сегодня проверили ваш самый загруженный сервис, какая часть его состояния общая и изменяемая и какая часть этого разделения действительно необходима?
- Каков ответ вашей команды по умолчанию, когда двум задачам нужно координироваться, и предпочли бы вы, чтобы это была неизменяемость или передача сообщений?
- Где неограниченные очереди или пулы без предела всё ещё прячутся в вашей системе и что с ними произойдёт при внезапном десятикратном всплеске трафика?
- Включают ли ваши запуски непрерывной интеграции детектор гонок или санитайзер потоков и когда один из них в последний раз поймал что-то до эксплуатации?
- Какова последовательная доля вашей самой распараллеленной нагрузки и ограничивает ли закон Амдала ускорение, за которым вы на самом деле гонитесь?
- Могла бы ваша команда воспроизвести ошибку чередования один на миллион по требованию и что потребовалось бы, чтобы до этого дойти?
Главные выводы
- Конкурентность структурирует программу как независимые задачи; параллелизм выполняет их одновременно. Решите, что вам нужно, прежде чем добавлять потоки.
- Общее изменяемое состояние корень почти каждой ошибки конкурентности; предпочитайте неизменяемость и передачу сообщений как безопасные умолчания для многих авторов.
- Обращайтесь к моделям высокого уровня (акторы, каналы, структурированная конкурентность, async/await) раньше написанных вручную блокировок, корректных в теории и опасных на практике.
- Понимайте атомарность, видимость и вашу модель памяти; используйте правильный примитив, держите блокировки кратко и вводите глобальный порядок блокировок, чтобы избежать взаимной блокировки, livelock и голодания.
- Ограничивайте каждую очередь и пул и применяйте обратное давление, чтобы всплеск деградировал изящно, а не падал (глава 3.3).
- Тестируйте чередования намеренно детекторами гонок, стресс-тестами и воспроизведением (глава 2.15) и уважайте закон Амдала при распараллеливании (глава 2.16).
- Для корпораций это пропускная способность и предотвращённые инциденты; для государства корректность и проверяемость в долгоживущих системах.
Источники и дополнительное чтение
- Brian Goetz et al., Java Concurrency in Practice (atomicity, visibility, the memory model, and safe publication).
- Herb Sutter, “The Free Lunch Is Over” (why software must embrace concurrency as clock speeds plateau).
- Leslie Lamport, “Time, Clocks, and the Ordering of Events in a Distributed System” (ordering and the foundations of concurrent reasoning).
- C. A. R. Hoare, “Communicating Sequential Processes” (Communications of the ACM, 1978): the CSP model behind channels.
- Carl Hewitt, Peter Bishop, and Richard Steiger, “A Universal Modular Actor Formalism for Artificial Intelligence” (the origin of the actor model).
- Edsger W. Dijkstra, “Cooperating Sequential Processes” (semaphores, mutual exclusion, and the deadlock problem).
- Maurice Herlihy and Nir Shavit, The Art of Multiprocessor Programming (locks, atomics, and lock-free data structures).
- Nathaniel J. Smith, “Notes on Structured Concurrency, or: Go Statement Considered Harmful” (the case for structured concurrency).
- Martin Kleppmann, Designing Data-Intensive Applications (concurrency and consistency where memory meets distributed systems).
- Gene M. Amdahl, “Validity of the Single Processor Approach to Achieving Large-Scale Computing Capabilities” (1967): the origin of Amdahl’s law.