对exec.Cmd.CombinedOutput的一点研究
CombinedOutput会自动设置命令的stdout和stderr为内存内的缓冲区,然后等待进程运行结束。其相关代码如下:
1 2 3 4 5 6 7 8 9 10 |
|
这段代码虽然看起来十分简单,但是实际上极其容易造成一种误解:把stdout和stderr设置成“同一个Writer”是安全的。
所以我也在我自己的代码中写出了类似下面这样看起来很像又不完全像的代码:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
在一台全新的服务器上,给出了如下的运行结果:时而正常、时而崩溃。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
这让我很费解。按照正常理解,stdout和stderr在进程内是两个不同的描述符,对应到两个文件,也就意味着:它们可以被多线程同时写。所以,上述写法一定是线程不安全的,会造成数据竞态。但是Go语言本身就能把它们设置为同一个Write呢?而我,只是模仿Go的标准库写了差不多的代码。
如果再去看看Cmd.Stdout/Stderr的文档,有下面的说明:
1 2 3 4 5 6 |
|
这里的文档正好解释了上面的问题:如果是同一个Writer且==,Write同一时刻只会被一个goroutine调用。这就保证了数据的安全。但这是如何保证的呢?
当Stdout和Stderr设置到同一个Writer时,下面的Go语言代码保证了只会创建唯一一个共享的os.File:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
而os.File的Write方法又调用了poll.FD.Write方法:
1 2 3 4 5 6 7 |
|
poll.FD内部一进来就对写操作进行了加锁:
1 2 3 4 5 6 7 |
|
所以综合各种以上措施后,把stdout和stderr设置成完全相同的同一个Writer是安全的。否则,应该自行加锁。