Tac say

只想做个程序演奏家

GO exec.command.Wait 执行后台程序,在重定向输出时卡住

在GO上发现以下现象

c := exec.Command("sh", "-c", "sleep 100 &")
var b bytes.Buffer
c.Stdout = &b

if e := c.Start(); nil != e {
    fmt.Printf("ERROR: %v\n", e)
}
if e := c.Wait(); nil != e {
    fmt.Printf("ERROR: %v\n", e)
}

这个代码会一直等到sleep 100完成后才退出, 与常识不符.

但去掉Stdout重定向后, 代码就不会等待卡住

c := exec.Command("sh", "-c", "sleep 100 &")
if e := c.Start(); nil != e {
    fmt.Printf("ERROR: %v\n", e)
}
if e := c.Wait(); nil != e {
    fmt.Printf("ERROR: %v\n", e)
}

在运行时打出stacktrace, 再翻翻GO的源代码, 发现GO卡在以下代码

func (c *Cmd) Wait() error {
    ...
    state, err := c.Process.Wait()
    ...
    var copyError error
    for _ = range c.goroutine {
        if err := <-c.errch; err != nil && copyError == nil {
            copyError = err
        }
    }
    ...
}

可以看到Wait()在等待Process结束后, 还等待了所有c.goroutinec.errch信号. 参看以下代码:

func (c *Cmd) stdout() (f *os.File, err error) {
    return c.writerDescriptor(c.Stdout)
}

func (c *Cmd) writerDescriptor(w io.Writer) (f *os.File, err error) {
    ...
    c.goroutine = append(c.goroutine, func() error {
        _, err := io.Copy(w, pr)
        return err
    })
    ...
}

重定向stdout时, 会添加一个监听任务到goroutine (stderr也是同理)

结论是由于将sleep 100放到后台执行, 其进程stdout并没有关闭, io.Copy()不会返回, 所以会卡住

临时的解决方法就是将后台进程的stdoutstderr重定向出去, 以下代码不会卡住:

c := exec.Command("sh", "-c", "sleep 100 >/dev/null 2>/dev/null &")
var b bytes.Buffer
c.Stdout = &b

if e := c.Start(); nil != e {
    fmt.Printf("ERROR: %v\n", e)
}
if e := c.Wait(); nil != e {
    fmt.Printf("ERROR: %v\n", e)
}

已经报了bug

但想不出好的GO代码的修改方案

Comments