并发写入文件

Concurrent writing to a file

在go中,如何控制对文本文件的并发写入?

我问这个是因为我将有多个 goroutines 使用相同的文件处理程序写入文本文件。

我写了这段代码来尝试看看会发生什么,但我不确定我是否做到了 "right":

package main

import (
    "os"
    "sync"
    "fmt"
    "time"
    "math/rand"
    "math"
)


func WriteToFile( i int, f *os.File, w *sync.WaitGroup ){
    //sleep for either 200 or 201 milliseconds
    randSleep := int( math.Floor( 200 + ( 2 * rand.Float64() ) ) )
    fmt.Printf( "Thread %d waiting %d\n", i, randSleep )
    time.Sleep( time.Duration(randSleep) * time.Millisecond )

    //write to the file
    fmt.Fprintf( f, "Printing out: %d\n", i )
    //write to stdout
    fmt.Printf( "Printing out: %d\n", i )
    w.Done()
}

func main() {
    rand.Seed( time.Now().UnixNano() )

    d, err := os.Getwd()
    if err != nil {
        fmt.Println( err )
    }
    filename := d + "/log.txt"

    f, err := os.OpenFile( filename, os.O_CREATE | os.O_WRONLY | os.O_TRUNC, 0666 )

    if err != nil {
        fmt.Println( err )
    }
    var w *sync.WaitGroup = new(sync.WaitGroup)
    w.Add( 10 )

    //start 10 writers to the file
    for i:=1; i <= 10; i++ {
        go WriteToFile( i, f, w )
    }

    //wait for writers to finish
    w.Wait()

}

我有一半希望输出会在文件中显示类似这样的内容,而不是我得到的连贯输出:

Printing Printing out: 2
out: 5
Poriuntitng: 6

基本上,由于缺乏同步,我预计角色会不连贯地出现并交织在一起。难道我没有写代码来哄骗这种行为吗?或者在调用 fmt.Fprintf 期间是否有某种机制同步写入?

有很多方法可以控制并发访问。最简单的是使用 Mutex:

var mu sync.Mutex

func WriteToFile( i int, f *os.File, w *sync.WaitGroup ){
    mu.Lock()
    defer mu.Unlock()
    // etc...
}

至于为什么你没有看到问题,Go 使用操作系统调用来实现文件访问,这些系统调用是 thread safe(强调):

According to POSIX.1-2008/SUSv4 Section XSI 2.9.7 ("Thread Interactions with Regular File Operations"):

All of the following functions shall be atomic with respect to each other in the effects specified in POSIX.1-2008 when they operate on regular files or symbolic links: ...

随后列出的 API 包括 write() 和 writev(2)。和 在跨线程应该是原子的效果中(和 进程)是文件偏移量的更新。然而,在 Linux 之前 3.14 版,情况并非如此:如果两个进程共享一个 打开文件描述(参见 open(2)) 执行 write()(或 writev(2)) 同时, I/O 操作不是原子的 尊重更新文件偏移量,结果是块 两个进程输出的数据可能(错误地)重叠。 这个 问题已在 Linux 3.14.

中修复

虽然我仍然会使用锁,因为 Go 代码不是自动线程安全的。 (两个goroutine修改同一个变量会导致奇怪的行为)

控制并发访问的一种简单方法是通过服务 goroutine,从通道接收消息。这个 goroutine 将拥有对该文件的唯一访问权限。因此访问将是顺序的,没有任何竞争问题。

频道在交错请求方面做得很好。客户端写入通道而不是直接写入文件。频道上的消息会自动为您插入。

与简单地使用 Mutex 相比,这种方法的好处是您可以开始将程序视为微服务的集合。这是 CSP 方式,可以轻松地从较小的组件组​​成大型系统。