Содержание

С вас вопросы, с нас ответы. Часть 3

Отвечаем на популярные вопросы по конкурентному программированию на Go
Консультирует:
Владимир Балун, Ex-Team Lead в Яндекс

1. Что такое конкурентность в Go и чем она отличается от параллелизма?

Конкурентность (concurrency) — это подход к организации программы, при котором несколько задач могут выполняться одновременно по логике, но не обязательно физически в один и тот же момент времени.

Параллелизм (parallelism) — это когда несколько задач реально выполняются одновременно на разных ядрах процессора.

На конференции Google один из создателей Go — Rob Pike — сформулировал это так: "Concurrency is not parallelism. Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once".

Представь, что ты готовишь ужин: ты поставил вариться пасту, потом проверяешь соус и режешь салат и затем возвращаешься к пасте. Это concurrency — ты переключаешься между задачами. А теперь представь, что у тебя есть помощник: ты режешь салат, а помощник варит пасту. Это уже parallelism — задачи реально выполняются одновременно.

В Go есть легковесные потоки — горутины. Ты запускаешь две задачи, и Go runtime сам решает, как их планировать. Даже если у тебя одно ядро, код выше всё равно конкурентный. Go будет просто быстро переключаться между задачами. Но если у тебя: несколько ядер и GOMAXPROCS > 1. Тогда горутины могут реально исполняться одновременно на разных ядрах.

То есть в Go: concurrency — есть всегда, а parallelism — только если позволяет железо.

Рассмотрим пример — ты пишешь HTTP API и каждый входящий запрос запускается в отдельной горутине. Это concurrency? Да, потому что сервер может обрабатывать тысячи запросов одновременно логически. Это parallelism? Только если есть несколько ядер.

Иногда еще говорят «конкурентный параллелизм» — по сути это: программа спроектирована конкурентно и при этом реально выполняется параллельно.

2. Как работают горутины в Go и чем они отличаются от потоков ОС?

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

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

Главное отличие заключается в том, как происходит переключение между задачами — так называемый context switching. Когда операционная система переключается между потоками, ей нужно сохранить состояние одного потока, перейти в режим ядра (kernel mode), восстановить состояние другого потока и только после этого продолжить выполнение. Это относительно медленная операция, особенно если потоков много. Чем чаще происходят такие переключения, тем больше накладных расходов. В Go всё устроено иначе. Планировщик горутин работает в user-space, то есть в пространстве пользователя. Переключение между горутинами не требует постоянных переходов в ядро. Рантайм Go сам управляет тем, какие горутины выполняются на каких потоках ОС, и делает это значительно дешевле. Такой легковесный context switching позволяет запускать десятки и сотни тысяч конкурентных задач без серьезной деградации производительности.

Второе важное отличие — размер стека. Поток операционной системы обычно стартует со стеком размером от 1 до 8 мегабайт. Даже если поток почти ничего не делает, память уже зарезервирована. Если создать 10 000 потоков, объём потребляемой памяти будет большим. Горутина стартует с 2 килобайтами стека. Более того, стек динамически растёт и уменьшается по мере необходимости. Это делает горутины крайне эффективными с точки зрения памяти. Благодаря этому Go идеально подходит для сервисов, которые обрабатывают тысячи одновременных соединений — например, веб-серверов или прокси серверов.

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

Именно сочетание трех факторов делает горутины особенными: маленький стартовый стек, дешевое переключение без постоянных переходов в kernel mode и собственный планировщик в user-space. В результате Go позволяет строить масштабируемые системы с высокой производительностью без сложной ручной работы с потоками ОС. Поэтому, когда говорят, что горутины — это легковесные потоки выполнения, имеют в виду не просто «быстро запускаются», а архитектурное отличие: они дешевле по памяти, дешевле по переключению контекста и лучше масштабируются. Это одна из ключевых причин, почему concurrency в Go считается одной из самых удобных и эффективных моделей в современной разработке.

3. Что такое каналы в Go и зачем они нужны, когда есть мьютексы и другие примитивы синхронизации?

Каналы в Go — это механизм передачи данных между горутинами. Вместо того чтобы несколько потоков делили общую память и защищали её мьютексами, горутины могут обмениваться сообщениями через каналы. В философии Go это формулируется так: «не делите память, делитесь сообщениями». Это принципиально отличает модель каналов от классического подхода с блокировками.

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

В программировании разница становится еще заметнее. Если несколько горутин изменяют одну и ту же структуру данных, приходится использовать sync.Mutex, следить за блокировками, избегать data race и дедлоков. Это усложняет код и увеличивает риск ошибок. Канал же позволяет построить систему, где одна горутина владеет состоянием, а остальные отправляют ей команды через канал. Таким образом, состояние не разделяется — оно инкапсулировано.
Почему это особенно эффективно в Go? Потому что горутины — это легковесные потоки выполнения. Они запускаются с очень маленьким стеком (примерно 2 килобайта) и динамически его увеличивают при необходимости. В отличие от потоков ОС, которым обычно выделяется 1–8 мегабайт стека сразу, goroutines позволяют создавать десятки и сотни тысяч конкурентных задач без огромного расхода памяти.

Но нужны ли тогда мьютексы, если есть каналы? Да, если вам нужно просто защитить счетчик или другую разделяемую структуру данных — мьютекс может быть проще. Но если вы строите конвейер обработки данных, систему воркеров или event-driven архитектуру, каналы позволяют выразить это естественно и безопасно. Например, при обработке очереди задач можно создать пул воркеров, которые читают задания из канала. Продюсер отправляет задачи в канал, воркеры их обрабатывают. Нет общей структуры данных, которую нужно блокировать и нет сложной схемы синхронизации.

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

задай вопрос, а мы ответим

другие статьи