est l'écriture simultanée sur stdout threadsafe?

Dec 09 2020

le code ci-dessous ne lance pas une course aux données

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

J'ai essayé plusieurs longueurs de données, avec une variante https://play.golang.org/p/29Cnwqj5K30

Ce message dit que ce n'est pas TS.

Ce mail ne répond pas vraiment à la question, ou je n'ai pas compris.

La documentation des paquets d' os et de fmt ne mentionne pas grand-chose à ce sujet. J'avoue ne pas avoir fouillé le code source de ces deux paquets pour trouver des explications supplémentaires, ils me paraissent trop complexes.

Quelles sont les recommandations et leurs références ?

Réponses

6 kostix Dec 09 2020 at 17:10

Je ne suis pas sûr que cela puisse être considéré comme une réponse définitive, mais je vais essayer de vous donner un aperçu.

Les F*-fonctions du fmtpaquet indiquent simplement qu'elles prennent une valeur d'un type implémentant une io.Writerinterface et l'appellent Write. Les fonctions elles-mêmes sont sûres pour une utilisation simultanée - dans le sens où il est possible d'appeler n'importe quel nombre de fmt.Fwhavetersimultanément: le package lui-même est préparé pour cela, mais la prise en charge d'une interface dans Go n'indique rien sur le type réel de concurrence.

En d'autres termes, le point réel où la concurrence peut ou non être autorisée est reporté au "rédacteur" auquel les fonctions d' fmtécriture sont attribuées. (Il convient également de garder à l'esprit que les fmt.*Print*fonctions sont autorisées à appeler Writesa destination un certain nombre de fois - par opposition à celles fournies par le package de stock log.)

Donc, nous avons essentiellement deux cas:

  • Implémentations personnalisées de io.Writer.
  • Des implémentations de stock de celui-ci, telles que *os.Fileou des enveloppes autour de sockets produites par les fonctions de netpackage.

Le premier cas est le plus simple: quoi que fasse l'implémenteur.

Le deuxième cas est plus difficile: si je comprends bien, la position de la bibliothèque standard Go à ce sujet (bien que cela ne soit pas clairement indiqué dans la documentation) en ce sens que les wrappers qu'elle fournit autour des «éléments» fournis par le système d'exploitation - tels que les descripteurs de fichiers et les sockets - sont raisonnablement "thin", et donc quelle que soit la sémantique qu'ils implémentent, est implémenté de manière transitoire par le code stdlib s'exécutant sur un système particulier.

Par exemple, POSIX exige que les write(2)appels soient atomiques les uns par rapport aux autres lorsqu'ils fonctionnent sur des fichiers normaux ou des liens symboliques. Cela signifie que, puisque tout appel à des Writeobjets encapsulant des descripteurs de fichiers ou des sockets aboutit en fait à un seul appel système "écriture" du système tagret, vous pouvez consulter la documentation du système d'exploitation cible et avoir une idée de ce qui va se passer.

Notez que POSIX ne parle que des objets du système de fichiers, et s'il os.Stdoutest ouvert à un terminal (ou un pseudo-terminal) ou à un tube ou à tout autre élément prenant en charge l' write(2)appel système, les résultats dépendront de ce que le sous-système concerné et / ou le pilote implémenter - par exemple, les données de plusieurs appels simultanés peuvent être entrecoupées, ou l'un des appels, ou les deux, peut simplement échouer par le système d'exploitation - peu probable, mais quand même.

Pour en revenir à Go, d'après ce que je comprends, les faits suivants sont valables à propos des types Go stdlib qui encapsulent les descripteurs de fichiers et les sockets:

  • Ils sont sûrs pour une utilisation simultanée par eux-mêmes (je veux dire, au niveau Go).
  • Ils «mappent» Writeet Readappellent de 1 à 1 l'objet sous-jacent, c'est-à-dire qu'un Writeappel n'est jamais divisé en deux appels système sous-jacents ou plus, et un Readappel ne renvoie jamais de données «collées» à partir des résultats de plusieurs appels système sous-jacents. (À propos, les gens se font parfois trébucher par ce comportement sans fioritures - par exemple, voyez ceci ou cela comme des exemples.)

Donc, fondamentalement, lorsque nous considérons cela avec le fait que vous fmt.*Print*êtes libre d'appeler Writeun nombre illimité de fois par un seul appel, vos exemples qui utilisent os.Stdout, seront:

  • N'entraînez jamais une course aux données - à moins que vous n'ayez attribué à la variable os.Stdoutune implémentation personnalisée, - mais
  • Les données réellement écrites sur le FD sous-jacent seront mélangées dans un ordre imprévisible qui peut dépendre de nombreux facteurs, y compris la version et les paramètres du noyau du système d'exploitation, la version de Go utilisée pour construire le programme, le matériel et la charge sur le système.

TL; DR

  • Plusieurs appels simultanés à l' fmt.Fprint*écriture sur la même valeur "writer" reportent leur concurrence sur l'implémentation (type) du "writer".
  • Il est impossible d'avoir une course aux données avec des objets "de type fichier" fournis par le Go stdlib dans la configuration que vous avez présentée dans votre question.
  • Le vrai problème ne sera pas avec les courses de données au niveau du programme Go mais avec l'accès simultané à une seule ressource se produisant au niveau du système d'exploitation. Et là, nous ne parlons pas (généralement) de courses de données car les OS de base supportés par Go exposent des choses sur lesquelles on peut «écrire» comme des abstractions, où une véritable course aux données indiquerait éventuellement un bogue dans le noyau ou dans le pilote (et le Le détecteur de course de Go ne pourra de toute façon pas le détecter car cette mémoire ne serait pas la propriété du runtime Go qui alimente le processus).

Fondamentalement, dans votre cas, si vous avez besoin de vous assurer que les données produites par un appel particulier à fmt.Fprint*sortent comme une seule pièce contiguë au récepteur de données réel fourni par le système d'exploitation, vous devez sérialiser ces appels car le fmtpackage ne fournit aucune garantie concernant le nombre d'appels Writesur le "writer" fourni pour les fonctions qu'il exporte.
La sérialisation peut être soit externe (explicite, c'est-à-dire "prendre un verrou, appeler fmt.Fprint*, libérer le verrou") ou interne - en enveloppant le os.Stdoutdans un type personnalisé qui gérerait un verrou et en l'utilisant). Et pendant que nous y sommes, le logpackage fait exactement cela, et peut être utilisé immédiatement comme les "enregistreurs" qu'il fournit, y compris celui par défaut, permettent d'empêcher la sortie des "en-têtes de journal" (tels que l'horodatage et le nom du fichier).