Базовый CS с нуля
Время и конкурентность: воспроизведи и почини race
Читать про race conditions — не то же самое, что смотреть, как один из них съедает твои данные. Построй наименьшую программу, проявляющую race — много thread’ов инкрементируют один общий counter — заставь неверное число появиться своими глазами, затем почини его lock и докажи фикс счётчиками, а не верой.
Преврати идеи юнита в то, что можно запустить: структурируй работу в thread’ы (concurrency), дай им трогать общее состояние параллельно, воспроизведи lost-update race, объясни его как переплетение read-add-write, затем добавь lock и проверь, что счёт корректен и стабилен на многих запусках.
Напиши крошечную многопоточную программу, инкрементирующую один общий counter из многих thread'ов, воспроизведи lost-update race condition, затем почини его lock — доказывая баг и фикс измеренными счётчиками до/после, а не оценками.
- Таблица до/после: 10 запусков без synchronization (с разными, заниженными счётчиками) рядом с 10 запусками с lock (все равны точному ожидаемому итогу).
- Ожидаемый итог указан явно (thread'ы x итерации на thread), чтобы разрыв в запусках без synchronization был однозначным.
- Абзац-объяснение, ПОЧЕМУ версия без synchronization теряет обновления, в терминах переплетения read-add-write — не просто 'thread'ы небезопасны'.
- Однопредложное утверждение, что изменил lock: он делает инкремент неделимым, поэтому только один thread находится в защищённой секции за раз.
- Измерь wall-clock-время для обеих версий и приведи рядом со счётчиками. Объясни, почему версия с lock может быть медленнее: защищённая секция теперь выполняется последовательно, поэтому thread'ы стоят в очереди за lock вместо параллельного выполнения.
- Сократи contention: пусть каждый thread держит приватный локальный счёт и добавляет его к общему counter один раз в конце под lock. Покажи, что результат всё ещё корректен, но быстрее, и объясни почему (гораздо меньше времени держится lock).
- Замени lock на примитив атомарного инкремента, если язык его предлагает (например, atomic-типы, Go's sync/atomic, Java's AtomicInteger). Покажи, что он корректен, и объясни, как он делает read-add-write одним неделимым шагом без явного lock.
- Добавь вторую общую переменную и намеренно неверный фикс (два отдельных lock'а или слишком ранний release), чтобы увидеть, как race возвращается, демонстрируя, что lock защищает лишь то, что реально охраняет.
Это цикл, который ты будешь запускать всякий раз, когда конкурентность встречает общее состояние: структурируй работу в thread’ы, дай им трогать одни данные и смотри, как race condition выдаёт неверный, меняющийся от запуска к запуску ответ, потому что counter = counter + 1 на деле read-add-write, и thread’ы переплетаются. Затем добавь lock, чтобы сделать общую секцию неделимой, и докажи счётчиками, что результат теперь ровно верный каждый раз. Сделав это раз на игрушечном counter, ты превращаешь production-версию — где общее состояние это баланс, остаток на складе или сессия — во что-то, о чём можно рассуждать, а не бояться.