Конкуренция в Go
Теория: Введение в конкуренцию
Программа, которая выполняет только одну задачу, движется по коду, как человек, читающий книгу по одной строке. Каждая операция ждет завершения предыдущей, и выполнение развивается по прямой линии. Такая схема работает, пока не появляется необходимость реагировать на внешние события. Сеть отвечает с задержкой, диски читают данные медленнее процессора, а фоновые операции живут своей жизнью. Ожидание превращается в паузу, а пауза блокирует все приложение.
Конкуренция решает эту проблему. Она позволяет программе вести несколько линий работы так, чтобы одна задержка не парализовала систему. Конкурентная структура не требует наличия нескольких ядер. Планировщик просто переключает активные участки выполнения, создавая ощущение одновременной работы.
Параллелизм относится уже к устройству процессора. Когда доступно несколько ядер, разные части программы могут исполняться физически одновременно. Конкуренция задает способ организации работы, а параллелизм превращает эту организацию в реальное ускорение. Такие два уровня — архитектурный и аппаратный — усиливают друг друга и создают гибкость, необходимую сложным системам.
Go строит модель конкуренции в сам язык. Он не заставляет вручную управлять потоками и бороться за память. Каждый независимый участок кода оформляется как горутина, а взаимодействие задается каналами. Эта комбинация превращает конкуренцию в естественную часть программы и избавляет разработчика от лишней сложности.
Параллелизм и конкуренция
Разница между параллелизмом и конкуренцией становится заметной, когда две задачи могут выполняться независимо. Параллелизм использует несколько ядер и действительно выполняет код одновременно. Ниже простой пример вычислений, которые раскладываются планировщиком по ядрам.
runtime.GOMAXPROCS(4) разрешает создать четыре потока ОС, которые смогут использовать четыре ядра
Каждая горутина считает свой квадрат и не зависит от других. При достаточном числе ядер работа идет одновременно.
Конкуренция действует иначе. Она не ускоряет вычисления за счет железа, но позволяет системе не простаивать. Если один участок ожидает сетевой ответ, другой может продолжать обработку. Так создается живая структура, способная вести несколько линий работы на одном ядре.
Планировщик переключает горутины. Пока первая горутина спит на таймере, вторая продолжает выводить данные. Это конкуренция: структура, в которой задачи живут одновременно, даже если исполняются последовательно.
Потоки и процессы
Традиционная многозадачность опирается на процессы. Процесс получает собственное адресное пространство, набор файловых дескрипторов и окружение. Такая изоляция защищает систему, но стоит дорого, потому что создание процесса требует значительных ресурсов.
Чтобы один процесс мог вести несколько линий работы, внутри него создаются потоки. Потоки разделяют память и ресурсы процесса, но имеют собственные стеки и независимые указатели инструкций. Это делает их легкими и быстрыми, но добавляет риски параллелизма.
Если несколько потоков обращаются к общей памяти без синхронизации, возникает гонка данных: результат зависит от того, кто успел первым.
Если же два потока пытаются захватить блокировки в разном порядке — например, один берет мьютекс A, потом B, а другой наоборот — легко получить взаимоблокировку (deadlock): оба потока ждут друг друга и ни один не может продолжить работу.
Типичная гонка проявляется, когда две единицы исполнения меняют одну переменную.
Синхронизация устраняет проблему, но вводит жесткие границы. Каждый вход в критическую секцию становится узким местом.
Взаимоблокировки появляются, когда два замка берутся в разном порядке. Один поток ждет второй замок, второй — первый, и оба становятся в тупик.
Go решает эти сложности иначе. Горутины работают поверх небольшого набора системных потоков, а переключение между ними контролируется рантаймом. Создание горутины занимает микросекунды, и система может поддерживать их тысячи и миллионы. Из-за этого конкуренция становится легкой и дешевой.
Модель CSP в Go
Модель CSP — Communicating Sequential Processes (не путать с Content Security Policy в веб-браузерах) описывает систему как набор независимых исполнителей, которые не делят память, а взаимодействуют только через передачу сообщений.
В Go эта идея реализована напрямую: горутины — это независимые последовательные процессы, а каналы — средство общения между ними. Каждая горутина выполняет свой линейный код, а канал задает протокол взаимодействия: кто отправляет, кто принимает и в каком порядке.
В небуферизованном канале отправка блокирует горутину, пока другая сторона не прочитает значение.
Буфер позволяет отправителю опережать получателя, но только до определенного размера очереди.
CSP показывает свою силу, когда исполнители объединяются в конвейер. Один этап принимает данные, преобразует их и передает дальше.
Каждый этап соблюдает свой контракт: чтение входа и корректное закрытие выхода.
Почему Go использует горутины и каналы
Go был создан в тот момент, когда классическая многопоточность стала затруднять разработку крупных систем. Потоки ОС оказались слишком тяжелыми, синхронизация — слишком хрупкой, а отладка — слишком дорогой. Горутины решили проблему масштабирования: они дешевы, создаются мгновенно и не требуют участия ядра.
Каналы решили проблему взаимодействия. Вместо совместного доступа к памяти система использует передачу значений. Сама операция передачи синхронизирует горутины и задает порядок событий. Благодаря этому код становится предсказуемым и лучше отражает структуру работы.
Такой подход упрощает проектирование сложных сервисов. Каждая горутина видит только свою задачу и правила общения. Каналы обеспечивают контроль над потоком данных. Планировщик распределяет работу по потокам ОС. В результате возникает модель, где сложные формы взаимодействия описываются простыми конструкциями языка, а масштабирование происходит без усложнения архитектуры.


