Перейти к содержимому
Теория · Горутины и каналы · урок 1 из 13

Горутины

Чему вы научитесь

Чему вы научитесь:

  • объяснять, чем конкурентность отличается от параллелизма и горутина — от потока;
  • запускать горутины и дожидаться их через sync.WaitGroup;
  • передавать значения между горутинами по каналам, закрывать каналы и ждать нескольких каналов через select;
  • защищать общие данные от гонки данных с помощью sync.Mutex.
Один повар ведёт три блюда сразу: суп, салат и сковороду, на бирке супа надпись go

Представьте кухню ресторана. Один повар может готовить три блюда сразу: ставит суп, пока он варится — режет салат, потом возвращается к супу. В каждый момент он делает что-то одно, но все три блюда продвигаются. Три повара у трёх плит работают иначе: каждый занят своим блюдом в одно и то же мгновение.

Конкурентность и параллелизм

Эти две картинки — два разных понятия.

Конкурентность

Конкурентность — когда программа состоит из нескольких независимых задач, которые продвигаются вперемешку. Это один повар и три блюда.

Конкурентность — про то, как устроена программа: из каких задач она состоит.

Параллелизм

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

Ядро — часть процессора, которая выполняет одну последовательность команд за раз. Параллелизм — про то, как программа выполняется.

Чем они отличаются

Этот вопрос часто задают на собеседовании: «Чем конкурентность отличается от параллелизма?» Ответ собирается из трёх пунктов:

  • конкурентность — про то, как устроена программа: из каких задач она состоит;
  • параллелизм — про то, как она выполняется;
  • конкурентная программа на нескольких ядрах может работать параллельно, а на одном ядре её задачи идут по очереди.

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

Горутины и потоки

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

Поток

Операционная система выполняет программы в потоках. Поток стоит дорого:

  • обычно под него сразу резервируют около мегабайта памяти или больше;
  • создаёт и переключает потоки сама операционная система.

Горутина

Горутине на старте хватает нескольких килобайт, и память она добирает, только когда ей нужно. Поэтому программа на Go может держать тысячи и даже сотни тысяч горутин.

Среда выполнения Go

Распределяет горутины среда выполнения Go — код, который Go добавляет в каждую программу. Она работает так:

  • раскладывает много горутин на немного потоков;
  • по умолчанию одновременно код Go выполняют столько потоков, сколько ядер доступно программе;
  • остальные горутины в это время ждут своей очереди, как повара ждут свободную плиту.

Здесь аналогия с кухней заканчивается. Настоящий шеф заранее знает, кто за кем встанет к плите. А среда выполнения Go ничего не обещает: какая горутина когда работает, может меняться от запуска к запуску.

Оператор go

Программа на Go выполняет вызовы по очереди: следующая строка ждёт, пока функция не вернётся. Ключевое слово go перед вызовом меняет это правило. Вызов запускается в новой горутине, а main идёт дальше, не дожидаясь его:

GO
package main

import "fmt"

func say(word string) {
    fmt.Println(word)
}

func main() {
    go say("hello")
    fmt.Println("main")
}

Давайте разберём, что здесь происходит:

  1. go say("hello") нанимает нового повара и сразу возвращает управление.
  2. Go вычисляет аргументы сразу, на строке с go, а тело say выполняет в новой горутине.
  3. Go отбрасывает результаты функции.
  4. fmt.Println("main") печатает main, и main возвращается.

Строку main программа печатает при каждом запуске, а hello — не всегда. Вместе с main заканчивается вся программа. Если say не успела дойти до fmt.Println, её вывод пропадает: кухня закрылась вместе с поварами.

Горутина из анонимной функции

Оператор go работает с любым вызовом:

  • функции;
  • метода;
  • анонимной функции.

Часто в горутине запускают анонимную функцию. Круглые скобки после её тела — это вызов:

GO
for i := range 3 {
    go func() {
        fmt.Println(i)
    }()
}

Разберём, что здесь происходит:

  1. Каждая итерация запускает свою горутину, и каждая видит своё значение i.
  2. main и здесь их не ждёт.
  3. Цикл может напечатать 0, 1 и 2 в любом порядке, только часть чисел или ничего.

Полагаться на порядок работы горутин нельзя.

Горутины в браузере

Задачи курса выполняются в браузере, где у программы одно ядро. Конкурентность там работает, а параллелизма нет: горутины выполняются по очереди. Но порядок их работы и там не задан.

Частая ошибка

Анонимную функцию передали в go, но не вызвали — забыли скобки после тела:

GO
go func() {
    fmt.Println("hello")
} // ✗
TEXT
expression in go must be function call

Программа не компилируется: оператору go нужен вызов функции, а не сама функция. Исправление — поставить () после закрывающей скобки: }().

Главное

  • Конкурентность — как программа разбита на задачи; параллелизм — их одновременное выполнение на разных ядрах.
  • Горутина намного дешевле потока: среда выполнения Go раскладывает много горутин на немного потоков.
  • go f() запускает f в новой горутине; когда main возвращается, программа заканчивается, не дожидаясь остальных.
  • Порядок работы горутин не задан и может меняться от запуска к запуску.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом