skip to Main Content

Is it thread safe, when two Goroutines writes to file concurrently by os.File.Write()?

According to this question Is os.File’s Write() threadsafe?, it isn’t thread safe. However, the output file ./test.txt of the following code didn’t occur errors.

And according to this question Safe to have multiple processes writing to the same file at the same time? [CentOs 6, ext4], the POSIX "raw" IO syscalls are thread safe. os.File.Write() uses the POSIX IO syscalls, so can we say it is thread safe?

package main

import (
    "fmt"
    "os"
    "sync"
)

func main() {
    filePath := "./test.txt"

    var wg sync.WaitGroup
    wg.Add(2)

    worker := func(name string) {
        // file, _ := os.Create(filePath)
        file, _ := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE, 0666)
        defer file.Close()
        defer wg.Done()
        for i := 0; i < 100000; i++ {
            if _, err := file.Write([]byte(name + ": is os.File.Write() thread safe?n")); err != nil {
                fmt.Println(err)
            }
        }
    }

    go worker("worker1")
    go worker("worker2")

    wg.Wait()
}

2

Answers


  1. Documentation does not explicitly say it is thread safe.

    Looking at Go 1.16.5 version source code though:

    // Write implements io.Writer.
    func (fd *FD) Write(buf []byte) (int, error) {
        if err := fd.writeLock(); err != nil {
            return 0, err
        }
        defer fd.writeUnlock()
        ...
    

    It uses internal synchronization. Unless you’re coding a mars lander I’d say it’s fine to assume writes are thread safe.

    Login or Signup to reply.
  2. In general, you should not expect that Write calls to an io.Writer will be written out atomically, i.e. all at once. Synchronization at a higher level is recommended if you don’t want interleaved outputs.

    Even if you can assume that for *os.File each call to Write will be written out atomically because of either internal locking or because it’s a single system call, there is no guarantee that whatever is using the file will do so. For example:

    fmt.Fprintf(f, "[%s] %sn", date, message)
    

    The fmt library does not guarantee that this will make a single call to the io.Writer. It may, for example, flush [, then the date, then ] then the message, and then n separately, which could result in two log messages being interleaved.

    Practically speaking, writes to *os.File will probably be atomic, but it is difficult to arrange for this to be useful without incurring significant allocations, and making this assumption might compromise the portability of your application to different operating systems or architectures, or even different environments. It is possible, for example, that your binary compiled to WASM will not have the same behavior, or that your binary when writing to an NFS-backed file will behave differently.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search