并发写入标准输出线程安全吗?

下面的代码不会引发数据竞争

package main

import (
    "fmt"
    "os"
    "strings"
)

func main() {
    x := strings.Repeat(" ", 1024)
    go func() {
        for {
            fmt.Fprintf(os.Stdout, x+"aan")
        }
    }()

    go func() {
        for {
            fmt.Fprintf(os.Stdout, x+"bbn")
        }
    }()

    go func() {
        for {
            fmt.Fprintf(os.Stdout, x+"ccn")
        }
    }()

    go func() {
        for {
            fmt.Fprintf(os.Stdout, x+"ddn")
        }
    }()

    <-make(chan bool)
}

我尝试了多种长度的数据,变体https://play.golang.org/p/29Cnwqj5K30

这篇文章说它不是TS。

这封邮件并没有真正回答问题,或者我没有理解。

os和fmt 的包文档对此没有太多提及。我承认我没有挖掘这两个包的源代码来找到进一步的解释,它们对我来说太复杂了。

有哪些建议和参考

回答

我不确定它是否有资格作为一个明确的答案,但我会尝试提供一些见解。

包的F*-functionsfmt只是声明它们获取实现io.Writer接口的类型的值并调用Write它。函数本身对于并发使用是安全的——从某种意义上说,可以调用任意数量的fmt.Fwhaveter并发:包本身已经为此做好了准备,但是在 Go 中支持接口并没有说明任何关于真正类型并发的信息。

换句话说,并发可能允许或不允许的真正点被推迟到写入函数的“fmt编写器”。(还应该记住,这些fmt.*Print*函数可以Write多次调用其目的地——而不是那些由 s​​tock 包提供的函数log。)

所以,我们基本上有两种情况:

  • 的自定义实现io.Writer
  • 它的库存实现,例如包*os.File的函数产生的套接字的net包装器。

第一种情况很简单:无论实现者做了什么。

第二种情况更难:据我所知,Go 标准库对此的立场(尽管在文档中没有明确说明),因为它围绕操作系统提供的“事物”(例如文件描述符和套接字)提供的包装器是合理的“瘦”,以及它们实现的任何语义,都由运行在特定系统上的 stdlib 代码传递实现。

例如,POSIX要求write(2)调用 在常规文件或符号链接上操作时彼此之间是原子的这意味着,由于Write对包装文件描述符或套接字的任何调用实际上都会导致 tagret 系统的单个“写入”系统调用,因此您可以查阅目标操作系统的文档并了解会发生什么。

请注意,POSIX 仅说明文件系统对象,如果它os.Stdout被打开到终端(或伪终端)或管道或支持write(2)系统调用的任何其他东西,结果将取决于相关子系统和/或驱动程序实现——例如,来自多个并发调用的数据可能散布,或者其中一个调用或两者都可能被操作系统失败——不太可能,但仍然如此。

回到 Go,根据我收集的信息,以下事实适用于封装文件描述符和套接字的 Go stdlib 类型:

  • 它们自己并发使用是安全的(我的意思是,在 Go 级别)。
  • 它们“映射”Write并以Read一对一的方式调用底层对象——也就是说,一个Write调用永远不会分成两个或多个底层系统调用,并且一个Read调用永远不会返回从多个底层系统调用的结果“粘合”的数据。(顺便说一句,人们偶尔会被这种朴实无华的行为绊倒——例如,以这个或这个为例。)

所以基本上,当我们考虑到这一点时,每次fmt.*Print*调用可以自由调用Write任意次数,您使用 的示例os.Stdout将:

  • 永远不会导致数据竞争——除非你已经为变量分配了os.Stdout一些自定义实现——但是
  • 实际写入底层 FD 的数据将以不可预测的顺序混合,这可能取决于许多因素,包括操作系统内核版本和设置、用于构建程序的 Go 版本、硬件和系统负载。

TL; 博士

  • 多个并发调用fmt.Fprint*写入相同的“编写器”值将它们的并发性推迟到“编写器”的实现(类型)。
  • 在您在问题中提出的设置中,不可能与 Go stdlib 提供的“类文件”对象进行数据竞争。
  • 真正的问题不在于 Go 程序级别上的数据竞争,而在于操作系统级别上发生的对单个资源的并发访问。在那里,我们(通常)不谈论数据竞争,因为 Go 支持的商品操作系统将人们可能“写入”的东西暴露为抽象,其中真正的数据竞争可能表明内核或驱动程序中的错误(以及Go 的竞争检测器无论如何都无法检测到它,因为该内存不会归为该进程提供动力的 Go 运行时所有)。

基本上,在您的情况下,如果您需要确保任何特定调用产生的数据fmt.Fprint*作为操作系统提供的实际数据接收器的单个连续部分出现,您需要序列化这些调用,因为fmt包不提供关于Write为其导出的函数调用提供的“编写器”的次数。
序列化可以是外部的(明确的,即“获取锁,调用fmt.Fprint*,释放锁”)或内部的——通过将 包装os.Stdout在将管理锁的自定义类型中,并使用它)。当我们在做的时候,log 包就是这样做的,并且可以直接用作它提供的“记录器”,包括默认的记录器,允许禁止输出“日志头”(例如时间戳和文件名)。


以上是并发写入标准输出线程安全吗?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>