Что такое горутина в Go и чем она отличается от системного потока (OS thread)?

Ответ

Горутина — это легковесная, управляемая средой выполнения (runtime) Go, функция или метод, выполняющийся конкурентно с другими горутинами. Это ключевая концепция для достижения конкурентности в Go.

Ключевые отличия от системных потоков:

  1. Стоимость создания и память:

    • Горутины: Очень дешевы. Начальный размер стека всего ~2 КБ, который может динамически расти и сжиматься. Можно легко запустить сотни тысяч и даже миллионы горутин.
    • Потоки ОС: Дорогие. Имеют фиксированный и значительно больший размер стека (обычно 1-8 МБ). Создание большого количества потоков сильно нагружает ОС.
  2. Управление и переключение контекста:

    • Горутины: Управляются планировщиком Go в пространстве пользователя (user space). Переключение между горутинами происходит очень быстро и не требует дорогостоящих системных вызовов.
    • Потоки ОС: Управляются ядром операционной системы. Переключение контекста между потоками — медленная операция.
  3. Модель выполнения (M:N):

    • Планировщик Go распределяет M горутин для выполнения на N системных потоках (где M обычно гораздо больше N). Это позволяет эффективно утилизировать процессорные ядра.

Идиоматичный пример с sync.WaitGroup:

Использование time.Sleep для синхронизации — плохая практика. Правильнее использовать примитивы синхронизации, например sync.WaitGroup.

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(1) // Сообщаем, что нужно дождаться одну горутину

    go func() {
        defer wg.Done() // Сигнализируем о завершении работы в конце функции
        fmt.Println("Горутина работает!")
        time.Sleep(time.Second)
    }()

    fmt.Println("Основная функция ждет завершения горутины...")
    wg.Wait() // Блокируем выполнение, пока счетчик WaitGroup не станет равен 0
    fmt.Println("Горутина завершила работу.")
}

Ответ 18+ 🔞

А, ну ты про эти штуки, горутины, да? Ну слушай, сейчас объясню так, что даже твоя бабка поймёт, хотя она, конечно, нихуя в этом не смыслит.

Представь себе, блядь, обычный поток в операционке — это как здоровенный КамАЗ. Места дохуя занимает, бензина жрёт, развернуться ебись-свинья. Запустить тысячу таких — и твой комп накрывается медным тазом, потому что операционка с ума сходит, пытаясь их всех переключать.

А горутина — это, сука, как электросамокат. Лёгкий, ёбушки-воробушки, места в памяти — кот наплакал, пару килобайт. Их можно, блядь, как тараканов развести — сотни тысяч! И управляет ими не ядро ОС, а свой, родной планировщик Go, который как хитрая жопа, распределяет эту ораву самокатчиков по настоящим потокам-КамАЗам. Перепрыгнул с одного на другой — и даже не чихнул. Быстро, дёшево, сердито.

А теперь смотри, как правильно делать, а то некоторые мудаки через time.Sleep синхронизируются — это пиздец какой-то, волнение ебать. Нормальные люди используют WaitGroup. Это как счётчик, понимаешь? Сказал ему: «Слушай, я тут одну горутину запустил, жди её». А в конце горутина пишет: «Всё, я сделала, свободна!». И основная программа сидит и ждёт, пока этот счётчик не обнулится. Красота, а не жизнь.

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(1) // Говорим: "Эй, мудила, запомни — одну ждём!"

    go func() {
        defer wg.Done() // А это волшебное слово, которое скажет "Всё, я кончил!" даже если тут пиздец случится
        fmt.Println("Горутина работает!")
        time.Sleep(time.Second)
    }()

    fmt.Println("Основная функция ждет завершения горутины...")
    wg.Wait() // Тут мы встаём как вкопанные и не двигаемся дальше, пока счётчик не ноль
    fmt.Println("Горутина завершила работу. Ура, товарищи!")
}

Вот и вся магия, ёпта. Никаких КамАЗов, одни быстрые самокаты, и все довольны. Главное — синхронизацию не проеби, а то получишь race condition, и тогда пидары налетят.