go

is concurrent write on stdout threadsafe?


below code does not throw a data race

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

I tried multiple length of data, with variant https://play.golang.org/p/29Cnwqj5K30

This post says it is not TS.

This mail does not really answer the question, or I did not understand.

Package documentation of os and fmt dont mention much about this. I admit i did not dig the source code of those two packages to find further explanations, they appear too complex to me.

What are the recommendations and their references ?


Solution

  • I'm not sure it would qualify as a definitive answer but I'll try to provide some insight.

    The F*-functions of the fmt package merely state they take a value of a type implementing io.Writer interface and call Write on it. The functions themselves are safe for concurrent use — in the sense it's OK to call any number of fmt.Fwhaveter concurrently: the package itself is prepared for that, but when it comes to concurrently writing to the same value of a type implementing io.Writer, the question becomes more complex because supporting of an interface in Go does not state anything about the real type concurrency-wise.

    In other words, the real point of where the concurrency may or may not be allowed is deferred to the "writer" which the functions of fmt write to. (One should also keep in mind that the fmt.*Print* functions are allowed to call Write on its destination any number of times during a single invocation, in a row, — as opposed to those provided by the stock package log.)

    So, we basically have two cases:

    The first case is the simple one: whatever the implementor did.

    The second case is harder: as I understand, the Go standard library's stance on this (albeit not clearly stated in the docs) is that the wrappers it provides around "things" provided by the OS—such as file descriptors and sockets—are reasonably "thin", and hence whatever semantics they implement, is transitively implemented by the stdlib code running on a particular system.

    For instance, POSIX requires that write(2) calls are atomic with regard to one another when they are operating on regular files or symbolic links. This means, since any call to Write on things wrapping file descriptors or sockets actually results in a single "write" syscall of the target system, you might consult the docs of the target OS and get the idea of what will happen.

    Note that POSIX only tells about filesystem objects, and if os.Stdout is opened to a terminal (or a pseudo-terminal) or to a pipe or to anything else which supports the write(2) syscall, the results will depend on what the relevant subsystem and/or the driver implement—for instance, data from multiple concurrent calls may be interspersed, or one of the calls, or both, may just be failed by the OS—unlikely, but still.

    Going back to Go, from what I gather, the following facts hold true about the Go stdlib types which wrap file descriptors and sockets:

    So basically when we consider this with the fact fmt.*Print* are free to call Write any number of times per a single call, your examples which use os.Stdout, will:


    TL;DR

    Basically, in your case, if you need to be sure the data produced by any particular call to fmt.Fprint* comes out as a single contiguous piece to the actual data receiver provided by the OS, you need to serialize these calls as the fmt package provides no guarantees regarding the number of calls to Write on the supplied "writer" for the functions it exports.
    The serialization may either be external (explicit, that is "take a lock, call fmt.Fprint*, release the lock") or internal — by wrapping the os.Stdout in a custom type which would manage a lock, and using it). And while we're at it, the log package does just that, and can be used straight away as the "loggers" it provides, including the default one, allow to inhibit outputting of "log headers" (such as the timestamp and the name of the file).