O que exatamente o runtime.Gosched faz?

86

Em uma versão anterior ao lançamento do go 1.5 do site Tour of Go , há um trecho de código semelhante a este.

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")
}

A saída é semelhante a esta:

hello
world
hello
world
hello
world
hello
world
hello

O que me incomoda é que quando runtime.Gosched()é removido, o programa não imprime mais "mundo".

hello
hello
hello
hello
hello

Por que? Como isso runtime.Gosched()afeta a execução?

Jason Yeo
fonte

Respostas:

143

Nota:

A partir do Go 1.5, GOMAXPROCS é definido como o número de núcleos do hardware: golang.org/doc/go1.5#runtime , abaixo da resposta original antes de 1.5.


Quando você executa o programa Go sem especificar a variável de ambiente GOMAXPROCS, as rotinas do Go são agendadas para execução em um único thread do sistema operacional. No entanto, para fazer o programa parecer multithread (é para isso que servem os goroutines, não são?), O agendador Go deve às vezes mudar o contexto de execução, para que cada goroutine possa fazer seu trabalho.

Como eu disse, quando a variável GOMAXPROCS não é especificada, o tempo de execução do Go só pode usar um thread, então é impossível alternar os contextos de execução enquanto o goroutine está realizando algum trabalho convencional, como cálculos ou mesmo IO (que é mapeado para funções C simples ) O contexto pode ser alternado somente quando primitivas de concorrência Go são usadas, por exemplo, quando você liga vários canais, ou (este é o seu caso) quando você diz explicitamente ao planejador para alternar os contextos - é para isso que runtime.Goschedserve.

Portanto, em resumo, quando o contexto de execução em uma goroutine atinge a Goschedchamada, o planejador é instruído a alternar a execução para outra goroutine. No seu caso, existem dois goroutines, principal (que representa o thread 'principal' do programa) e adicional, aquele com o qual você criou go say. Se você remover a Goschedchamada, o contexto de execução nunca será transferido do primeiro goroutine para o segundo, portanto, nenhum 'mundo' para você. Quando Goschedestá presente, o escalonador transfere a execução em cada iteração de loop da primeira goroutine para a segunda e vice-versa, então você tem 'hello' e 'world' intercalados.

Para sua informação, isso é chamado de 'multitarefa cooperativa': os goroutines devem ceder explicitamente o controle a outros goroutines. A abordagem usada na maioria dos sistemas operacionais contemporâneos é chamada de 'multitarefa preemptiva': threads de execução não estão preocupados com a transferência de controle; o planejador alterna os contextos de execução de forma transparente para eles. A abordagem cooperativa é freqüentemente usada para implementar 'threads verdes', ou seja, corrotinas simultâneas lógicas que não mapeiam 1: 1 para threads do sistema operacional - é assim que o tempo de execução Go e suas goroutinas são implementados.

Atualizar

Eu mencionei a variável de ambiente GOMAXPROCS, mas não expliquei o que é. É hora de consertar isso.

Quando esta variável é definida como um número positivo N, o tempo de execução Go será capaz de criar até Nencadeamentos nativos, nos quais todos os encadeamentos verdes serão agendados. Thread nativo: um tipo de thread criado pelo sistema operacional (threads do Windows, pthreads etc). Isso significa que se Nfor maior que 1, é possível que goroutines sejam programadas para executar em diferentes threads nativas e, conseqüentemente, rodar em paralelo (pelo menos, até as capacidades do seu computador: se o seu sistema for baseado em processador multicore, é provável que esses threads sejam realmente paralelos; se o seu processador tiver um único núcleo, a multitarefa preemptiva implementada nos threads do sistema operacional criará uma visibilidade de execução paralela).

É possível definir a variável GOMAXPROCS usando a runtime.GOMAXPROCS()função em vez de pré-definir a variável de ambiente. Use algo assim em seu programa em vez do atual main:

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

Neste caso, você pode observar resultados interessantes. É possível que você obtenha linhas 'hello' e 'world' impressas intercaladas de maneira desigual, por exemplo

hello
hello
world
hello
world
world
...

Isso pode acontecer se goroutines estiverem programadas para separar threads de sistema operacional. É assim que funciona a multitarefa preemptiva (ou processamento paralelo no caso de sistemas com vários núcleos): threads são paralelos e sua saída combinada é indeterminística. BTW, você pode deixar ou remover a Goschedchamada, parece não ter efeito quando GOMAXPROCS é maior que 1.

A seguir está o que obtive em várias execuções do programa com runtime.GOMAXPROCSchamada.

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

Veja, às vezes a saída é bonita, às vezes não. Indeterminismo em ação :)

Outra atualização

Parece que nas versões mais recentes do compilador Go, o tempo de execução do Go força as goroutines a renderem não apenas no uso de primitivos de simultaneidade, mas também nas chamadas do sistema operacional. Isso significa que o contexto de execução pode ser alternado entre goroutines também em chamadas de funções IO. Consequentemente, em compiladores Go recentes, é possível observar um comportamento indeterminístico mesmo quando GOMAXPROCS não está definido ou está definido como 1.

Vladimir Matveev
fonte
Bom trabalho ! Mas eu não encontrei esse problema no go 1.0.3, estranho.
WoooHaaaa
1
Isso é verdade. Acabei de verificar isso com o go 1.0.3 e, sim, esse comportamento não apareceu: mesmo com GOMAXPROCS == 1 o programa funcionou como se GOMAXPROCS> = 2. Parece que no 1.0.3 o planejador foi ajustado.
Vladimir Matveev
Acho que as coisas mudaram em relação ao compilador 1.4. Exemplo na questão de OPs parece ser a criação de threads de sistema operacional, enquanto este (-> gobyexample.com/atomic-counters ) parece criar um agendamento cooperativo. Atualize a resposta se for verdade
tez
8
A partir do Go 1.5, GOMAXPROCS é definido como o número de núcleos do hardware: golang.org/doc/go1.5#runtime
thepanuto
1
@paulkon, se Gosched()é necessário ou não depende do seu programa, não depende do GOMAXPROCSvalor. A eficiência da multitarefa preemptiva sobre a cooperativa também depende do seu programa. Se o seu programa for limitado por E / S, então a multitarefa cooperativa com E / S assíncrona provavelmente será mais eficiente (ou seja, terá mais rendimento) do que E / S síncrona baseada em thread; se o seu programa for limitado pela CPU (por exemplo, cálculos longos), a multitarefa cooperativa será muito menos útil.
Vladimir Matveev
8

O agendamento cooperativo é o culpado. Sem ceder, o outro (digamos "mundo") goroutine pode legalmente ter zero chances de executar antes / quando o principal terminar, que por especificações termina todos os gorutines - isto é. todo o processo.

zzzz
fonte
1
ok, então runtime.Gosched()rende. O que isso significa? Ele retorna o controle para a função principal?
Jason Yeo
5
Neste caso específico, sim. Geralmente, ele pede ao planejador para iniciar e executar qualquer uma das goroutines "prontas" em uma ordem de seleção intencionalmente não especificada.
zzzz