1. Модель конкурентности Go: эволюционный переход от потоков ОС к легковесным горутинам
Модель конкурентности Go: эволюционный переход от потоков ОС к легковесным горутинам
Почему программа, создающая 100 000 потоков в Java или C++, скорее всего, приведет к падению системы по Out of Memory, в то время как аналогичный код на Go даже не заставит кулер вашего ноутбука вращаться быстрее? Ответ кроется не в магии компилятора, а в фундаментальном переосмыслении того, как программный код должен взаимодействовать с вычислительными мощностями процессора.
Проблема 1:0. Тяжеловесность потоков ОС
Чтобы понять гениальность модели Go, нужно вспомнить, как работают классические потоки операционной системы (Threads). В традиционных языках программирования один поток приложения напрямую отображается на один поток ОС (модель ).
Это создает две критические проблемы:
> Конкурентность — это не параллелизм. Конкурентность — это способ структурирования программы так, чтобы она могла справляться с множеством задач одновременно. Параллелизм — это когда эти задачи физически выполняются в один и тот же момент на разных ядрах процессора. > > Роб Пайк, "Concurrency is not Parallelism"
Решение Go: Модель M:N
Go отказывается от жесткой привязки потока приложения к потоку ОС. Вместо этого используется модель , где горутин мультиплексируются на потоков операционной системы.
| Параметр | Поток ОС (Thread) | Горутина (Goroutine) | | :--- | :--- | :--- | | Размер стека | Фиксированный (~2 МБ) | Динамический (от 2 КБ) | | Создание/Уничтожение | Дорого (вызов ядра ОС) | Дешево (аллокация в куче) | | Переключение | Медленное (Hardware/OS) | Быстрое (Runtime Go) | | Управление | Планировщик ОС | Планировщик Go (Runtime) |
Горутина начинается всего с 2 КБ памяти. Если стек заполняется, среда выполнения Go (Runtime) выделяет новый, более просторный сегмент и копирует туда данные. Это позволяет запускать миллионы горутин на обычном сервере.
Механизм переключения: Кооперативность vs Вытеснение
В обычных ОС используется вытесняющая многозадачность (preemptive multitasking). Планировщик ОС может прервать поток в любой момент, даже посреди сложного вычисления.
Go исторически использовал кооперативную многозадачность с элементами вытеснения. Горутины сами «уступали» место в определенных точках:
Однако, начиная с версии Go 1.14, в язык было введено асинхронное вытеснение. Теперь, если горутина заняла поток ОС и выполняет плотный цикл вычислений более 10 мс, Runtime может принудительно приостановить её, используя сигналы ОС. Это решило проблему «жадных» горутин, которые могли заблокировать работу всей программы.
Почему это работает быстрее?
Секрет производительности не только в малом размере стека. Ключевое отличие — в том, где происходит принятие решения о переключении.
Когда поток ОС блокируется (например, ждет ответа от базы данных), планировщик Go видит это. Вместо того чтобы заставлять процессор простаивать в ожидании переключения контекста на уровне ядра, Go Runtime просто перебрасывает другие «живые» горутины на свободные потоки ОС.
Для процессора поток ОС остается активным, он продолжает выполнять полезную работу, просто внутри этого потока сменилась выполняемая функция. Это минимизирует количество дорогостоящих переходов из пространства пользователя (User Space) в пространство ядра (Kernel Space).
Подготовка к погружению в G-M-P
Чтобы эффективно управлять этим процессом, Go использует три сущности, которые мы детально разберем в следующей главе:
Именно связка этих трех элементов позволяет Go достигать невероятной пропускной способности, сохраняя при этом простоту написания кода через ключевое слово go.