¿Es segura la escritura simultánea en stdout thread?
el siguiente código no lanza una carrera de datos
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)
}
Probé múltiples longitudes de datos, con variante https://play.golang.org/p/29Cnwqj5K30
Esta publicación dice que no es TS.
Este correo realmente no responde a la pregunta, o no la entendí.
La documentación del paquete de os y fmt no menciona mucho sobre esto. Admito que no indagué en el código fuente de esos dos paquetes para encontrar más explicaciones, me parecen demasiado complejos.
¿Cuáles son las recomendaciones y sus referencias ?
Respuestas
No estoy seguro de que califique como una respuesta definitiva, pero intentaré proporcionar una idea.
Las F*
-funciones del fmt
paquete simplemente indican que toman un valor de un tipo de io.Writer
interfaz de implementación y lo invocan Write
. Las funciones en sí mismas son seguras para el uso simultáneo, en el sentido de que está bien llamar a cualquier número de forma fmt.Fwhaveter
simultánea: el paquete en sí está preparado para eso, pero el soporte de una interfaz en Go no indica nada sobre la concurrencia de tipo real.
En otras palabras, el punto real en el que la concurrencia puede o no estar permitida se le confiere al "escritor" al que pertenecen las funciones de fmt
escritura. (También se debe tener en cuenta que las fmt.*Print*
funciones pueden llamar Write
a su destino cualquier número de veces, a diferencia de las proporcionadas por el paquete de acciones log
).
Entonces, básicamente tenemos dos casos:
- Implementaciones personalizadas de
io.Writer
. - Implementaciones de stock de la misma, como
*os.File
o envoltorios alrededor de sockets producidos por las funciones denet
paquete.
El primer caso es el simple: cualquier cosa que haya hecho el implementador.
El segundo caso es más difícil: según tengo entendido, la postura de la biblioteca estándar de Go sobre esto (aunque no se indique claramente en los documentos) en el sentido de que las envolturas que proporciona alrededor de "cosas" proporcionadas por el sistema operativo, como descriptores de archivos y sockets, son razonablemente "thin", y por lo tanto, cualquier semántica que implementen, se implementa transitivamente mediante el código stdlib que se ejecuta en un sistema en particular.
Por ejemplo, POSIX requiere que las write(2)llamadas sean atómicas entre sí cuando operan en archivos regulares o enlaces simbólicos. Esto significa que, dado que cualquier llamada a Write
cosas que envuelven descriptores de archivos o sockets en realidad da como resultado una única llamada al sistema de "escritura" del sistema tagret, puede consultar los documentos del sistema operativo de destino y hacerse una idea de lo que sucederá.
Tenga en cuenta que POSIX solo informa sobre los objetos del sistema de archivos, y si os.Stdout
se abre a una terminal (o una pseudo-terminal) oa una tubería o cualquier otra cosa que admita la write(2)
llamada al sistema, los resultados dependerán de lo que sea el subsistema relevante y / o el controlador. implementar, por ejemplo, los datos de múltiples llamadas simultáneas pueden estar intercalados, o una de las llamadas, o ambas, puede simplemente fallar por el sistema operativo, es poco probable, pero aún así.
Volviendo a Go, por lo que he recopilado, los siguientes hechos son ciertos sobre los tipos de stdlib de Go que envuelven los descriptores de archivo y los sockets:
- Son seguros para uso simultáneo por sí mismos (quiero decir, en el nivel Go).
- Ellos "mapean"
Write
yRead
llaman 1 a 1 al objeto subyacente, es decir, unaWrite
llamada nunca se divide en dos o más llamadas al sistema subyacentes, y unaRead
llamada nunca devuelve datos "pegados" de los resultados de múltiples llamadas al sistema subyacentes. (Por cierto, las personas ocasionalmente se tropiezan con este comportamiento sencillo; por ejemplo, vea esto o esto como ejemplos).
Entonces, básicamente, cuando consideramos esto con el hecho de que fmt.*Print*
somos libres de llamar Write
cualquier número de veces por una sola llamada, sus ejemplos que usan os.Stdout
:
- Nunca resulte en una carrera de datos, a menos que haya asignado a la variable
os.Stdout
alguna implementación personalizada, pero - Los datos realmente escritos en el FD subyacente se mezclarán en un orden impredecible que puede depender de muchos factores, incluida la versión y la configuración del kernel del sistema operativo, la versión de Go utilizada para compilar el programa, el hardware y la carga en el sistema.
TL; DR
- Varias llamadas simultáneas para
fmt.Fprint*
escribir al mismo valor de "escritor" difieren su concurrencia a la implementación (tipo) del "escritor". - Es imposible tener una carrera de datos con objetos "similares a archivos" proporcionados por Go stdlib en la configuración que ha presentado en su pregunta.
- El problema real no será con las carreras de datos en el nivel del programa Go, sino con el acceso simultáneo a un solo recurso que ocurre en el nivel del sistema operativo. Y allí, no hablamos (generalmente) de carreras de datos porque los sistemas operativos básicos que soporta el sistema operativo Go exponen cosas en las que uno puede "escribir" como abstracciones, donde una carrera de datos real posiblemente indicaría un error en el kernel o en el controlador (y el El detector de carreras de Go no podrá detectarlo de todos modos, ya que esa memoria no sería propiedad del tiempo de ejecución de Go que alimenta el proceso).
Básicamente, en su caso, si necesita asegurarse de que los datos producidos por cualquier llamada en particular a fmt.Fprint*
salgan como una sola pieza contigua al receptor de datos real proporcionado por el sistema operativo, debe serializar estas llamadas, ya que el fmt
paquete no ofrece garantías con respecto a el número de llamadas al Write
"escritor" proporcionado para las funciones que exporta.
La serialización puede ser externa (explícita, es decir, "tomar un bloqueo, llamar fmt.Fprint*
, liberar el bloqueo") o interna, envolviendo el os.Stdout
en un tipo personalizado que administraría un bloqueo y usándolo). Y ya que estamos en ello, el log
paquete hace precisamente eso, y se puede utilizar de inmediato, ya que los "registradores" que proporciona, incluido el predeterminado, permiten inhibir la salida de "encabezados de registro" (como la marca de tiempo y el nombre del archivo).