Подтвердить что ты не робот

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

В перед выпуском версии 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() влияет на выполнение?

4b9b3361

Ответ 1

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

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

Итак, короче говоря, когда контекст выполнения в одном goroutine достигает вызова Gosched, планировщику поручается переключить выполнение на другой goroutine. В вашем случае есть два goroutines, main (который представляет "основной" поток программы) и дополнительный, тот, который вы создали с помощью go say. Если вы удалите вызов Gosched, контекст выполнения никогда не будет перенесен с первой goroutine на второй, следовательно, для вас не будет "мира". Когда присутствует Gosched, планировщик переносит выполнение на каждой итерации цикла с первой горуты на вторую и наоборот, поэтому у вас есть "hello" и "world" interleaved.

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

Обновление

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

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

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

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

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

hello
hello
world
hello
world
world
...

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

Ниже приводится то, что я получил на нескольких прогонах программы с вызовом runtime.GOMAXPROCS.

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 runtime заставляет goroutines выводить не только на использование примитивов concurrency, но и на системные вызовы ОС. Это означает, что контекст выполнения может переключаться между goroutines также на вызовы функций ввода-вывода. Следовательно, в последних компиляторах Go можно наблюдать недетерминированное поведение, даже если GOMAXPROCS не задано или установлено в 1.

Ответ 2

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