Экспресс-курс · No. 16
Программа, что делает одно за раз, проста и часто слишком медленна. Так что мы заставляем софт жонглировать многими задачами или гонять их на многих ядрах разом. Отсюда берётся скорость — и отсюда же берётся целое семейство странных багов, все укоренённые в одном: два куска работы трогают одни данные в один момент.
19 понятий · схема у каждого · шпаргалка в конце
Два слова используют так, будто они значат одно, а это не так. Одно про жонглирование; другое про много рук. Разложить их по местам — фундамент для всего остального.
Карта: 19 понятий в шести группах
Карта метро: сначала видишь линии целиком, потом едешь по станциям.
Сначала основы — что такое concurrency, parallelism и ядро процессора. Дальше пять групп, по одной на главу: потоки и процессы, ожидание (blocking, async, event loop), общее состояние и гонки, блокировки и их опасности, и более безопасные паттерны, что убирают гонку в корне.
Как читать каждое понятие
Паспорт: одинаковые поля у всех, поэтому их легко сравнивать.
У каждого понятия дальше одно и то же: английское название (запомни его — это термин, по нему ищут в документации), образ из жизни, схема и карточка с полями: проблема, идея, пример, цена и связи с другими понятиями. В конце — карта связей и шпаргалка.
Concurrency — конкурентность, жонглирование задачами
Один повар ведёт четыре блюда — режет, потом мешает, потом проверяет духовку, быстро переключаясь между ними.
Concurrency (конкурентность) — это управление многими задачами через переключение между ними: один работник продвигает несколько дел, но никогда не делает два в один и тот же миг. Один повар, жонглирующий четырьмя блюдами, — это конкурентность. Это способ структурировать работу.
- Проблема
- Задач много, а работник один — и он тратит время на ожидание.
- Идея
- Переключаться между задачами, чтобы все двигались вперёд.
- Пример
- Один повар ведёт четыре блюда, по шагу от каждого по очереди.
- Цена
Сложность: задачи переплетаются, и порядок шагов становится непредсказуем.
- Связи
Часто сочетается с Parallelism, но это другая идея. Основа для Async и Event Loop.
Parallelism — параллелизм, много рук
Четыре повара, каждый готовит своё блюдо в один и тот же миг.
Parallelism (параллелизм) — это когда много задач буквально работают в один миг, на нескольких работниках. Четыре повара — это параллелизм. Это способ исполнять работу. Concurrency и parallelism часто идут вместе, но это разные идеи: первое — про то, как разложить работу, второе — про то, сколько рук её делает.
- Проблема
Работы так много, что одного работника не хватает, даже если он не ждёт.
- Идея
- Запустить задачи в один миг на нескольких работниках (ядрах).
- Пример
- Большое вычисление, разбитое на куски, задействует каждое ядро машины.
- Цена
- Нужно несколько ядер; общие данные между ними становятся опасны.
- Связи
Лекарство для CPU-bound работы. Опирается на CPU Core, Thread и Process.
CPU Core — одно ядро конкурентно, много ядер параллельны
Один кассир-билетёр может обслужить всю очередь, ведя быстрые шаги каждого по очереди; ряд кассиров обслуживает нескольких в одну и ту же секунду.
Ядро (core) процессора делает одно за раз, но может переключаться между задачами так быстро, что кажется, будто делает многое, — это конкурентность на одном ядре. Настоящему параллелизму нужно несколько ядер, реально работающих разом. У современных машин много ядер, так что реальный софт обычно делает оба: несколько задач в полёте (конкурентно), часть по-настоящему вместе (параллельно).
- Что это
- Один исполнитель в процессоре: в каждый миг делает одно дело.
- Как работает
Быстро переключается между задачами — поэтому кажется, что делает многое.
- Пример
- Машина с 8 ядрами может по-настоящему делать 8 вещей в один миг.
- Зачем знать
- Число ядер объясняет, какие ускорения вообще возможны.
- Связи
Одно ядро даёт Concurrency, много ядер — Parallelism. Переключение стоит Context Switch.
Зачем вообще заморачиваться
Официант, что принял полный заказ одного стола, отнёс его и лишь потом подошёл к следующему, оставил бы зал людей ждать впустую.
Делать одно за раз тратит огромные количества ожидания. Пока одна задача ждёт — сетевого ответа, диска, пользователя, — работник мог бы продвигать другую. Конкурентность отвоёвывает это простаивающее время; параллелизм добавляет сырую мощь под тяжёлую работу. Вместе они — вот почему приложение остаётся отзывчивым при загрузке и почему большое вычисление может задействовать каждое ядро. Цена — сложность, о которой весь остаток курса.
Concurrency — это жонглирование многими задачами через переключение; parallelism — запуск их в один миг на многих ядрах. Разные идеи: одна структурирует работу, другая её исполняет.
Чтобы гонять вещи конкурентно, ты делишь работу на отдельные линии исполнения. Есть два главных вида, и разница между ними в основном про одно: делят ли они память.
Process — процесс, изолированная программа
Две отдельные кухни в двух отдельных зданиях — у каждой свои ингредиенты, свои столы, и они не могут тронуть запасы другой без звонка между ними.
Процесс (process) — это работающая программа со своей приватной памятью, изолированная от других процессов. Два процесса не могут случайно испортить данные друг друга, потому что не делят ничего, — чтобы общаться, они должны намеренно передавать сообщения. Эта изоляция делает процессы безопасными и крепкими, но тяжелее: запуск одного стоит больше, а разговор между ними медленнее.
- Проблема
- Одна программа может случайно испортить данные другой.
- Идея
- Каждой программе — своя приватная память, отделённая стеной.
- Пример
- Две запущенные программы на машине; общаются только сообщениями.
- Цена
- Тяжелее: запуск стоит больше, разговор между процессами медленнее.
- Связи
Внутри процесса живут Thread. Общение процессов — это уже Message Passing.
Thread — поток, делит память с собратьями
Несколько поваров на одной кухне, делящих те же столы и ингредиенты, — быстро координировать, но они могут потянуться за тем же ножом в тот же миг.
Поток (thread) — это линия исполнения внутри процесса, и потоки одного процесса делят ту же память. Это делает их лёгкими и быстрыми в координации — но это же и источник почти каждого бага конкурентности, потому что два потока могут тронуть одни данные в одно время. Общая память — это мощь и опасность: дешёвое общение, опасные столкновения.
- Проблема
- Процессы тяжёлые, а частям одной программы надо тесно делить данные.
- Идея
- Несколько линий исполнения внутри одного процесса, с общей памятью.
- Пример
- Несколько поваров на одной кухне, у одних и тех же столов.
- Цена
- Два потока могут тронуть одни данные разом — корень почти всех гонок.
- Связи
Общая память потоков — это Shared Mutable State, отсюда Race Condition и Lock.
Context Switch — переключение контекста стоит времени
Работник, жонглирующий делами, должен отложить одно, вспомнить точно, где он был, и взять другое, — и всё это откладывание и подхватывание берёт реальное время.
Когда одно ядро переключается с задачи на задачу, оно делает переключение контекста (context switch): сохраняет, где было, и грузит состояние следующей задачи. Это быстро, но не бесплатно, и делать это слишком часто — тысячи крошечных задач, молотящих, — тратит время в чистых накладных. Поэтому «просто сделай больше потоков» не автоматически быстрее.
- Что это
- Ядро сохраняет состояние одной задачи и загружает состояние другой.
- Пример
Ядро чередует задачи A и B; между ними — короткая пауза на «сохранить и загрузить».
- Подвох
Слишком много задач — и само жонглирование стоит больше, чем экономит («молотилка»).
- Вывод
- «Больше потоков» не значит автоматически «быстрее».
- Связи
- Цена Concurrency на одном CPU Core; растёт с числом Thread.
Процессы изолированы, с приватной памятью и безопасным-но-тяжёлым обменом сообщениями. Потоки делят память — лёгкие и быстрые, и корень большинства багов конкурентности.
Бо́льшую часть времени программа не вычисляет — она ждёт. Как ты обращаешься с этим ожиданием, блокирующе или асинхронно, решает, может ли один работник оставаться занятым или сидит без дела.
Blocking Call — блокирующий вызов останавливается и ждёт
Стоять у стойки, пока клерк ушёл в подсобку за твоим заказом, не делая ничего до его возвращения, — ты не можешь помочь никому другому, пока стоишь там.
Блокирующий (blocking) вызов — это когда работник останавливается и ждёт результата, прежде чем делать что-либо ещё, — спросил базу и просто стоит, пока она не ответит. Пока заблокирован, тот работник не достигает ничего. Для одного быстрого шага это нормально; но если блокируешься на медленном — сеть, диск, другие сервисы, — ты тратишь ровно то время, что мог бы потратить на другую работу.
- Что это
- Вызов, на котором работник останавливается и ждёт результата.
- Пример
- Спросил базу — и стоишь, пока она не ответит.
- Когда нормально
- Один быстрый шаг: ждать почти нечего.
- Цена
- На медленном (сеть, диск, другие сервисы) работник тратит время впустую.
- Связи
- Противоположность Async / Await.
Async / Await — один работник остаётся занятым
Взять номерок в людной кулинарии: вместо того чтобы стоять замороженным, ты начинаешь другие дела и возвращаешься, когда твой номер выкликнут.
Асинхронная (asynchronous, неблокирующая) работа это переворачивает: ты начинаешь медленную операцию и вместо ожидания идёшь делать что-то ещё, подхватывая результат, когда он готов. Ключевые слова async и await во многих языках выражают ровно это — «начни это и дай мне продолжить; вернись, когда закончится». Один работник может иметь сотни медленных операций в полёте разом. Так один поток обслуживает тысячи ждущих соединений.
- Проблема
- Блокирующий работник простаивает, пока ждёт медленное.
- Идея
- Начать медленное, не ждать, а продолжить; вернуться, когда готово.
- Пример
Скрипт делает много сетевых вызовов: с async они идут вместе, а не один за другим.
- Цена
- Код сложнее читать; помогает только там, где есть ожидание.
- Когда брать
- I/O-bound работа: сеть, диск, другие сервисы.
- Связи
- Обычно работает на Event Loop. Противоположность Blocking Call.
Event Loop — цикл событий жонглирует ожиданием
Единственный, очень организованный администратор, что принимает каждый звонок, запускает медленные поиски и берётся за того, кто готов следующим, — никогда не заблокированный ни на одном.
Async обычно работает на цикле событий (event loop): один работник, что начинает задачи, и всякий раз, когда медленное завершается, выполняет маленький кусок кода, что его ждал, и идёт дальше. Ощущается, будто много всего происходит разом, но это один работник, быстро переключающийся между готовыми задачами. Это модель за средами вроде JavaScript и Node — поразительно эффективными в жонглировании ожиданием, на одном потоке.
- Что это
- Один поток, который по кругу берёт следующую готовую задачу.
- Как работает
Запускает медленное; когда оно кончилось, выполняет маленький кусок кода, что его ждал.
- Пример
- JavaScript и Node: тысячи ожиданий на одном потоке.
- Подвох
Тяжёлое вычисление занимает этот единственный поток — и все остальные ждут.
- Связи
- Двигатель Async / Await; лучше всего для I/O-bound работы.
I/O-bound vs CPU-bound — чего ждёт твоя работа
Работе, что в основном ждёт доставок, помогает жонглёр получше; работе, что в основном тяжёлый подъём, нужно больше мускулов, а не лучшее расписание.
Правильный инструмент зависит от того, чего твоя работа ждёт. I/O-bound работа — в основном ждёт сети, диска или других сервисов — идеальна для async: один работник жонглирует всем ожиданием. CPU-bound работа — тяжёлое вычисление, что держит ядро полностью занятым, — не получает помощи от async (нет простаивающего времени отвоёвывать) и вместо этого нуждается в параллелизме по ядрам. Неверно оценить, что у тебя, — вот почему некоторые «ускорения» не делают ничего.
- I/O-bound
- Работа в основном ждёт сети, диска, других сервисов → async.
- CPU-bound
- Тяжёлое вычисление держит ядро занятым → parallelism по ядрам.
- Пример
Много сетевых вызовов — I/O-bound. Большое вычисление на всех ядрах — CPU-bound.
- Подвох
- Перепутать — и «ускорение» не даёт ничего.
- Связи
- Выбирает между Async / Await и Parallelism.
Блокирующий останавливается и ждёт; async начинает медленное и остаётся занятым. Async выигрывает для ожидания (I/O-bound); настоящий параллелизм — для тяжёлых вычислений (CPU-bound).
Вот сердце того, почему конкурентность сложна. В тот миг, когда две вещи могут тронуть одни данные в одно время, ты можешь получить баг, что прерывист, невидим в тестах и бесит при попытке воспроизвести.
Race Condition — состояние гонки: две руки, один предмет
Два человека тянутся за последним печеньем в один миг — оба видят, что оно там, оба хватают, и что будет дальше, зависит от точного тайминга в доли секунды.
Состояние гонки (race condition) — это когда результат зависит от точного тайминга конкурентных операций, трогающих общие данные. Два потока оба читают счётчик как 5, оба прибавляют один, оба пишут 6 — и единица тихо теряется, потому что верный ответ был 7. Никто не сделал ошибки в логике; шаги просто переплелись плохо. Это определяющий баг конкурентности.
- Что это
- Результат зависит от точного тайминга задач над общими данными.
- Пример
- Два потока читают счётчик 5, оба пишут 6 — а правильный ответ 7.
- Причина
- Общие и изменяемые данные плюс одновременный доступ.
- Подвох
- Ошибки в логике нет — шаги просто переплелись в плохом порядке.
- Связи
Растёт из Shared Mutable State. Лечится Lock, Message Passing, Immutability, Atomic Operation.
Non-determinism — ужас в том, что он прерывист
Дребезг в машине, что исчезает в тот миг, как ты привозишь её к механику, — реальный, прерывистый и невозможный показать по требованию.
Состояния гонки мерзки, потому что недетерминированы (non-deterministic): они зависят от тайминга, так что появляются раз в тысячу прогонов, никогда в твоих тестах и только под реальной нагрузкой в проде. Один и тот же ввод может сработать или упасть в зависимости от микроскопического расписания. Поэтому у багов конкурентности грозная репутация — их нельзя надёжно воспроизвести, и «у меня на машине работает» — ровно то, чего ждёшь, даже когда оно сломано.
- Что это
- Один и тот же ввод даёт разный результат в разных прогонах.
- Почему
- Результат зависит от микроскопического расписания задач.
- Пример
Баг появляется раз в тысячу прогонов — никогда в тестах, только под нагрузкой в проде.
- Подвох
«У меня на машине работает» — ровно то, чего ждёшь, даже когда сломано.
- Связи
Делает Race Condition такой опасной. Поэтому баги лучше сделать невозможными, а не ловить.
Shared Mutable State — общее изменяемое состояние
Столкновения случаются только над тем, что обоим позволено хватать и менять, — запри общий шкаф или дай каждому свой, и драки прекратятся.
Заметь точный источник: данные, что одновременно общие (shared — больше одной задачи может дотянуться) и изменяемые (mutable — их можно поменять). Убери любое свойство — и гонка исчезает: данные, что трогает лишь одна задача, безопасны, а данные, что никто не меняет, безопасно делить свободно. Это ключевая мысль, на которой строится остаток курса: каждое лекарство — это способ управлять, ограничить или избежать общего изменяемого состояния.
- Что это
- Данные, к которым тянутся несколько задач и которые можно менять.
- Идея
- Убери «общее» или «изменяемое» — и гонке негде жить.
- Пример
Общий счётчик или список, в который пишут два потока, — опасно. Данные одной задачи или неизменяемые — безопасно.
- Вывод
Каждое лекарство в курсе управляет, ограничивает или убирает это состояние.
- Связи
Источник Race Condition. Lock управляет им, Message Passing убирает «общее», Immutability убирает «изменяемое».
Состояние гонки — это две задачи, трогающие общие, изменяемые данные разом, с результатом, висящим на тайминге. Оно прерывисто, невидимо в тестах — и определяющий баг конкурентности.
Классическое лекарство от гонки — сделать так, чтобы лишь одна задача трогала общие данные за раз. Оно работает — но инструмент, которым ты это насаждаешь, несёт собственную знаменитую опасность.
Lock / Mutex — блокировка даёт одной задаче исключительный доступ
Единственный ключ от туалета на одного: кто держит — заходит; все остальные ждут своей очереди снаружи. Двух человек внутри разом — никогда.
Блокировка (lock, или mutex — от «mutual exclusion», взаимное исключение) гарантирует, что лишь одна задача входит в участок кода за раз. Прежде чем тронуть общие данные, задача должна захватить блокировку; другие, желающие её, ждут, пока она не освободится. Защищённый участок — это критическая секция (critical section). Сделанное верно, это превращает хаотичную свалку над общими данными в упорядоченную очередь по-одному, и состояние гонки исчезает.
- Проблема
- Две задачи меняют общие данные разом — гонка.
- Идея
Захватить блокировку → поработать в критической секции → отпустить. Остальные ждут.
- Пример
- Поток берёт блокировку, прибавляет единицу к счётчику и отпускает её.
- Цена
- Ожидание других задач; риск Deadlock и Contention.
- Когда брать
- Когда общее изменяемое состояние правда не избежать.
- Связи
Управляет Shared Mutable State. Часто дешевле — Atomic Operation или Message Passing.
Deadlock — взаимоблокировка: все ждут вечно
Два человека у узкой двери, каждый отказывается отступить, пока не отступит другой, — оба застряли, вежливо, навсегда.
Блокировки несут свой классический сбой: взаимоблокировку (deadlock). Задача A держит блокировку 1 и хочет блокировку 2; задача B держит блокировку 2 и хочет блокировку 1. Каждая ждёт, пока другая отпустит, и ни одна не отпускает — обе замёрзли навсегда. Это идёт от захвата нескольких блокировок в разном порядке. Частое лекарство — дисциплина: всегда захватывай блокировки в одном согласованном порядке, чтобы круговое ожидание (circular wait) не смогло сложиться.
- Что это
- Задачи ждут друг друга по кругу, и ни одна не может продолжить.
- Пример
- A держит блокировку 1 и хочет 2; B держит 2 и хочет 1.
- Причина
- Несколько блокировок захватываются в разном порядке.
- Лекарство
- Всегда брать блокировки в одном согласованном порядке.
- Связи
- Классическая опасность Lock / Mutex.
Lock Contention — блокировки верны, но дороги
Один ключ от туалета держит порядок, но если он постоянно всем нужен, образуется очередь, и весь офис замедляется до скорости той единственной двери.
Блокировки чинят гонки, но у них есть цена: пока одна задача держит блокировку, другие ждут, так что тяжёлая блокировка может стереть скорость, ради которой ты вообще брал конкурентность. Чрезмерная блокировка создаёт состязание (contention) — все стоят в очереди на одну блокировку. Так что держи блокировки как можно короче, защищай как можно меньше и предпочитай конструкции, что требуют меньше блокировок. Верная программа, сериализованная на одной блокировке, немногим лучше однопоточной.
- Что это
- Много задач стоят в очереди на одну и ту же блокировку.
- Пример
- Программа, сериализованная на одной блокировке, — почти однопоточная.
- Цена
- Стирает выигрыш от конкурентности: верно, но медленно.
- Лекарство
Держать блокировки коротко, защищать как можно меньше, выбирать конструкции с меньшим числом блокировок.
- Связи
- Вторая цена Lock / Mutex после Deadlock.
Блокировка даёт одной задаче за раз исключительный доступ, убивая гонку. Но блокировки могут уйти в deadlock, а чрезмерная блокировка сериализует всё — верно, но медленно.
Блокировки борются с симптомами общего изменяемого состояния. Более глубокий ход — спроектировать так, чтобы его было меньше с чем бороться, — и пара паттернов делает целые классы багов конкурентности попросту невозможными.
Message Passing — не дели память, передавай сообщения
Вместо многих поваров, дерущихся за один общий стол, каждый работает на своей станции и передаёт готовые тарелки по раздаче — двое никогда не тянутся за одним.
Мощная альтернатива общей памяти — передача сообщений (message passing): задачи не трогают одни данные; они шлют друг другу сообщения через очередь (queue). Лишь одна задача владеет данным куском, а другие просят изменений, посылая сообщение. Есть знаменитый лозунг: «не общайтесь, деля память; делите память, общаясь». Убери общий доступ — и убираешь гонку в корне, без всяких блокировок.
- Проблема
- Общая память требует блокировок и рождает гонки.
- Идея
Данными владеет одна задача; остальные просят изменений, посылая сообщения через очередь.
- Пример
Повара на своих станциях передают тарелки по раздаче, а не делят один стол.
- Цена
- Сообщения идут медленнее, чем прямой доступ к памяти.
- Когда брать
- Когда можно дать каждому куску данных одного владельца.
- Связи
Убирает «общее» из Shared Mutable State. Так общаются и Process.
Immutability — неизменяемые данные нельзя загонять
Вручить каждому свою ксерокопию вместо одного общего оригинала — они все могут читать разом, и никто не может черкнуть на том, что читает другой.
Если данные никогда не меняются после создания — если они неизменяемы (immutable), — то любое число задач может читать их в одно время с нулевым риском, потому что опасность была не в делении, а в делении и изменении. Вместо мутации значения на месте ты делаешь новое. Неизменяемость убирает одну половину условия «общее и изменяемое», а с ней — целую категорию багов. Поэтому функциональные стили так сильно на неё опираются.
- Проблема
- Общие данные кто-то меняет, пока другие читают.
- Идея
- Не менять значение на месте, а делать новое.
- Пример
- Каждому — своя ксерокопия; черкнуть в чужой нельзя.
- Цена
- Новые копии вместо правки на месте.
- Когда брать
- Данные, которые многие читают. Опора функционального стиля.
- Связи
- Убирает «изменяемое» из Shared Mutable State.
Atomic Operation — атомарные операции неделимы
Турникет, что считает каждого проходящего одним неделимым щелчком, — нет полусосчитанного мига, в который двое могут проскользнуть.
Некоторые операции атомарны (atomic): они случаются как один неделимый шаг, что нельзя прервать на полпути, так что две задачи не могут застать друг друга в середине обновления. Языки и базы дают атомарные счётчики, атомарные обмены и транзакции ровно для тех маленьких общих обновлений, что любят гонки. Когда тебе правда надо делить меняющееся значение, атомарная операция часто дешевле и безопаснее полной блокировки.
- Проблема
- Обновление из нескольких шагов может прерваться на середине.
- Идея
- Сделать обновление одним неделимым шагом.
- Пример
- Атомарный счётчик, атомарный обмен, транзакция в базе.
- Цена
- Подходит только для маленьких обновлений, не для большой логики.
- Когда брать
- Общее меняющееся значение, вроде счётчика.
- Связи
- Часто дешевле и безопаснее Lock / Mutex; чинит Race Condition.
Лучший баг конкурентности — сделанный невозможным: передавай сообщения вместо деления, делай данные неизменяемыми или используй атомарные операции — и гонке негде жить.
Конкурентность — это мощь, что стоит добавлять нарочно, а не рефлексом. Навык — тянуться к ней, только когда задача требует, и выбирать простейшую модель, что делает дело.
Подбери инструмент под узкое место
Ты не нанимаешь больше грузчиков на работу, что вся ожидание, и не берёшь лучшего диспетчера на работу, что вся тяжёлый подъём, — ты подбираешь помощь туда, куда реально уходит время.
Прежде чем добавлять конкурентность, знай своё узкое место (bottleneck). Если работа I/O-bound — в основном ожидание — async на одном потоке часто решает это просто. Если она CPU-bound — в основном вычисление — нужен настоящий параллелизм по ядрам. Если ни то ни другое — быстро и просто — добавление конкурентности просто покупает баги без выигрыша. Самая частая ошибка — тянуться к потокам, когда обычный последовательный код был уже достаточно быстр.
Держи общую поверхность крошечной
Мастерская, где каждый в основном работает один, с одной маленькой, ясно помеченной общей полкой инструментов, — чем меньше общего, тем меньше может пойти не так.
Раз каждый баг конкурентности идёт от общего изменяемого состояния, главный ход — минимизировать деление. Держи бо́льшую часть данных во владении одной задачи, делай что можешь неизменяемым и ограничь по-настоящему общие, меняющиеся части маленькой, тщательно охраняемой поверхностью. Конструкцию с крошечной общей поверхностью можно реально осмыслить; ту, где всё общее и изменяемое, — это дом с привидениями. Простота тут не опциональна — это вся защита.
Понятия связаны между собой
Генеалогическое древо: у всех бед одна прародительница, а у всех лекарств — одна цель.
Почти всё в курсе крутится вокруг одной точки. Потоки делят память → появляется общее изменяемое состояние → оно рождает гонки. Четыре лекарства — lock, message passing, immutability, atomic — управляют им или убирают его. У блокировок свои беды: deadlock и contention. Отдельно стоит выбор под нагрузку: ожидание → async + event loop, вычисления → parallelism.
- Какое узкое место — 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атомарная операция
Обновление одним неделимым шагом.