Golang中读取外部命令时出现致命错误:所有goroutines均处于休眠状态 - 死锁问题

Golang中读取外部命令时出现致命错误:所有goroutines均处于休眠状态 - 死锁问题 我想用Python编写一个MIME/多部分消息到标准输出,然后在Golang中使用mime/multipart包来读取这个消息。这只是一个学习练习。

我尝试模拟这个示例

我在StackOverflow上问了同样的问题:链接

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 (
    "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()
}

它给我这个错误:goroutine错误:获取下一个部分失败:multipart: NextPart: EOF 退出状态 1 请帮助我理解这个概念并让这个示例程序正常工作。


更多关于Golang中读取外部命令时出现致命错误:所有goroutines均处于休眠状态 - 死锁问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

11 回复

我明天试试。现在该睡觉了。

更多关于Golang中读取外部命令时出现致命错误:所有goroutines均处于休眠状态 - 死锁问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


嗯。如果不关闭通道会有什么不同吗?也许它们在主 Go 协程能够读取之前就已经关闭了。

哇,太感谢了。这个方法真的有效。你无法想象我有多开心。你太棒了!!

再次感谢

无法使用 CombinedOutput() 方法,因为它会返回字节数组,而我无法用它来创建 io.Reader,因此无法使用 Nextpart 逐部分获取消息。

要恢复标准输出和标准错误,使用 o ,e := cmd.CombinedOutput() 似乎就足够了,因为您需要等待命令结束。使用管道似乎过于复杂。

func main() {
    fmt.Println("hello world")
}

您可以尝试将运行命令和获取输出流的逻辑放在go协程内部。这样安排是合理的,因为不能让两个或更多go协程同时从同一个命令输出中读取数据(至少这会导致异常现象)

我遇到了通过通道传递的EOF错误,因此通道工作正常。但存在其他问题。我从Golang示例中复制粘贴了MIME消息,所以我认为这部分没有错误。

你能在你的电脑上尝试重现这个问题吗?

我在Windows 10 64位系统上,使用的Go版本是go1.11.1 windows/amd64。我原以为Stack Overflow上的那位用户肯定是在用Linux系统,于是我用Linux Mint启动盘进行了测试。令人惊讶的是,代码在Linux环境下正常运行,但我必须在Windows上让它也能工作。

Go语言应该是跨平台的,为什么会出现这种行为?

我在readCommand()函数中添加了exec.Command()。

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

    cmd := exec.Command("python", "output.py")
    cmd.Stderr = os.Stderr
    pr, err := cmd.StdoutPipe()
    if err != nil {
        log.Fatal(err)
    }
    if err := cmd.Start(); err != nil {
        log.Fatal(err)
    }
mr := multipart.NewReader(pr, "foo")

/ * other stuff */
cmd.Wait()
}

但这仍然不起作用。

引用 nekkoMaster 的发言:

func readCommand(wg *sync.WaitGroup, resc chan<- byte, errc chan<- error) {
    defer wg.Done()
    defer close(errc)
    defer close(resc)
    cmd := exec.Command("python", "output.py")
    cmd.Stderr = os.Stderr
    pr, err := cmd.StdoutPipe()
    if err != nil {
        log.Fatal(err)
    }
    if err := cmd.Start(); err != nil {
        log.Fatal(err)
    }
    mr := multipart.NewReader(pr, "foo")
    /* 其他内容 */
    cmd.Wait()
}

现在测试后发现问题是Python导致的。默认情况下,Python似乎会为输出到标准输出的每个\n额外添加一个\r。这是为了在Windows系统上补偿行尾是\r\n而不是\n的情况。所以output.py的输出结果是:

“–foo\r\r\nFoo: one\r\r\n\r\r\nA section\r\r\n–foo\r\r\nFoo: two\r\r\n\r\r\nAnd another\r\r\n–foo–\r\r\n”

你必须找到一种方法让Python以原始格式输出字符串,或者直接在Python程序中使用\n

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

在您的代码中,死锁问题的根本原因是读取goroutine在遇到io.EOF错误时过早关闭了通道,而主goroutine仍在等待从这些通道接收数据。以下是修复后的代码:

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()

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

    for {
        part, err := mr.NextPart()
        if err == io.EOF {
            return
        }
        if err != nil {
            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)
    }

    go func() {
        wg.Wait()
        close(resc)
        close(errc)
    }()

    for {
        select {
        case err, ok := <-errc:
            if !ok {
                errc = nil
            } else if err != nil {
                log.Fatal(errors.Wrap(err, "error from goroutine"))
            }
        case res, ok := <-resc:
            if !ok {
                resc = nil
            } else {
                fmt.Printf("Part from goroutine: %q\n", res)
            }
        }

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

    cmd.Wait()
}

主要修改:

  1. readCommand函数中移除了通道关闭操作,改为在主goroutine中处理
  2. 添加了一个新的goroutine来等待readCommand完成,然后关闭通道
  3. 正确处理io.EOF错误,不将其发送到错误通道

这个版本应该能正确处理MIME多部分消息而不会出现死锁。当Python脚本输出完整的MIME消息时,multipart读取器会正确解析各个部分,主程序会依次打印每个部分的内容。

回到顶部