Ist das gleichzeitige Schreiben auf stdout threadsicher?
Der folgende Code löst kein Datenrennen aus
package main
import (
"fmt"
"os"
"strings"
)
func main() {
x := strings.Repeat(" ", 1024)
go func() {
for {
fmt.Fprintf(os.Stdout, x+"aa\n")
}
}()
go func() {
for {
fmt.Fprintf(os.Stdout, x+"bb\n")
}
}()
go func() {
for {
fmt.Fprintf(os.Stdout, x+"cc\n")
}
}()
go func() {
for {
fmt.Fprintf(os.Stdout, x+"dd\n")
}
}()
<-make(chan bool)
}
Ich habe mehrere Datenlängen mit Variante ausprobiert https://play.golang.org/p/29Cnwqj5K30
Dieser Beitrag sagt, es ist nicht TS.
Diese Mail beantwortet die Frage nicht wirklich oder ich habe sie nicht verstanden.
Die Paketdokumentation von os und fmt erwähnt nicht viel darüber. Ich gebe zu, dass ich den Quellcode dieser beiden Pakete nicht ausgegraben habe, um weitere Erklärungen zu finden. Sie erscheinen mir zu komplex.
Was sind die Empfehlungen und ihre Referenzen ?
Antworten
Ich bin mir nicht sicher, ob es sich um eine endgültige Antwort handelt, aber ich werde versuchen, einen Einblick zu geben.
Die F*
-Funktionen des fmt
Pakets geben lediglich an, dass sie einen Wert einer Typ-Implementierungsschnittstelle annehmen io.Writer
und aufrufen Write
. Die Funktionen selbst sind für die gleichzeitige Verwendung sicher - in dem Sinne, dass es in Ordnung ist, eine beliebige Anzahl von Funktionen fmt.Fwhaveter
gleichzeitig aufzurufen : Das Paket selbst ist darauf vorbereitet, aber die Unterstützung einer Schnittstelle in Go sagt nichts über den realen Typ in Bezug auf die Parallelität aus.
Mit anderen Worten, der tatsächliche Punkt, an dem die Parallelität zulässig sein kann oder nicht, wird auf den "Schreiber" verschoben, an den die Funktionen des fmt
Schreibens gesendet werden. (Man sollte auch bedenken, dass die fmt.*Print*
Funktionen Write
ihr Ziel beliebig oft aufrufen dürfen - im Gegensatz zu denen, die im Aktienpaket enthalten sind log
.)
Wir haben also grundsätzlich zwei Fälle:
- Benutzerdefinierte Implementierungen von
io.Writer
. - Standardimplementierungen davon, wie
*os.File
oder Wrapper um Sockets, die durch die Funktionen desnet
Pakets erzeugt werden.
Der erste Fall ist der einfache: Was auch immer der Implementierer getan hat.
Der zweite Fall ist schwieriger: Soweit ich weiß, ist die Haltung der Go-Standardbibliothek dazu (wenn auch nicht klar in den Dokumenten angegeben) dahingehend, dass die Wrapper, die sie für "Dinge" bereitstellt, die vom Betriebssystem bereitgestellt werden - wie Dateideskriptoren und Sockets - vernünftig sind "thin" und damit die von ihnen implementierte Semantik wird transitiv vom stdlib-Code implementiert, der auf einem bestimmten System ausgeführt wird.
Zum Beispiel erfordertwrite(2) POSIX , dass Aufrufe in Bezug aufeinander atomar sind, wenn sie mit regulären Dateien oder symbolischen Links ausgeführt werden. Dies bedeutet, dass Write
Sie die Dokumente des Zielbetriebssystems konsultieren und sich ein Bild davon machen können, was passieren wird , da jeder Aufruf von Dingen, die Dateideskriptoren oder Sockets umschließen, tatsächlich zu einem einzigen "Schreib" -Syscall des Tagret-Systems führt.
Beachten Sie, dass POSIX nur über Dateisystemobjekte informiert. Wenn os.Stdout
es für ein Terminal (oder ein Pseudo-Terminal) oder für eine Pipe oder für alles andere geöffnet wird, das den write(2)
Systemaufruf unterstützt , hängen die Ergebnisse davon ab, welches Subsystem und / oder welcher Treiber relevant sind Implementieren - zum Beispiel können Daten von mehreren gleichzeitigen Aufrufen eingestreut werden oder einer der Aufrufe oder beide können nur vom Betriebssystem fehlgeschlagen werden - unwahrscheinlich, aber dennoch.
Zurück zu Go: Soweit ich weiß, gelten die folgenden Fakten für die Go stdlib-Typen, die Dateideskriptoren und Sockets umschließen:
- Sie können sicher gleichzeitig verwendet werden (ich meine, auf der Go-Ebene).
- Sie „Karte“
Write
undRead
rufen 1-zu-1 die Aufgabe zugrunde, das heißt, eineWrite
wird Anruf nie in zwei oder mehr zugrunde liegenden syscalls, und einRead
Anruf nie wieder Daten „ zusammengeklebt“ aus den Ergebnissen von mehrer zugrunde liegenden syscalls. (Übrigens werden Menschen gelegentlich von diesem schnörkellosen Verhalten gestolpert - sehen Sie sich dies oder das zum Beispiel als Beispiele an.)
Wenn wir dies mit der Tatsache betrachten, dass fmt.*Print*
es frei ist, Write
beliebig oft pro einzelnen Anruf anzurufen, werden Ihre Beispiele, die dies verwenden os.Stdout
, Folgendes tun :
- Führen Sie niemals zu einem Datenrennen - es sei denn, Sie haben der Variablen
os.Stdout
eine benutzerdefinierte Implementierung zugewiesen -, aber - Die tatsächlich in den zugrunde liegenden FD geschriebenen Daten werden in einer unvorhersehbaren Reihenfolge gemischt, die von vielen Faktoren abhängen kann, einschließlich der Betriebssystemkernversion und -einstellungen, der Version von Go, die zum Erstellen des Programms verwendet wird, der Hardware und der Systemlast.
TL; DR
- Mehrere gleichzeitige Aufrufe zum
fmt.Fprint*
Schreiben auf denselben "Writer" -Wert verschieben ihre Parallelität auf die Implementierung (Typ) des "Writers". - Es ist unmöglich, ein Datenrennen mit "dateiähnlichen" Objekten zu veranstalten, die von der Go stdlib in dem Setup bereitgestellt werden, das Sie in Ihrer Frage vorgestellt haben.
- Das eigentliche Problem besteht nicht in Datenrennen auf Go-Programmebene, sondern im gleichzeitigen Zugriff auf eine einzelne Ressource auf Betriebssystemebene. Und dort sprechen wir (normalerweise) nicht über Datenrennen, weil die von OS Go unterstützten Commodity-Dinge Dinge, in die man als Abstraktionen "schreiben" kann, als Abstraktionen offenlegen, bei denen ein echtes Datenrennen möglicherweise auf einen Fehler im Kernel oder im Treiber (und im Treiber) hinweisen würde Der Renndetektor von Go kann es sowieso nicht erkennen, da dieser Speicher nicht der Go-Laufzeit gehört, die den Prozess antreibt.
Wenn Sie in Ihrem Fall sicher sein müssen, dass die von einem bestimmten Anruf erzeugten Daten fmt.Fprint*
als ein einziges zusammenhängendes Teil des vom Betriebssystem bereitgestellten tatsächlichen Datenempfängers ausgegeben werden, müssen Sie diese Anrufe im fmt
Allgemeinen serialisieren, da das Paket keine Garantien in Bezug auf bietet Die Anzahl der Aufrufe Write
des angegebenen "Writers" für die exportierten Funktionen.
Die Serialisierung kann entweder extern (explizit, dh "Sperre nehmen, aufrufen fmt.Fprint*
, Sperre aufheben") oder intern sein, indem die os.Stdout
in einen benutzerdefinierten Typ eingeschlossen wird, der eine Sperre verwaltet, und sie verwendet wird. Und wenn wir schon dabei sind, macht das log
Paket genau das und kann sofort verwendet werden, da die "Logger", die es bereitstellt, einschließlich des Standard-Loggers, die Ausgabe von "Log-Headern" (wie dem Zeitstempel und dem Namen) verhindern können der Datei).