Ist das gleichzeitige Schreiben auf stdout threadsicher?

Dec 09 2020

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

6 kostix Dec 09 2020 at 17:10

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 fmtPakets geben lediglich an, dass sie einen Wert einer Typ-Implementierungsschnittstelle annehmen io.Writerund 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.Fwhavetergleichzeitig 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 fmtSchreibens gesendet werden. (Man sollte auch bedenken, dass die fmt.*Print*Funktionen Writeihr 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.Fileoder Wrapper um Sockets, die durch die Funktionen des netPakets 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 WriteSie 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.Stdoutes 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“ Writeund Readrufen 1-zu-1 die Aufgabe zugrunde, das heißt, eine Writewird Anruf nie in zwei oder mehr zugrunde liegenden syscalls, und ein ReadAnruf 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, Writebeliebig 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.Stdouteine 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 fmtAllgemeinen serialisieren, da das Paket keine Garantien in Bezug auf bietet Die Anzahl der Aufrufe Writedes 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.Stdoutin einen benutzerdefinierten Typ eingeschlossen wird, der eine Sperre verwaltet, und sie verwendet wird. Und wenn wir schon dabei sind, macht das logPaket 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).