Go. Concurrency
Модуль 1
Модуль 1
Что такое Программа?
Представьте, что программа — это кулинарный рецепт.

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

Аналогия:
Рецепт борща в кулинарной книге. Он просто лежит на странице. В нем есть шаги: «1. Нарезать свеклу. 2. Поставить кастрюлю на огонь. 3. Залить водой…». Сам по себе рецепт ничего не готовит. Он просто описывает процесс.

В компьютере:
Это ваш скомпилированный файл (например, myapp. exe в Windows или просто myapp в Linux). Он лежит на диске и ждет, пока его кто-то не «запустит».
Итог: Программа — это статичный, неактивный набор инструкций.
Что такое Процесс?
Теперь представьте, что процесс — это сам процесс готовки по этому рецепту.

Определение:
Процесс — это экземпляр выполняющейся программы. Когда вы запускаете программу, операционная система создает для нее процесс. Процесс — это активный объект.

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

Ключевые свойства процесса:

Изолированность: Если на вашей кухне (в вашем процессе) готовится борщ, это никак не влияет на соседнюю кухню (другой процесс), где, возможно, пекут пирог. У каждого процесса своя, отдельная «кухня».
Свои ресурсы: Операционная система выделяет каждому процессу свою собственную память (как рабочую поверхность на кухне), свои файлы (ингредиенты) и другие системные ресурсы. Процессы не могут напрямую использовать память друг друга.

Итог: Процесс — это программа в действии, у которой есть свои выделенные и защищенные ресурсы.
Что такое Поток (Thread)?
А теперь самое интересное. Поток — это повар на кухне.

Определение:
Поток (или нить) — это наименьшая единица выполнения кода внутри процесса. Один процесс может иметь один или несколько потоков.

Аналогия:
Вы готовите борщ в одиночку. Вы — это один поток. Вы последовательно выполняете задачи: нарезали овощи, поставили вариться, помешали, пошли нарезать зелень. Вы все делаете по очереди. Если суп нужно варить 30 минут, вы просто ждете, ничего не делая. Это однопоточная программа.
А теперь представьте, что вы наняли помощника. Теперь у вас на кухне два потока (два повара). Один повар (поток 1) следит за супом: помешивает, регулирует огонь. Второй повар (поток 2) в это же время готовит салат: режет овощи, заправляет соусом.
Оба повара работают в рамках одной кухни (одного процесса). Они пользуются общей плитой, общими ножами и общими продуктами (разделяют общую память и ресурсы). Это делает работу намного быстрее! Но есть и проблема: если обоим поварам одновременно нужна одна и та же разделочная доска, они могут помешать друг другу. Это называется «состояние гонки» (race condition).

Ключевые свойства потока:

Легковесность: Создать нового потока («нанять помощника») гораздо проще и дешевле, чем создавать целый новый процесс («строить новую кухню»).
Общие ресурсы: Все потоки внутри одного процесса имеют доступ к общей памяти этого процесса. Это позволяет им легко обмениваться данными, но и создает риски.

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

Связываем всё вместе перед переходом к Go
Программа (Рецепт) — это main. go, просто текст с инструкциями. Процесс (Кухня) создается, когда вы выполняете go run main.go. Операционная система запускает ваш код как процесс и выделяет ему память и ресурсы. Поток (Повар) — это изначально один главный поток в этом процессе, который начинает выполнять код из функции main (). Это как один повар на кухне.

Проблема, которую решает Go:

Что если нашему «повару» нужно выполнить долгую задачу, например, скачать файл из интернета? Весь «процесс готовки» (наша программа) встанет и будет ждать. Неэффективно!

Решение Go:

Вместо того чтобы нанимать дорогих и «тяжелых» помощников (обычные потоки ОС), Go предлагает нанимать целую армию дешевых и быстрых горутин. Они работают в рамках того же процесса (на той же кухне), но управляются не операционной системой, а «шеф-поваром» (средой выполнения Go), который очень эффективно распределяет между ними задачи.
Такой переход от «повара» к «армии помощников» — это и есть переход от однопоточной программы к конкурентной программе на Go.
Однопоточная программа: последовательное выполнение задач
Чтобы понять проблему однопоточной программы, рассмотрим пример с почтовым отделением. Представьте, что вы — один-единственный работник на почте. У вас есть список дел на сегодня: принять посылку, отправить большую посылку в другой город и выдать посылку другому клиенту.
Быстрые задачи: всё идёт по плану
Вы приходите на работу и начинаете выполнять задачи последовательно. Сначала принимаете посылку от клиента — это занимает пару минут. Затем берётесь за отправку большой посылки: заполняете документы, звоните курьеру. Это занимает 30 минут. Когда курьер уезжает, вы подходите к следующему клиенту, чтобы выдать ему посылку — ещё несколько минут.

В этом случае всё прошло гладко, потому что задачи были последовательными и не слишком долгими. Программа работает предсказуемо.
Появляется долгая задача: программа «зависает»
Теперь представим, что отправка посылки — это не просто заполнение бумаг, а запрос в интернет. Вы отправляете запрос и… должны ждать ответа. Сервер может отвечать 5 секунд, а может и 30 секунд.

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

Этот пример показывает основную проблему однопоточных программ: любая долгая задача блокирует выполнение всех остальных задач.
Вывод "на пальцах"
Программа — это вы, работник.

Поток (Thread) — это ваши руки и голова, которые могут выполнять только одну задачу в конкретный момент времени.

Долгая задача (запрос в сеть) — это ожидание ответа от сервера.

Пока вы (программа) ждете, ваши руки (поток) заняты ожиданием и не могут взяться за другую работу. Вся программа «замораживается» на этом месте.

Именно эту проблему и решает многопоточность (конкурентность) в Go. Она позволяет «нанять помощников» (создать новые горутины), чтобы пока один «работник» ждет ответа от сервера, другой мог в это время обслуживать клиентов.

Аналогия:
Повар на кухне, который готовит суп, и пока он варится, просто стоит и смотрит на кастрюлю, вместо того чтобы нарезать салат.
Современные процессоры имеют несколько ядер. Как заставить их работать на нас?
Это ключевой момент, который показывает всю мощь современных компьютеров. Давайте продолжим нашу аналогию с почтой.
Аналогия: Расширение почтового отделения
Раньше у нас в почтовом отделении был один работник (одно ядро процессора). Он мог делать только одно дело за раз. Теперь представьте, что бизнес пошел в гору, и руководство наняло еще работников. Теперь в отделении четыре работника (четырехъядерный процессор).

Проблема: Если они не будут скоординированы, это будет хаос. Один начнет принимать посылку, другой в это же время попытается использовать тот же весы, третий будет стоять без дела. Нам нужен менеджер, который будет распределять задачи.
Менеджер: Среда выполнения Go (Go Runtime)
В мире Go роль такого «менеджера» выполняет среда выполнения Go (Go Runtime).

Задачи (что делать?) — это наши горутины. (Принять посылку, отправить запрос, выдать посылку).
Работники (кто делает?) — это ядра процессора.
Менеджер (кто управляет?) — это Go Runtime.
Когда вы запускаете свою Go-программу, «менеджер» (Runtime) готов к работе. Он видит, сколько у него есть «работников» (ядер) и ждет от вас «задачи» (горутин).
Как заставить их работать на нас?
Go позволяет вам использовать этих «работников» двумя способами, которые важно различать.
Конкурентность (Concurrency) — Один работник, который multitasking
Даже если у нас всего один работник (одно ядро), Go может сделать программу отзывчивой.

Как? Наш работник стал очень быстрым и умным. Он берет задачу «отправить запрос в сеть», запускает ее (отправляет) и, пока ждет ответа, не стоит без дела, а быстро переключается на другую задачу: «выдать посылку клиенту». Он постоянно переключается между задачами, которые находятся в состоянии ожидания.

Результат: Программа не «зависает», потому что работник всегда занят чем-то полезным. Он просто очень быстро жонглирует задачами.
Параллелизм (Parallelism) — Несколько работников работают одновременно
А теперь самое интересное. У нас четыре работника (четыре ядра). «Менеджер» (Go Runtime) видит это и поступает умно. Он берет задачу «отправить долгий запрос в сеть» и отдает её Работнику № 1. Работник № 1 отправляет запрос и теперь ждет ответа.

В этот же самый момент менеджер видит, что Работник № 2, № 3 и № 4 свободны. Он отдает им другие задачи: «Принять посылку», «Выдать посылку», «Подготовить документы».

Результат: Задачи выполняются физически одновременно, в одно и то же время, на разных ядрах. Программа работает в 4 раза быстрее (в идеале), чем на одном ядре.
Как это выглядит в коде Go?
Вам, как программисту, не нужно думать, какой работник на каком ядре будет работать. Вы просто говорите «менеджеру», какие задачи нужно выполнить, с помощью ключевого слова go.
func main() {
    // Мы говорим "менеджеру": вот тебе задача, запусти ее!
    // Не жди ее завершения, давай дальше.
    go doLongNetworkRequest() // Эту задачу "менеджер" отдаст, например, Работнику №1

    // А мы пока делаем что-то еще. Эту задачу "менеджер" может отдать Работнику №2
    doAnotherTask()
}
Волшебство Go: GOMAXPROCS
Раньше (до версии Go 1.5) «менеджер» по умолчанию использовал только одного работника (одно ядро), даже если их было больше. Чтобы заставить его использовать всех, нужно было вручную настраивать переменную runtime. GOMAXPROCS ().

Но сейчас (начиная с Go 1.5) это больше не нужно! По умолчанию Go Runtime использует все доступные ядра процессора.

Итог «на пальцах»:

Вы пишете код, разбивая его на логические задачи (функции). Вы запускаете эти задачи как горутины (go myTask ()). Go Runtime (менеджер) автоматически распределяет все эти горутины по всем доступным ядрам процессора (работникам). Ваша программа становится быстрой и отзывчивой, потому что задачи выполняются параллельно на разных ядрах, а вы управляете этим с помощью очень простого синтаксиса.
Ввести понятие конкурентности (Concurrency)
и параллелизма (Parallelism). Это ключевой момент!
Вы абсолютно правы! Это самый важный и самый часто путаемый момент. Давайте разложим его по полочкам, используя самую известную и удачную аналогию от Роба Пайка (одного из создателей Go).

Представьте, что вы пришли в кофейню.
Конкурентность (Concurrency) — Один бариста, но он multitasking-мастер
Конкурентность — это Deal with multiple things at once. (Иметь дело со многими вещами одновременно).

В нашей кофейне работает один-единственный бариста, но он очень опытный. К нему подходят три клиента с заказами: капучино, американо и латте.

Что делает наш бариста? Он берет заказ на капучино, ставит эспрессо вариться. Пока варится эспрессо (30 секунд), он не стоит и не ждет! Он быстро переключается на следующий заказ: берет чашку для американо. Эспрессо для капучино готово. Он переключается обратно, наливает его в чашку и начинает взбивать молоко. Пока взбивается молоко, он снова переключается на американо, заливает его горячей водой и отдает клиенту. Он заканчивает капучино и отдает его. Затем берется за латте.

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

В программировании: Это ваша программа на одном ядре процессора. Она может запустить долгую задачу (например, запрос в сеть), и пока она ждет ответа, переключиться на выполнение другой задачи (например, обновить интерфейс). Программа не «зависает», потому что она конкурентна.
Параллелизм (Parallelism) — Два бариста работают одновременно
Параллелизм — это Do multiple things at once. (Делать много вещей одновременно).
Теперь в кофейне стало очень шумно, и наняли второго бариста. К ним подходят те же три клиента с заказами: капучино, американо и латте.

Что происходит теперь? Бариста № 1 берет заказ на капучино и начинает его делать. В этот же самый момент Бариста № 2 берет заказ на американо и тоже начинает его делать.
Они работают физически одновременно, в одно и то же время. Когда один из них освободится, он возьмет третий заказ.

Ключевой момент: Задачи выполняются действительно в одно и то же время, потому что для этого есть несколько исполнителей (несколько ядер процессора).

В программировании: Это ваша программа на многоядерном процессоре. Одна задача (поток/горутина) выполняется на ядре № 1, а другая задача — на ядре № 2. Они не мешают друг другу и выполняются параллельно, что ускоряет общую работу.
Сводная таблица: Главное отличие

Критерий

Конкурентность (Concurrency)

Параллелизм (Parallelism)

Суть

Структура. Способ организации и написания кода.

Выполнение. Способ запуска кода на железе.

Аналогия

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

Два бариста, которые делают разные задачи одновременно.

Железо

Может быть реализована на одном ядре.

Требует нескольких ядер процессора.

Цель

Сделать программу отзывчивой, правильно структурировать логику.

Ускорить вычисления, повысить производительность.

Как это связано с Go?
А вот здесь и кроется магия Go.

Go — это язык, созданный для написания конкурентных программ. Он дает вам простые инструменты (go, channels), чтобы легко структурировать ваш код как набор независимых, взаимодействующих задач (горутин). Вы думаете о баристе, который жонглирует заказами.
А Go Runtime (менеджер, о котором мы говорили) автоматически берет вашу конкурентную программу и, если возможно, запускает ее параллельно на всех доступных ядрах процессора. Он нанимает второго, третьего и четвертого бариста за вас!

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

Аналогия (классическая от Роба Пайка):
Конкурентность — это Deal with multiple things at once (иметь дело со многими задачами одновременно). Повар одновременно варит суп, жарит котлеты и следит за салатом. Он быстро переключается между задачами.

Параллелизм — это Do multiple things at once (делать много задач одновременно). Два повара на кухне, один варит суп, другой жарит котлеты. Они работают физически одновременно.

Вывод: Go — это язык, созданный для написания конкурентных программ, которые могут эффективно выполняться параллельно на многоядерных процессорах.

Философия Go
Представить главную мантру конкурентности в Go:
«Не общайтесь, разделяя память; вместо этого, разделяйте память, общаясь.»

Простыми словами: лучше чтобы у каждой своей маленькой задачи (горутины) были свои данные, а они обменивались сообщениями, чем чтобы все лезли в один общий ящик (память) и мешали друг другу.
Зачем Go свой планировщик?
Мы уже знаем, что у нас есть «дорогие» потоки операционной системы (OS Threads). Управлять ими напрямую — дорого: создание потока требует много ресурсов и времени, а переключение между потоками — это «тяжелая» операция для ОС, которая должна сохранить контекст одного потока и загрузить контекст другого.

Go говорит: «Зачем просить ОС переключать наших дорогих рабочих, если мы можем делать это сами, быстро и умно?». Для этого и существует собственный планировщик в среде выполнения Go (Go Runtime).

Модель G-M-P
G-M-P — это три ключевых компонента, с которыми работает планировщик Go.
G (Goroutine) — Задача
Это уже знакомая нам горутина. Аналогия: это не сам рабочий, а карточка с заданием. Создать такую карточку очень дешево и быстро. На ней написано, какой код нужно выполнить.
Философия Go
Представить главную мантру конкурентности в Go:
«Не общайтесь, разделяя память; вместо этого, разделяйте память, общаясь.»

Простыми словами: лучше чтобы у каждой своей маленькой задачи (горутины) были свои данные, а они обменивались сообщениями, чем чтобы все лезли в один общий ящик (память) и мешали друг другу.
M (Machine/Thread) — Рабочий
Это настоящий поток операционной системы (OS Thread). Это «мощный», но «дорогой» рабочий, который выполняет код. Аналогия: это и есть тот самый строитель (OS Thread), который физически выполняет работу. У Go есть пул таких рабочих.
P (Processor/Context) — Рабочее место Менеджера
Это самая важная и изобретательная часть модели. P — это не процессор CPU, а контекст или «рабочее место», которое связывает G и M. У каждого P есть своя локальная очередь runnable-горутин.

Аналогия: Это рабочий стол менеджера. На этом столе есть локальный ящик для входящих заданий (runq), все необходимые инструменты (память, GC и т. д.), а за этим столом сидит один рабочий (M).

По умолчанию Go создает P столько, сколько у вас логических ядер CPU. Если у вас 4-ядерный процессор, у вас будет 4 «рабочих стола» (P).

Как это все работает вместе?
Представьте себе офис с 4 рабочими столами (P) и 4 рабочими (M).
Обычная работа
Вы пишете go myTask (). Планировщик Go берет эту «карточку с заданием» (G) и кладет её в ящик на один из свободных столов (P). Рабочий (M), сидящий за этим столом, видит новую карточку, берет её и начинает выполнять.
Системный вызов (Блокировка)
Рабочий (M) взял карточку «Скачать файл из сети». Он отправляет запрос в сеть и… засыпает, потому что ждет ответа от ОС. В этот момент стол (P) остается без рабочего!

Магия Go: Стол (P) не ждет. Он «увольняет» заснувшего рабочего (M) и немедленно идет в «кадровое агентство» (Go Runtime) и нанимает нового, свежего рабочего (M). Этот новый рабочий садится за стол и берет следующую карточку из ящика. Работа не останавливается!
Work Stealing (Воровство работы)
Рабочий за столом № 1 закончил все задания в своем ящике. Он сидит без дела. Но он видит, что у рабочего за столом № 2 ящик переполнен.

Магия Go: Рабочий № 1 подходит к столу № 2 и крадет половину карточек из его ящика, чтобы у себя была работа. Это обеспечивает идеальную балансировку нагрузки и не дает одним рабочим простаивать, пока другие завалены.
Когда блокирующий рабочий просыпается
Тот самый рабочий, который ждал ответа из сети, наконец-то получает его. Он просыпается. Что делать? Его стол (P) уже занят другим рабочим. Он идет в «кадровое агентство» и говорит: «Я свободен, дайте мне любую работу». Его ставят в список «спящих» рабочих, которые могут быть вызваны, если где-то снова потребуется замена.
Итог: Почему эта модель так хороша?
Масштабируемость: Вы можете создавать сотни тысяч горутин (G), потому что они — это просто «карточки». Планировщик эффективно распределяет их по ограниченному числу рабочих (M) и столов (P).

Производительность: Переключение между горутинами (G) происходит в пространстве пользователя, а не в ядре ОС. Это в разы быстрее, чем переключение между потоками (M).

Эффективное использование ядер: Модель автоматически использует все ваши ядра CPU (по одному P на ядро) и балансирует нагрузку между ними с помощью work stealing.

Устойчивость к блокировкам: Планировщик умно отделяет рабочих от столов, когда те блокируются на системных вызовах, сохраняя общую производительность.
Как это влияет на вас как на программиста?
Почти никак! Вы просто пишете go func (), а планировщик G-M-P делает всю эту сложную работу за вас. Но понимание этой модели помогает осознать, почему горутины «дешевые», понимать, как GOMAXPROCS может влиять на производительность (хотя сейчас defaults почти всегда хороши), и писать код, который лучше работает с планировщиком (например, избегать долгих циклов без переключений, которые могут «заблокировать» рабочий M на долго).

Это элегантное решение, которое является одной из главных причин популярности Go для построения высоконагруженных систем.