Что именно делает runtime.Gosched?

86

В версии, предшествующей выпуску go 1.5 веб-сайта Tour of Go , есть фрагмент кода, который выглядит следующим образом.

package main

import (
    "fmt"
    "runtime"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        runtime.Gosched()
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

Результат выглядит так:

hello
world
hello
world
hello
world
hello
world
hello

Меня беспокоит то, что при runtime.Gosched()удалении программа перестает печатать "мир".

hello
hello
hello
hello
hello

Почему это так? Как runtime.Gosched()влияет на исполнение?

Джейсон Йео
источник

Ответы:

143

Заметка:

Начиная с Go 1.5, GOMAXPROCS настроен на количество ядер оборудования: golang.org/doc/go1.5#runtime , ниже исходного ответа до 1.5.


Когда вы запускаете программу Go без указания переменной среды GOMAXPROCS, горутины Go планируются для выполнения в одном потоке ОС. Однако, чтобы программа выглядела многопоточной (для чего нужны горутины, не так ли?), Планировщик Go должен иногда переключать контекст выполнения, чтобы каждая горутина могла выполнять свою часть работы.

Как я уже сказал, когда переменная GOMAXPROCS не указана, среде выполнения Go разрешено использовать только один поток, поэтому невозможно переключать контексты выполнения, пока goroutine выполняет некоторую обычную работу, например вычисления или даже ввод-вывод (который отображается на простые функции C. ). Контекст может переключаться только тогда, когда используются примитивы параллелизма Go, например, когда вы включаете несколько каналов, или (это ваш случай), когда вы явно указываете планировщику переключать контексты - это то runtime.Gosched, для чего.

Короче говоря, когда контекст выполнения в одной горутине достигает Goschedвызова, планировщик получает указание переключить выполнение на другую горутину. В вашем случае есть две горутины, основная (которая представляет «основной» поток программы) и дополнительная, которую вы создали go say. Если вы удалите Goschedвызов, контекст выполнения никогда не будет перенесен из первой горутины во вторую, поэтому для вас не будет «мира». Когда Goschedприсутствует, планировщик передает выполнение на каждой итерации цикла от первой горутины ко второй и наоборот, так что у вас чередуются «привет» и «мир».

К вашему сведению, это называется «кооперативная многозадачность»: горутины должны явно передавать управление другим горутинам. Подход, используемый в большинстве современных операционных систем, называется «вытесняющей многозадачностью»: потоки выполнения не связаны с передачей управления; планировщик прозрачно переключает им контексты выполнения. Кооперативный подход часто используется для реализации «зеленых потоков», то есть логических параллельных сопрограмм, которые не отображают 1: 1 в потоки ОС - так реализуется среда выполнения Go и ее горутины.

Обновить

Я упомянул переменную среды GOMAXPROCS, но не объяснил, что это такое. Пора это исправить.

Если для этой переменной установлено положительное число N, среда выполнения Go сможет создавать до Nсобственных потоков, для которых будут запланированы все зеленые потоки. Родной поток - это своего рода поток, который создается операционной системой (потоки Windows, pthreads и т. Д.). Это означает, что если Nбольше 1, возможно, что горутины будут запланированы для выполнения в разных собственных потоках и, следовательно, будут выполняться параллельно (по крайней мере, с учетом возможностей вашего компьютера: если ваша система основана на многоядерном процессоре, она вполне вероятно, что эти потоки будут по-настоящему параллельными; если ваш процессор имеет одноядерный процессор, то вытесняющая многозадачность, реализованная в потоках ОС, создаст видимость параллельного выполнения).

Можно установить переменную GOMAXPROCS с помощью runtime.GOMAXPROCS()функции вместо предварительной установки переменной среды. Используйте в своей программе что-то подобное вместо текущего main:

func main() {
    runtime.GOMAXPROCS(2)
    go say("world")
    say("hello")
}

В этом случае можно наблюдать интересные результаты. Возможно, что вы получите напечатанные строки hello и world с неравномерным чередованием, например

hello
hello
world
hello
world
world
...

Это может произойти, если горутины запланированы для разделения потоков ОС. Фактически, именно так работает вытесняющая многозадачность (или параллельная обработка в случае многоядерных систем): потоки параллельны, а их комбинированный вывод является неопределенным. Кстати, вы можете оставить или удалить Goschedвызов, похоже, это не действует, когда GOMAXPROCS больше 1.

Вот что я получил при нескольких запусках программы с runtime.GOMAXPROCScall.

hyperplex /tmp % go run test.go
hello
hello
hello
world
hello
world
hello
world
hyperplex /tmp % go run test.go
hello
world
hello
world
hello
world
hello
world
hello
world
hyperplex /tmp % go run test.go
hello
hello
hello
hello
hello
hyperplex /tmp % go run test.go
hello
world
hello
world
hello
world
hello
world
hello
world

Видите ли, иногда результат бывает красивым, иногда нет. Индетерминизм в действии :)

Еще одно обновление

Похоже, что в более новых версиях компилятора Go среда выполнения Go заставляет горутины уступать не только при использовании примитивов параллелизма, но и при системных вызовах ОС. Это означает, что контекст выполнения можно переключать между горутинами также при вызове функций ввода-вывода. Следовательно, в последних компиляторах Go можно наблюдать недетерминированное поведение, даже когда GOMAXPROCS не установлен или установлен в 1.

Владимир Матвеев
источник
Прекрасная работа ! Но я не встречал этой проблемы в go 1.0.3, wierd.
WoooHaaaa
1
Это верно. Я только что проверил это с помощью go 1.0.3, и да, такого поведения не было: даже с GOMAXPROCS == 1 программа работала так, как будто GOMAXPROCS> = 2. Похоже, что в 1.0.3 планировщик был изменен.
Владимир Матвеев
Я думаю, что с компилятором 1.4 все изменилось. Пример в вопросе OPs, похоже, создает потоки ОС, в то время как этот (-> gobyexample.com/atomic-counters ), похоже, создает совместное планирование. Обновите ответ, если это правда
tez
8
Начиная с Go 1.5, GOMAXPROCS настроен на количество ядер оборудования: golang.org/doc/go1.5#runtime
thepanuto
1
@paulkon, нужен он или нет, Gosched()зависит от вашей программы, это не зависит от GOMAXPROCSзначения. Эффективность вытесняющей многозадачности над кооперативной также зависит от вашей программы. Если ваша программа связана с вводом-выводом, то совместная многозадачность с асинхронным вводом-выводом, вероятно, будет более эффективной (т. Е. С большей пропускной способностью), чем синхронный ввод-вывод на основе потоков; если ваша программа ограничена процессором (например, длинные вычисления), то совместная многозадачность будет гораздо менее полезной.
Владимир Матвеев
8

Виной всему совместное планирование. Без уступки другая (скажем, «мировая») горутина может юридически получить нулевые шансы на выполнение до / после завершения main, что согласно спецификации завершает все горутины - т.е. весь процесс.

zzzz
источник
1
хорошо, так runtime.Gosched()уступает. Что это значит? Он возвращает управление основной функции?
Джейсон Йео
5
В данном конкретном случае да. Обычно он просит планировщик задействовать и запустить любую из «готовых» горутин в намеренно неуказанном порядке выбора.
zzzz