Ab wann kann eine Goroutine nachgeben?
Ich versuche ein besseres Verständnis dafür zu erlangen, wie Goroutinen in Go-Programmen geplant sind, insbesondere an welchen Punkten sie anderen Goroutinen nachgeben können. Wir wissen, dass eine Goroutine Syscals liefert, die sie blockieren würden, aber anscheinend ist dies nicht das ganze Bild.
Diese Frage wirft ähnliche Bedenken auf, und die am besten bewerteten Antworten besagen, dass eine Goroutine auch Funktionsaufrufe einschalten kann, da dies den Scheduler aufrufen würde, um zu prüfen, ob die Stapel vergrößert werden müssen, aber dies besagt ausdrücklich
Wenn Sie keine Funktionsaufrufe haben, nur ein bisschen Mathe, dann sperrt Goroutine den Thread, bis er beendet wird oder etwas trifft, das anderen die Ausführung bringen könnte.
Ich habe ein einfaches Programm geschrieben, um das zu überprüfen und zu beweisen:
package main
import "fmt"
var output [30]string // 3 times, 10 iterations each.
var oi = 0
func main() {
runtime.GOMAXPROCS(1) // Or set it through env var GOMAXPROCS.
chanFinished1 := make(chan bool)
chanFinished2 := make(chan bool)
go loop("Goroutine 1", chanFinished1)
go loop("Goroutine 2", chanFinished2)
loop("Main", nil)
<- chanFinished1
<- chanFinished2
for _, l := range output {
fmt.Println(l)
}
}
func loop(name string, finished chan bool) {
for i := 0; i < 1000000000; i++ {
if i % 100000000 == 0 {
output[oi] = name
oi++
}
}
if finished != nil {
finished <- true
}
}
HINWEIS: Mir ist bewusst, dass das Einfügen eines Werts in das Array und das Inkrementieren oiohne Synchronisierung nicht ganz korrekt ist, aber ich möchte den Code einfach und frei von Dingen halten, die zu einem Wechsel führen könnten. Das Schlimmste, was passieren kann, ist, einen Wert zu setzen, ohne den Index zu erhöhen (Überschreiben), was keine große Sache ist.
Im Gegensatz zu dieser Antwort habe ich die Verwendung von Funktionsaufrufen (einschließlich append()der integrierten loop()Funktion ) der Funktion, die als Goroutine gestartet wird, vermieden. Außerdem stelle ich explizit fest, GOMAXPROCS=1welche gemäß der Dokumentation :
begrenzt die Anzahl der Betriebssystem-Threads, die Go-Code auf Benutzerebene gleichzeitig ausführen können.
Dennoch in der Ausgabe sehe ich immer noch die Nachrichten Main/ Goroutine 1/ Goroutine 2verschachtelt, eine der folgenden Bedeutung:
- Die Ausführung der Goroutine unterbricht und die Goroutine gibt in einigen Momenten die Kontrolle auf.
GOMAXPROCSfunktioniert nicht wie in der Dokumentation angegeben, es werden mehr Betriebssystem-Threads gestartet, um Goroutinen zu planen.
Entweder ist die Antwort nicht vollständig oder einige Dinge haben sich seit 2016 geändert (ich habe auf Go 1.13.5 und 1.15.2 getestet).
Es tut mir leid, wenn die Frage beantwortet wurde, aber ich konnte weder eine Erklärung dafür finden, warum dieses spezielle Beispiel Kontrolle liefert, noch Punkte, an denen Goroutinen im Allgemeinen Kontrolle liefern (mit Ausnahme des Blockierens von Systemaufrufen).
HINWEIS: Diese Frage ist rein theoretisch. Ich versuche derzeit nicht, eine praktische Aufgabe zu lösen. Im Allgemeinen gehe ich jedoch davon aus, dass wir wissen, wo eine Goroutine nachgeben kann und wo wir die redundante Verwendung von Synchronisationsprimitiven nicht vermeiden können.
Antworten
Mit Go Version 1.14 wurde die asynchrone Preemption eingeführt:
Goroutinen sind jetzt asynchron präemptibel. Infolgedessen blockieren Schleifen ohne Funktionsaufrufe den Scheduler möglicherweise nicht mehr oder verzögern die Speicherbereinigung erheblich. Dies wird auf allen Plattformen unterstützt , außer
windows/arm,darwin/arm,js/wasmundplan9/*.
Wie in Sind der Kanal Sperrpunkte für die Goroutine-Planung gesendet? Die Vorkaufspunkte von Go können sich von einer Version zur nächsten ändern. Durch die asynchrone Vorauszahlung werden fast überall mögliche Vorauszahlungspunkte hinzugefügt.
Ihre Schreibvorgänge in das outputArray sind nicht synchronisiert und Ihr oiIndex ist nicht atomar, was bedeutet, dass wir nicht wirklich sicher sein können, was in Bezug auf das Ausgabearray passiert. Das Hinzufügen von Atomizität mit einem Mutex führt natürlich zu kooperativen Planungspunkten. Dies ist zwar nicht die Quelle für kooperative Planungswechsel (die basierend auf Ihrer Ausgabe erfolgen müssen), sie beeinträchtigen jedoch unser Verständnis des Programms.
Das outputArray enthält Zeichenfolgen und unter Verwendung von Zeichenketten können die Speicherbereinigungssystem aufrufen, die können Schlösser und Ursache Scheduling Schalt verwenden. Dies ist also die wahrscheinlichste Ursache für das Planen von Switching in Implementierungen vor Go-1.14.
Wie @torek hervorgehoben hat, verwenden die beliebtesten Laufzeitumgebungen für GO seit einigen Monaten (seit 1.14) die vorbeugende Planung. Ansonsten variieren die Punkte, an denen eine Goroutine nachgeben kann, abhängig von der Laufzeitumgebung und der Veröffentlichung, aber William Kennedy gibt eine gute Zusammenfassung.
Ich erinnere mich auch, dass dem Compiler vor einigen Jahren eine Option hinzugefügt wurde, um lang laufenden Schleifen Ertragspunkte hinzuzufügen, aber dies war eine experimentelle Option, die normalerweise nicht ausgelöst wurde. (Natürlich können Sie dies manuell tun, indem Sie ab runtime.GoSchedund zu in Ihrer Schleife aufrufen .)
Was Ihren Test betrifft, bin ich überrascht über das Ergebnis, das Sie unter Go 1.13.5 erzielt haben. Das Verhalten ist aufgrund der Datenrennen nicht genau definiert (ich weiß, dass Sie Synchronisationsmechanismen vermieden haben, um das Auslösen eines Ertrags zu vermeiden), aber ich hätte dieses Ergebnis nicht erwartet. Eine Sache ist, dass das Setzen GOMAXPROCSauf 1 bedeutet, dass nur eine Goroutine gleichzeitig ausgeführt wird, aber das muss nicht unbedingt bedeuten, dass wenn eine andere Goroutine ausgeführt wird, sie auf demselben Kern ausgeführt wird. Ein anderer Kern hat einen anderen Cache und (ohne Synchronisation) unterschiedliche Meinungen über die Werte von outputund oi.
Aber darf ich vorschlagen, dass Sie einfach das Ändern globaler Variablen vergessen und einfach eine Nachricht vor und nach der Besetztschleife protokollieren. Dies sollte deutlich zeigen (in GO <1.14), dass jeweils nur ein Lopp ausgeführt wird. (Ich habe vor vielen Jahren versucht, dasselbe Experiment wie Sie durchzuführen, und das schien zu funktionieren.)