fedorthinks

Экспресс-курс · No. 16

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

19 понятий · схема у каждого · шпаргалка в конце

§ 01

Два слова используют так, будто они значат одно, а это не так. Одно про жонглирование; другое про много рук. Разложить их по местам — фундамент для всего остального.

Карта: 19 понятий в шести группах

Карта метро: сначала видишь линии целиком, потом едешь по станциям.

Сначала основы — что такое concurrency, parallelism и ядро процессора. Дальше пять групп, по одной на главу: потоки и процессы, ожидание (blocking, async, event loop), общее состояние и гонки, блокировки и их опасности, и более безопасные паттерны, что убирают гонку в корне.

BasicsConcurrencyParallelismCPU coreThreads & processesProcessThreadContext switchWaitingBlocking callAsync / awaitEvent loopI/O- vs CPU-boundShared stateRace conditionNon-determinismShared mutable stateLocksLock / mutexDeadlockContentionSafer patternsMessage passingImmutabilityAtomic operation
Шесть групп понятий — по ним идут главы 1–6. Глава 7 собирает всё вместе.

Как читать каждое понятие

Паспорт: одинаковые поля у всех, поэтому их легко сравнивать.

У каждого понятия дальше одно и то же: английское название (запомни его — это термин, по нему ищут в документации), образ из жизни, схема и карточка с полями: проблема, идея, пример, цена и связи с другими понятиями. В конце — карта связей и шпаргалка.

Concurrency — конкурентность, жонглирование задачами

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

Concurrency (конкурентность) — это управление многими задачами через переключение между ними: один работник продвигает несколько дел, но никогда не делает два в один и тот же миг. Один повар, жонглирующий четырьмя блюдами, — это конкурентность. Это способ структурировать работу.

WorkerABCDACBDABCDtime4 tasks · 1 worker · fast switchingprogress on all four — but never two at the same instant
Один работник, четыре задачи: он быстро переключается, и все движутся вперёд — но в каждый миг делается только одна.
ConceptConcurrency· конкурентность
Проблема
Задач много, а работник один — и он тратит время на ожидание.
Идея
Переключаться между задачами, чтобы все двигались вперёд.
Пример
Один повар ведёт четыре блюда, по шагу от каждого по очереди.
Цена

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

Связи

Часто сочетается с Parallelism, но это другая идея. Основа для Async и Event Loop.

Parallelism — параллелизм, много рук

Четыре повара, каждый готовит своё блюдо в один и тот же миг.

Parallelism (параллелизм) — это когда много задач буквально работают в один миг, на нескольких работниках. Четыре повара — это параллелизм. Это способ исполнять работу. Concurrency и parallelism часто идут вместе, но это разные идеи: первое — про то, как разложить работу, второе — про то, сколько рук её делает.

Core 1Task ACore 2Task BCore 3Task CCore 4Task Dsame instant4 tasksreally runtogethertime
Четыре ядра, четыре задачи — в один и тот же миг все четыре реально работают.
ConceptParallelism· параллелизм
Проблема

Работы так много, что одного работника не хватает, даже если он не ждёт.

Идея
Запустить задачи в один миг на нескольких работниках (ядрах).
Пример
Большое вычисление, разбитое на куски, задействует каждое ядро машины.
Цена
Нужно несколько ядер; общие данные между ними становятся опасны.
Связи

Лекарство для CPU-bound работы. Опирается на CPU Core, Thread и Process.

CPU Core — одно ядро конкурентно, много ядер параллельны

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

Ядро (core) процессора делает одно за раз, но может переключаться между задачами так быстро, что кажется, будто делает многое, — это конкурентность на одном ядре. Настоящему параллелизму нужно несколько ядер, реально работающих разом. У современных машин много ядер, так что реальный софт обычно делает оба: несколько задач в полёте (конкурентно), часть по-настоящему вместе (параллельно).

Core 1ABABABCore 2CDCDCDtimeswitching inside one core = concurrencytwo cores running at once = parallelism
Внутри каждого ядра задачи чередуются (конкурентность); два ядра работают одновременно (параллелизм).
TermCPU Core· ядро процессора
Что это
Один исполнитель в процессоре: в каждый миг делает одно дело.
Как работает

Быстро переключается между задачами — поэтому кажется, что делает многое.

Пример
Машина с 8 ядрами может по-настоящему делать 8 вещей в один миг.
Зачем знать
Число ядер объясняет, какие ускорения вообще возможны.
Связи

Одно ядро даёт Concurrency, много ядер — Parallelism. Переключение стоит Context Switch.

Зачем вообще заморачиваться

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

Делать одно за раз тратит огромные количества ожидания. Пока одна задача ждёт — сетевого ответа, диска, пользователя, — работник мог бы продвигать другую. Конкурентность отвоёвывает это простаивающее время; параллелизм добавляет сырую мощь под тяжёлую работу. Вместе они — вот почему приложение остаётся отзывчивым при загрузке и почему большое вычисление может задействовать каждое ядро. Цена — сложность, о которой весь остаток курса.

one at a timeAA waits (network)BB waits (disk)done (late)overlapped (concurrent)AA waitsBB waitsdone (sooner)while A waits, the worker already starts B
По очереди: работник простаивает, пока ждёт. Внахлёст: пока A ждёт, уже идёт B — и всё кончается раньше.

Concurrency — это жонглирование многими задачами через переключение; parallelism — запуск их в один миг на многих ядрах. Разные идеи: одна структурирует работу, другая её исполняет.

§ 02

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

Process — процесс, изолированная программа

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

Процесс (process) — это работающая программа со своей приватной памятью, изолированная от других процессов. Два процесса не могут случайно испортить данные друг друга, потому что не делят ничего, — чтобы общаться, они должны намеренно передавать сообщения. Эта изоляция делает процессы безопасными и крепкими, но тяжелее: запуск одного стоит больше, а разговор между ними медленнее.

Process APrivate memoryits own thread(s)Process BPrivate memoryits own thread(s)messagereplynothing shared → safe, but heavier to start and to talk
У каждого процесса своя память. Общаются только сообщениями — безопасно, но дороже.
ConceptProcess· процесс
Проблема
Одна программа может случайно испортить данные другой.
Идея
Каждой программе — своя приватная память, отделённая стеной.
Пример
Две запущенные программы на машине; общаются только сообщениями.
Цена
Тяжелее: запуск стоит больше, разговор между процессами медленнее.
Связи

Внутри процесса живут Thread. Общение процессов — это уже Message Passing.

Thread — поток, делит память с собратьями

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

Поток (thread) — это линия исполнения внутри процесса, и потоки одного процесса делят ту же память. Это делает их лёгкими и быстрыми в координации — но это же и источник почти каждого бага конкурентности, потому что два потока могут тронуть одни данные в одно время. Общая память — это мощь и опасность: дешёвое общение, опасные столкновения.

One processThread 1Thread 2Thread 3Shared memorythe same data for every threadcheap to coordinate · can collide on the same data
Три потока в одном процессе смотрят в одну и ту же общую память.
ConceptThread· поток
Проблема
Процессы тяжёлые, а частям одной программы надо тесно делить данные.
Идея
Несколько линий исполнения внутри одного процесса, с общей памятью.
Пример
Несколько поваров на одной кухне, у одних и тех же столов.
Цена
Два потока могут тронуть одни данные разом — корень почти всех гонок.
Связи

Общая память потоков — это Shared Mutable State, отсюда Race Condition и Lock.

Context Switch — переключение контекста стоит времени

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

Когда одно ядро переключается с задачи на задачу, оно делает переключение контекста (context switch): сохраняет, где было, и грузит состояние следующей задачи. Это быстро, но не бесплатно, и делать это слишком часто — тысячи крошечных задач, молотящих, — тратит время в чистых накладных. Поэтому «просто сделай больше потоков» не автоматически быстрее.

a few switches — fineTask AswitchTask BswitchTask Athrashing — thousands of tiny tasksAswBswCswDswEswFswGswHswIswJswsave state · load state — past a point the juggling costs more than it saves
Пара переключений — мелочь. Тысячи крошечных задач — и время уходит на само переключение.
TermContext Switch· переключение контекста
Что это
Ядро сохраняет состояние одной задачи и загружает состояние другой.
Пример

Ядро чередует задачи A и B; между ними — короткая пауза на «сохранить и загрузить».

Подвох

Слишком много задач — и само жонглирование стоит больше, чем экономит («молотилка»).

Вывод
«Больше потоков» не значит автоматически «быстрее».
Связи
Цена Concurrency на одном CPU Core; растёт с числом Thread.

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

§ 03

Бо́льшую часть времени программа не вычисляет — она ждёт. Как ты обращаешься с этим ожиданием, блокирующе или асинхронно, решает, может ли один работник оставаться занятым или сидит без дела.

Blocking Call — блокирующий вызов останавливается и ждёт

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

Блокирующий (blocking) вызов — это когда работник останавливается и ждёт результата, прежде чем делать что-либо ещё, — спросил базу и просто стоит, пока она не ответит. Пока заблокирован, тот работник не достигает ничего. Для одного быстрого шага это нормально; но если блокируешься на медленном — сеть, диск, другие сервисы, — ты тратишь ровно то время, что мог бы потратить на другую работу.

Workerasks and standsDatabaseslow answerqueryanswer (much later)workblocked — doing nothingworkthe worker stands still until the answer comes back
Работник отправил запрос и стоит без дела, пока не придёт ответ.
TermBlocking Call· блокирующий вызов
Что это
Вызов, на котором работник останавливается и ждёт результата.
Пример
Спросил базу — и стоишь, пока она не ответит.
Когда нормально
Один быстрый шаг: ждать почти нечего.
Цена
На медленном (сеть, диск, другие сервисы) работник тратит время впустую.
Связи
Противоположность Async / Await.

Async / Await — один работник остаётся занятым

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

Асинхронная (asynchronous, неблокирующая) работа это переворачивает: ты начинаешь медленную операцию и вместо ожидания идёшь делать что-то ещё, подхватывая результат, когда он готов. Ключевые слова async и await во многих языках выражают ровно это — «начни это и дай мне продолжить; вернись, когда закончится». Один работник может иметь сотни медленных операций в полёте разом. Так один поток обслуживает тысячи ждущих соединений.

Worker (one thread)start Astart Bstart Cother workA ✓C ✓B ✓await A — in flightawait B — in flightawait C — in flightstart the slow call, keep working, pick up the result when readyawait = “come back here when it finishes”
Работник запускает A, B и C, делает другую работу и забирает каждый результат, когда тот готов.
ConceptAsync / Await· асинхронность, неблокирующая работа
Проблема
Блокирующий работник простаивает, пока ждёт медленное.
Идея
Начать медленное, не ждать, а продолжить; вернуться, когда готово.
Пример

Скрипт делает много сетевых вызовов: с async они идут вместе, а не один за другим.

Цена
Код сложнее читать; помогает только там, где есть ожидание.
Когда брать
I/O-bound работа: сеть, диск, другие сервисы.
Связи
Обычно работает на Event Loop. Противоположность Blocking Call.

Event Loop — цикл событий жонглирует ожиданием

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

Async обычно работает на цикле событий (event loop): один работник, что начинает задачи, и всякий раз, когда медленное завершается, выполняет маленький кусок кода, что его ждал, и идёт дальше. Ощущается, будто много всего происходит разом, но это один работник, быстро переключающийся между готовыми задачами. Это модель за средами вроде JavaScript и Node — поразительно эффективными в жонглировании ожиданием, на одном потоке.

Ready queuereply arrivedtimer firedfile readEvent loopone threadSlow I/Onetwork · disknextstartrepeatwhen done, its callback joins the queue
Цикл берёт следующую готовую задачу, запускает медленное и повторяет. Готовые ответы встают в очередь.
ConceptEvent Loop· цикл событий
Что это
Один поток, который по кругу берёт следующую готовую задачу.
Как работает

Запускает медленное; когда оно кончилось, выполняет маленький кусок кода, что его ждал.

Пример
JavaScript и Node: тысячи ожиданий на одном потоке.
Подвох

Тяжёлое вычисление занимает этот единственный поток — и все остальные ждут.

Связи
Двигатель Async / Await; лучше всего для I/O-bound работы.

I/O-bound vs CPU-bound — чего ждёт твоя работа

Работе, что в основном ждёт доставок, помогает жонглёр получше; работе, что в основном тяжёлый подъём, нужно больше мускулов, а не лучшее расписание.

Правильный инструмент зависит от того, чего твоя работа ждёт. I/O-bound работа — в основном ждёт сети, диска или других сервисов — идеальна для async: один работник жонглирует всем ожиданием. CPU-bound работа — тяжёлое вычисление, что держит ядро полностью занятым, — не получает помощи от async (нет простаивающего времени отвоёвывать) и вместо этого нуждается в параллелизме по ядрам. Неверно оценить, что у тебя, — вот почему некоторые «ускорения» не делают ничего.

I/O-boundcpuwaitcpuwaitcpuAsyncjuggle the waitingCPU-boundbusy computingParallelismmore coresasync can't help work that never waits
Слева работа в основном ждёт — её спасает async. Справа ядро всё время занято — нужны ещё ядра.
TermI/O-bound vs CPU-bound· упор в ожидание / упор в вычисления
I/O-bound
Работа в основном ждёт сети, диска, других сервисов → async.
CPU-bound
Тяжёлое вычисление держит ядро занятым → parallelism по ядрам.
Пример

Много сетевых вызовов — I/O-bound. Большое вычисление на всех ядрах — CPU-bound.

Подвох
Перепутать — и «ускорение» не даёт ничего.
Связи
Выбирает между Async / Await и Parallelism.

Блокирующий останавливается и ждёт; async начинает медленное и остаётся занятым. Async выигрывает для ожидания (I/O-bound); настоящий параллелизм — для тяжёлых вычислений (CPU-bound).

§ 04

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

Race Condition — состояние гонки: две руки, один предмет

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

Состояние гонки (race condition) — это когда результат зависит от точного тайминга конкурентных операций, трогающих общие данные. Два потока оба читают счётчик как 5, оба прибавляют один, оба пишут 6 — и единица тихо теряется, потому что верный ответ был 7. Никто не сделал ошибки в логике; шаги просто переплелись плохо. Это определяющий баг конкурентности.

Thread AcounterThread Bread 5read 5write 5 + 1 = 6write 5 + 1 = 65566expected 7 — got 6: one update silently lostno logic mistake — the steps just interleaved badly
Оба потока прочитали 5, оба записали 6. Ждали 7 — одно обновление тихо потерялось.
TermRace Condition· состояние гонки
Что это
Результат зависит от точного тайминга задач над общими данными.
Пример
Два потока читают счётчик 5, оба пишут 6 — а правильный ответ 7.
Причина
Общие и изменяемые данные плюс одновременный доступ.
Подвох
Ошибки в логике нет — шаги просто переплелись в плохом порядке.
Связи

Растёт из Shared Mutable State. Лечится Lock, Message Passing, Immutability, Atomic Operation.

Non-determinism — ужас в том, что он прерывист

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

Состояния гонки мерзки, потому что недетерминированы (non-deterministic): они зависят от тайминга, так что появляются раз в тысячу прогонов, никогда в твоих тестах и только под реальной нагрузкой в проде. Один и тот же ввод может сработать или упасть в зависимости от микроскопического расписания. Поэтому у багов конкурентности грозная репутация — их нельзя надёжно воспроизвести, и «у меня на машине работает» — ровно то, чего ждёшь, даже когда оно сломано.

same code · same input · 60 runs✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✗✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓one run fails — it depends on micro-timing, and rarely shows up in tests
Тот же код и тот же ввод: почти все прогоны проходят, а один падает — из-за тайминга.
TermNon-determinism· недетерминированность
Что это
Один и тот же ввод даёт разный результат в разных прогонах.
Почему
Результат зависит от микроскопического расписания задач.
Пример

Баг появляется раз в тысячу прогонов — никогда в тестах, только под нагрузкой в проде.

Подвох

«У меня на машине работает» — ровно то, чего ждёшь, даже когда сломано.

Связи

Делает Race Condition такой опасной. Поэтому баги лучше сделать невозможными, а не ловить.

Shared Mutable State — общее изменяемое состояние

Столкновения случаются только над тем, что обоим позволено хватать и менять, — запри общий шкаф или дай каждому свой, и драки прекратятся.

Заметь точный источник: данные, что одновременно общие (shared — больше одной задачи может дотянуться) и изменяемые (mutable — их можно поменять). Убери любое свойство — и гонка исчезает: данные, что трогает лишь одна задача, безопасны, а данные, что никто не меняет, безопасно делить свободно. Это ключевая мысль, на которой строится остаток курса: каждое лекарство — это способ управлять, ограничить или избежать общего изменяемого состояния.

not sharedsharedimmutablemutablesafeprivate and frozensaferead freelysafeowned by one taskRACEthe danger zoneremove either property and the race vanishes
Опасна только одна клетка из четырёх: данные и общие, и изменяемые.
ConceptShared Mutable State· общее изменяемое состояние
Что это
Данные, к которым тянутся несколько задач и которые можно менять.
Идея
Убери «общее» или «изменяемое» — и гонке негде жить.
Пример

Общий счётчик или список, в который пишут два потока, — опасно. Данные одной задачи или неизменяемые — безопасно.

Вывод

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

Связи

Источник Race Condition. Lock управляет им, Message Passing убирает «общее», Immutability убирает «изменяемое».

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

§ 05

Классическое лекарство от гонки — сделать так, чтобы лишь одна задача трогала общие данные за раз. Оно работает — но инструмент, которым ты это насаждаешь, несёт собственную знаменитую опасность.

Lock / Mutex — блокировка даёт одной задаче исключительный доступ

Единственный ключ от туалета на одного: кто держит — заходит; все остальные ждут своей очереди снаружи. Двух человек внутри разом — никогда.

Блокировка (lock, или mutex — от «mutual exclusion», взаимное исключение) гарантирует, что лишь одна задача входит в участок кода за раз. Прежде чем тронуть общие данные, задача должна захватить блокировку; другие, желающие её, ждут, пока она не освободится. Защищённый участок — это критическая секция (critical section). Сделанное верно, это превращает хаотичную свалку над общими данными в упорядоченную очередь по-одному, и состояние гонки исчезает.

critical sectionThread Aholds the lockThread BwaitsLockShared datacounterblockedacquire → work → release: one task at a time
Поток A держит блокировку и работает в критической секции. Поток B ждёт снаружи.
ToolLock / Mutex· блокировка, взаимное исключение
Проблема
Две задачи меняют общие данные разом — гонка.
Идея

Захватить блокировку → поработать в критической секции → отпустить. Остальные ждут.

Пример
Поток берёт блокировку, прибавляет единицу к счётчику и отпускает её.
Цена
Ожидание других задач; риск Deadlock и Contention.
Когда брать
Когда общее изменяемое состояние правда не избежать.
Связи

Управляет Shared Mutable State. Часто дешевле — Atomic Operation или Message Passing.

Deadlock — взаимоблокировка: все ждут вечно

Два человека у узкой двери, каждый отказывается отступить, пока не отступит другой, — оба застряли, вежливо, навсегда.

Блокировки несут свой классический сбой: взаимоблокировку (deadlock). Задача A держит блокировку 1 и хочет блокировку 2; задача B держит блокировку 2 и хочет блокировку 1. Каждая ждёт, пока другая отпустит, и ни одна не отпускает — обе замёрзли навсегда. Это идёт от захвата нескольких блокировок в разном порядке. Частое лекарство — дисциплина: всегда захватывай блокировки в одном согласованном порядке, чтобы круговое ожидание (circular wait) не смогло сложиться.

Task ALock 1Task BLock 2holdsholdswantswantscircular waitfix: always take locks in the same order (1, then 2)
A держит 1 и хочет 2, B держит 2 и хочет 1 — круг замкнулся, обе ждут вечно.
TermDeadlock· взаимоблокировка
Что это
Задачи ждут друг друга по кругу, и ни одна не может продолжить.
Пример
A держит блокировку 1 и хочет 2; B держит 2 и хочет 1.
Причина
Несколько блокировок захватываются в разном порядке.
Лекарство
Всегда брать блокировки в одном согласованном порядке.
Связи
Классическая опасность Lock / Mutex.

Lock Contention — блокировки верны, но дороги

Один ключ от туалета держит порядок, но если он постоянно всем нужен, образуется очередь, и весь офис замедляется до скорости той единственной двери.

Блокировки чинят гонки, но у них есть цена: пока одна задача держит блокировку, другие ждут, так что тяжёлая блокировка может стереть скорость, ради которой ты вообще брал конкурентность. Чрезмерная блокировка создаёт состязание (contention) — все стоят в очереди на одну блокировку. Так что держи блокировки как можно короче, защищай как можно меньше и предпочитай конструкции, что требуют меньше блокировок. Верная программа, сериализованная на одной блокировке, немногим лучше однопоточной.

waiting in lineT5T4T3T2LockT1insideheavy locking serialises the work — correct, but slowhold locks briefly · protect as little as possible
Четыре потока стоят в очереди к одной блокировке — работа идёт по одному.
TermLock Contention· состязание за блокировку
Что это
Много задач стоят в очереди на одну и ту же блокировку.
Пример
Программа, сериализованная на одной блокировке, — почти однопоточная.
Цена
Стирает выигрыш от конкурентности: верно, но медленно.
Лекарство

Держать блокировки коротко, защищать как можно меньше, выбирать конструкции с меньшим числом блокировок.

Связи
Вторая цена Lock / Mutex после Deadlock.

Блокировка даёт одной задаче за раз исключительный доступ, убивая гонку. Но блокировки могут уйти в deadlock, а чрезмерная блокировка сериализует всё — верно, но медленно.

§ 06

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

Message Passing — не дели память, передавай сообщения

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

Мощная альтернатива общей памяти — передача сообщений (message passing): задачи не трогают одни данные; они шлют друг другу сообщения через очередь (queue). Лишь одна задача владеет данным куском, а другие просят изменений, посылая сообщение. Есть знаменитый лозунг: «не общайтесь, деля память; делите память, общаясь». Убери общий доступ — и убираешь гонку в корне, без всяких блокировок.

Task ATask BQueuemsgmsgmsgOwner taskonly it touches the datadon't communicate by sharing memory;share memory by communicating
Задачи шлют сообщения в очередь. Данные трогает только задача-владелец.
PatternMessage Passing· передача сообщений
Проблема
Общая память требует блокировок и рождает гонки.
Идея

Данными владеет одна задача; остальные просят изменений, посылая сообщения через очередь.

Пример

Повара на своих станциях передают тарелки по раздаче, а не делят один стол.

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

Убирает «общее» из Shared Mutable State. Так общаются и Process.

Immutability — неизменяемые данные нельзя загонять

Вручить каждому свою ксерокопию вместо одного общего оригинала — они все могут читать разом, и никто не может черкнуть на том, что читает другой.

Если данные никогда не меняются после создания — если они неизменяемы (immutable), — то любое число задач может читать их в одно время с нулевым риском, потому что опасность была не в делении, а в делении и изменении. Вместо мутации значения на месте ты делаешь новое. Неизменяемость убирает одну половину условия «общее и изменяемое», а с ней — целую категорию багов. Поэтому функциональные стили так сильно на неё опираются.

Value v1never changesValue v2a new copyupdateReader 1Reader 2Reader 3many readers at once — nothing to race on
Значение v1 не меняется — его читают сколько угодно задач. Изменение рождает новое значение v2.
PatternImmutability· неизменяемость, immutable data
Проблема
Общие данные кто-то меняет, пока другие читают.
Идея
Не менять значение на месте, а делать новое.
Пример
Каждому — своя ксерокопия; черкнуть в чужой нельзя.
Цена
Новые копии вместо правки на месте.
Когда брать
Данные, которые многие читают. Опора функционального стиля.
Связи
Убирает «изменяемое» из Shared Mutable State.

Atomic Operation — атомарные операции неделимы

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

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

not atomic: three stepsreadadd 1writeanother task can slip in between the stepsatomic: one stepincrement — one indivisible stepatomic counters · atomic swaps · transactions
«Прочитать, прибавить, записать» — три шага, и между ними может влезть другая задача. Атомарный инкремент — один шаг.
PatternAtomic Operation· атомарная операция
Проблема
Обновление из нескольких шагов может прерваться на середине.
Идея
Сделать обновление одним неделимым шагом.
Пример
Атомарный счётчик, атомарный обмен, транзакция в базе.
Цена
Подходит только для маленьких обновлений, не для большой логики.
Когда брать
Общее меняющееся значение, вроде счётчика.
Связи
Часто дешевле и безопаснее Lock / Mutex; чинит Race Condition.

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

§ 07

Конкурентность — это мощь, что стоит добавлять нарочно, а не рефлексом. Навык — тянуться к ней, только когда задача требует, и выбирать простейшую модель, что делает дело.

Подбери инструмент под узкое место

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

Прежде чем добавлять конкурентность, знай своё узкое место (bottleneck). Если работа I/O-bound — в основном ожидание — async на одном потоке часто решает это просто. Если она CPU-bound — в основном вычисление — нужен настоящий параллелизм по ядрам. Если ни то ни другое — быстро и просто — добавление конкурентности просто покупает баги без выигрыша. Самая частая ошибка — тянуться к потокам, когда обычный последовательный код был уже достаточно быстр.

Where does time go?AsyncI/O-bound: waitingParallelismCPU-bound: computingSequentialalready fast enoughmostly waitingcomputingneithermost common mistake: threads where plain code was enough
Сначала спроси, куда уходит время. Ожидание → async, вычисления → параллелизм, ни то ни другое → оставь как есть.

Держи общую поверхность крошечной

Мастерская, где каждый в основном работает один, с одной маленькой, ясно помеченной общей полкой инструментов, — чем меньше общего, тем меньше может пойти не так.

Раз каждый баг конкурентности идёт от общего изменяемого состояния, главный ход — минимизировать деление. Держи бо́льшую часть данных во владении одной задачи, делай что можешь неизменяемым и ограничь по-настоящему общие, меняющиеся части маленькой, тщательно охраняемой поверхностью. Конструкцию с крошечной общей поверхностью можно реально осмыслить; ту, где всё общее и изменяемое, — это дом с привидениями. Простота тут не опциональна — это вся защита.

Понятия связаны между собой

Генеалогическое древо: у всех бед одна прародительница, а у всех лекарств — одна цель.

Почти всё в курсе крутится вокруг одной точки. Потоки делят память → появляется общее изменяемое состояние → оно рождает гонки. Четыре лекарства — lock, message passing, immutability, atomic — управляют им или убирают его. У блокировок свои беды: deadlock и contention. Отдельно стоит выбор под нагрузку: ожидание → async + event loop, вычисления → parallelism.

Threadsshare memoryShared mutable stateRace conditioncausesLock / mutexMessage passingImmutabilityAtomic opsDeadlockContentionI/O-boundAsync + event loopCPU-boundParallelism (cores)every fix controls, limits or removes shared mutable state
Главные связи между понятиями курса.
Прежде чем добавить конкурентность
  • Какое узкое место — I/O-bound (async), CPU-bound (параллелизм) или ни то ни другое (не надо)?
  • Какие данные общие и изменяемые, и могут ли две задачи тронуть их разом?
  • Могу ли я избежать деления — передачей сообщений, неизменяемостью или одной атомарной операцией?
  • Если надо блокировать, мала ли критическая секция и согласован ли порядок блокировок?
  • Может ли уйти в deadlock, и исключил ли я круговое ожидание?
  • Стоит ли добавленная сложность того, или последовательный код был уже достаточно быстр?
Признаки, что ты справляешься хорошо
  • Ты тянешься к async для ожидания и к параллелизму для вычислений — не рефлексом.
  • Можешь указать ровно, какие данные общие и изменяемые, и их мало.
  • Ты предпочитаешь передачу сообщений и неизменяемость разбрасыванию блокировок повсюду.
  • Твои блокировки короткие, упорядоченные, и ты порассуждал о deadlock.
  • Ты не добавляешь конкурентность там, где обычный последовательный код был уже достаточно быстр.

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

Шпаргалка: все 19 понятий

Основы

Concurrencyконкурентность

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

Parallelismпараллелизм

Много задач в один миг на многих ядрах. Способ исполнять работу.

CPU Coreядро процессора

Делает одно за раз; одно ядро — конкурентность, много — параллелизм.

Потоки и процессы

Processпроцесс

Изолированная программа со своей памятью. Безопасно, но тяжело.

Threadпоток

Линия исполнения внутри процесса; потоки делят память. Быстро и опасно.

Context Switchпереключение контекста

Сохранить одну задачу, загрузить другую. Не бесплатно.

Ожидание

Blocking Callблокирующий вызов

Остановиться и ждать ответа, ничего не делая.

Async / Awaitасинхронность

Начать медленное, продолжить работу, вернуться, когда готово.

Event Loopцикл событий

Один поток по кругу берёт следующую готовую задачу.

I/O-bound vs CPU-boundожидание / вычисления

Ждёт → async. Считает → параллелизм.

Общее состояние

Race Conditionсостояние гонки

Результат зависит от тайминга задач над общими данными.

Non-determinismнедетерминированность

Тот же ввод — разный результат. Баг редко виден в тестах.

Shared Mutable Stateобщее изменяемое состояние

Источник всех гонок. Убери «общее» или «изменяемое».

Блокировки

Lock / Mutexблокировка

Одна задача за раз в критической секции.

Deadlockвзаимоблокировка

Задачи ждут друг друга по кругу. Лекарство — один порядок блокировок.

Lock Contentionсостязание

Очередь на одну блокировку стирает скорость.

Более безопасные паттерны

Message Passingпередача сообщений

У данных один владелец; остальные шлют ему сообщения.

Immutabilityнеизменяемость

Данные не меняются — читать могут все разом.

Atomic Operationатомарная операция

Обновление одним неделимым шагом.

Конец экспресс-курса · 7 глав · 19 понятий

Дальше — практика: возьми что-то, что много ждёт, — скрипт, делающий много сетевых вызовов один за другим, — и сделай вызовы async, чтобы они шли вместе. Смотри, как он заканчивается за долю времени. Потом найди общий счётчик или список и порассуждай, что будет, если две задачи ударят в него разом. Идеи становятся конкретными в тот миг, как ты чувствуешь ускорение и замечаешь гонку. Но держи одну мысль выше прочих: делать много вещей разом — это как софт остаётся быстрым, и каждая сложная часть восходит к тому же корню — две вещи трогают одни данные в одно время. Управь этим — и ты приручил конкурентность.