Co dokładnie robi runtime.Gosched?

86

W wersji poprzedzającej wydanie go 1.5 strony internetowej Tour of Go , istnieje fragment kodu, który wygląda następująco.

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

Wynik wygląda następująco:

hello
world
hello
world
hello
world
hello
world
hello

Martwi mnie to, że po runtime.Gosched()usunięciu program nie drukuje już „świata”.

hello
hello
hello
hello
hello

Dlaczego to jest takie? Jak runtime.Gosched()wpływa na wykonanie?

Jason Yeo
źródło

Odpowiedzi:

143

Uwaga:

Począwszy od wersji Go 1.5, GOMAXPROCS jest ustawiony na liczbę rdzeni sprzętu: golang.org/doc/go1.5#runtime , poniżej oryginalnej odpowiedzi przed 1.5.


Gdy uruchamiasz program Go bez określenia zmiennej środowiskowej GOMAXPROCS, goroutiny Go są planowane do wykonania w pojedynczym wątku systemu operacyjnego. Jednak aby program wyglądał na wielowątkowość (do tego służą gorutyny, prawda?), Program planujący Go musi czasami zmieniać kontekst wykonania, aby każdy z nich mógł wykonać swoją pracę.

Jak powiedziałem, gdy zmienna GOMAXPROCS nie jest określona, ​​środowisko uruchomieniowe Go może używać tylko jednego wątku, więc nie można przełączać kontekstów wykonywania, gdy goroutine wykonuje jakąś konwencjonalną pracę, taką jak obliczenia lub nawet IO (która jest odwzorowana na zwykłe funkcje C ). Kontekst można przełączyć tylko wtedy, gdy używane są prymitywy współbieżności Go, np. Gdy włączasz kilka kanałów lub (tak jest w twoim przypadku), gdy jawnie nakazujesz harmonogramowi przełączanie kontekstów - do tego służy runtime.Gosched.

Więc, w skrócie, kiedy kontekst wykonania w jednej gorutynie osiągnie Goschedwywołanie, program planujący jest instruowany, aby przełączyć wykonanie na inną gorutynę. W twoim przypadku są dwa gorutiny, główny (który reprezentuje „główny” wątek programu) i dodatkowy, ten, który utworzyłeś go say. Jeśli usuniesz Goschedwywołanie, kontekst wykonania nigdy nie zostanie przeniesiony z pierwszego gorutynu do drugiego, stąd nie będzie dla Ciebie „świata”. Kiedy Goschedjest obecny, planista przenosi wykonanie w każdej iteracji pętli z pierwszej gorutyny do drugiej i odwrotnie, więc masz przeplatane „witaj” i „świat”.

FYI, nazywa się to „wielozadaniowością opartą na współpracy”: gorutyny muszą wyraźnie ustępować innym gorutynom. Podejście stosowane w większości współczesnych systemów operacyjnych nazywa się „wielozadaniowością wywłaszczającą”: ​​wątki wykonawcze nie są związane z przenoszeniem sterowania; zamiast tego program planujący przełącza konteksty wykonywania na nie. Podejście kooperatywne jest często stosowane do implementacji „zielonych wątków”, to znaczy logicznych współbieżnych procedur, które nie mapują 1: 1 na wątki systemu operacyjnego - w ten sposób implementowane jest środowisko uruchomieniowe Go i jego gorutyny.

Aktualizacja

Wspomniałem o zmiennej środowiskowej GOMAXPROCS, ale nie wyjaśniłem, co to jest. Czas to naprawić.

Gdy ta zmienna jest ustawiona na liczbę dodatnią N, środowisko uruchomieniowe Go będzie mogło utworzyć do Nwątków natywnych, dla których zaplanowane zostaną wszystkie zielone wątki. Wątek macierzysty rodzaj wątku, który jest tworzony przez system operacyjny (wątki Windows, pthreads itp.). Oznacza to, że jeśli Njest większe niż 1, możliwe jest, że gorutyny zostaną zaplanowane do wykonania w różnych wątkach natywnych iw konsekwencji będą działać równolegle (przynajmniej do możliwości komputera: jeśli system jest oparty na procesorze wielordzeniowym, jest prawdopodobne, że te wątki będą naprawdę równoległe; jeśli twój procesor ma jeden rdzeń, wielozadaniowość z wywłaszczaniem zaimplementowana w wątkach systemu operacyjnego zapewni widoczność wykonywania równoległego).

Możliwe jest ustawienie zmiennej GOMAXPROCS za pomocą runtime.GOMAXPROCS()funkcji zamiast wstępnego ustawiania zmiennej środowiskowej. Użyj czegoś takiego w swoim programie zamiast bieżącego main:

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

W tym przypadku można zaobserwować ciekawe wyniki. Może się zdarzyć, że linie „hello” i „world” zostaną wydrukowane nierównomiernie, np

hello
hello
world
hello
world
world
...

Może się to zdarzyć, jeśli goroutyny mają oddzielać wątki systemu operacyjnego. W rzeczywistości tak działa wielozadaniowość z wywłaszczaniem (lub przetwarzanie równoległe w przypadku systemów wielordzeniowych): wątki są równoległe, a ich łączny wynik jest nieokreślony. BTW, możesz opuścić lub usunąć Goschedpołączenie, wydaje się, że nie ma to żadnego efektu, gdy GOMAXPROCS jest większe niż 1.

Oto, co otrzymałem w kilku uruchomieniach programu z runtime.GOMAXPROCSwywołaniem.

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

Widzisz, czasami wyjście jest ładne, a czasami nie. Indeterminizm w akcji :)

Kolejna aktualizacja

Wygląda na to, że w nowszych wersjach kompilatora Go środowisko uruchomieniowe Go wymusza na gorutynach nie tylko wykorzystanie elementów podstawowych współbieżności, ale także wywołań systemowych systemu operacyjnego. Oznacza to, że kontekst wykonania może być przełączany między gorutynami również przy wywołaniach funkcji IO. W związku z tym w najnowszych kompilatorach Go można zaobserwować niezdeterministyczne zachowanie, nawet gdy GOMAXPROCS jest nieustawiony lub ustawiony na 1.

Vladimir Matveev
źródło
Dobra robota ! Ale nie napotkałem tego problemu pod go 1.0.3, dziwne.
WoooHaaaa
1
To prawda. Właśnie sprawdziłem to w go 1.0.3 i tak, to zachowanie się nie pojawiło: nawet z GOMAXPROCS == 1 program działał tak, jakby GOMAXPROCS> = 2. Wygląda na to, że w 1.0.3 harmonogram został zmodyfikowany.
Vladimir Matveev
Myślę, że wszystko się zmieniło w kompilatorze 1.4. Przykład w pytaniu OP wydaje się tworzyć wątki systemu operacyjnego, podczas gdy to (-> gobyexample.com/atomic-counters ) wydaje się tworzyć wspólne planowanie. Proszę zaktualizować odpowiedź, jeśli to prawda
tez
8
Od wersji Go 1.5, GOMAXPROCS jest ustawiony na liczbę rdzeni sprzętu: golang.org/doc/go1.5#runtime
thepanuto
1
@paulkon, to, czy Gosched()jest potrzebny , czy nie , zależy od twojego programu, nie zależy od GOMAXPROCSwartości. Skuteczność wielozadaniowości z wywłaszczaniem w stosunku do kooperatywnej zależy również od programu. Jeśli Twój program jest powiązany z operacjami we / wy, wówczas współpraca wielozadaniowa z asynchronicznym we / wy będzie prawdopodobnie bardziej wydajna (tj. Będzie miała większą przepustowość) niż synchroniczne operacje we / wy oparte na wątkach; jeśli twój program jest związany z procesorem (np. długie obliczenia), wówczas wielozadaniowość kooperacyjna będzie znacznie mniej przydatna.
Vladimir Matveev
8

Winowajcą jest wspólne planowanie. Bez ustępowania, druga gorutyna (powiedzmy „świat”) może legalnie mieć zerowe szanse na wykonanie przed / po zakończeniu main, co zgodnie ze specyfikacją kończy wszystkie gorutyny - tj. cały proces.

zzzz
źródło
1
ok, więc runtime.Gosched()ustępuje. Co to znaczy? Czy daje kontrolę z powrotem do głównej funkcji?
Jason Yeo
5
W tym konkretnym przypadku tak. Generalnie prosi planistę o uruchomienie i uruchomienie dowolnej z „gotowych” gorutyn w celowo nieokreślonej kolejności wyboru.
zzzz