从外部命令读取错误:致命错误,所有goroutine都处于睡眠状态-死锁

时间:2018-10-06 14:46:23

标签: go concurrency

我想用Python将mime / multipart消息写到标准输出,并使用mime/multipart包在Golang中读取该消息。这只是一个学习练习。

我尝试模拟this example

output.py

#!/usr/bin/env python2.7
import sys
s = "--foo\r\nFoo: one\r\n\r\nA section\r\n" +"--foo\r\nFoo: two\r\n\r\nAnd another\r\n" +"--foo--\r\n"
print s 

main.go

package main

import (
    "io"
    "os/exec"
    "mime/multipart"
    "log"
    "io/ioutil"
    "fmt"
    "sync"
)

var wg sync.WaitGroup

func main() {
    pr,pw := io.Pipe()
    defer pw.Close()

    cmd := exec.Command("python","output.py")
    cmd.Stdout = pw

    mr := multipart.NewReader(pr,"foo")

    wg.Add(1)
    go func() {
        defer wg.Done()
        for {
            p, err := mr.NextPart()
            if err == io.EOF {
                fmt.Println("EOF")
                return
            }
            if err != nil {
                log.Fatal(err)
            }
            slurp, err := ioutil.ReadAll(p)
            if err != nil {
                log.Fatal(err)
            }
            fmt.Printf("Part : %q\n", slurp)
            return
        }
    }()

    if err := cmd.Start(); err != nil {
        log.Fatal(err)
    }

    cmd.Wait()
    wg.Wait()
}

go run main.go的输出:

fatal error: all goroutines are asleep - deadlock!

有关StackOverflow上此主题的其他答案与通道未关闭有关,但我什至没有使用通道。我了解到某个地方存在无限循环或类似现象,但我看不到它。

1 个答案:

答案 0 :(得分:0)

尝试类似的操作(在下面的说明中):

package main

import (
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "mime/multipart"
    "os"
    "os/exec"
    "sync"

    "github.com/pkg/errors"
)

func readCommand(cmdStdout io.ReadCloser, wg *sync.WaitGroup, resc chan<- []byte, errc chan<- error) {
    defer wg.Done()
    defer close(errc)
    defer close(resc)

    mr := multipart.NewReader(cmdStdout, "foo")

    for {
        part, err := mr.NextPart()
        if err != nil {
            if err == io.EOF {
                fmt.Println("EOF")
            } else {
                errc <- errors.Wrap(err, "failed to get next part")
            }

            return
        }

        slurp, err := ioutil.ReadAll(part)
        if err != nil {
            errc <- errors.Wrap(err, "failed to read part")
            return
        }

        resc <- slurp
    }
}

func main() {
    cmd := exec.Command("python", "output.py")
    cmd.Stderr = os.Stderr
    pr, err := cmd.StdoutPipe()
    if err != nil {
        log.Fatal(err)
    }

    var wg sync.WaitGroup
    wg.Add(1)

    resc := make(chan []byte)
    errc := make(chan error)
    go readCommand(pr, &wg, resc, errc)

    if err := cmd.Start(); err != nil {
        log.Fatal(err)
    }

    for {
        select {
        case err, ok := <-errc:
            if !ok {
                errc = nil
                break
            }

            if err != nil {
                log.Fatal(errors.Wrap(err, "error from goroutine"))
            }

        case res, ok := <-resc:
            if !ok {
                resc = nil
                break
            }

            fmt.Printf("Part from goroutine: %q\n", res)
        }

        if errc == nil && resc == nil {
            break
        }
    }

    cmd.Wait()
    wg.Wait()
}

无特殊顺序:

  • 不是使用io.Pipe()作为命令的Stdout,而是询问命令的StdoutPipe()cmd.Wait()将确保它为您关闭。
  • cmd.Stderr设置为os.Stderr,以便可以看到Python程序生成的错误。
    • 我注意到,只要Python程序写入标准错误,该程序就会挂起。现在没有了:)
  • 不要将WaitGroup设置为全局变量;将对它的引用传递给goroutine。
  • 而不是在goroutine中log.Fatal()进入,而是创建一个错误通道以将错误传达回main()
  • 创建结果通道以将结果传达回main(),而不是在goroutine中打印结果。
  • 确保通道已关闭,以防止阻塞/蛋氨酸泄漏。
  • 将goroutine分离为适当的函数,以使代码更易于阅读和遵循。
  • 在此示例中,我们可以在goroutine中创建multipart.Reader(),因为这是我们代码中唯一使用它的部分。
  • 请注意,我正在使用Wrap()包中的errors将上下文添加到错误消息中。当然,这与您的问题无关,但是是个好习惯。

for { select { ... } }部分可能令人困惑。 This是我发现的介绍这一概念的文章。基本上,select允许我们从当前可读的这两个通道(rescerrc)中读取任何一个,然后在关闭通道时将它们分别设置为nil。当两个通道均为nil时,循环退出。这样一来,我们就可以处理“结果或错误”。

编辑:作为johandalabacka said on the Golang Forum,看来这里的主要问题是Windows上的Python向输出添加了额外的\r,问题是您的Python程序应省略输出字符串中的\rsys.stdout.write()而不是print()。输出也可以在Golang端进行清理,但是,除了无法在不修改Python端的情况下正确解析之外,此答案仍将改善程序的并发机制。