Golang中读取外部命令时出现致命错误:所有goroutines均处于休眠状态 - 死锁问题
Golang中读取外部命令时出现致命错误:所有goroutines均处于休眠状态 - 死锁问题
我想用Python编写一个MIME/多部分消息到标准输出,然后在Golang中使用mime/multipart包来读取这个消息。这只是一个学习练习。
我尝试模拟这个示例
我在StackOverflow上问了同样的问题:链接
#!/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
我明天试试。现在该睡觉了。
更多关于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()
}
主要修改:
- 在
readCommand函数中移除了通道关闭操作,改为在主goroutine中处理 - 添加了一个新的goroutine来等待
readCommand完成,然后关闭通道 - 正确处理
io.EOF错误,不将其发送到错误通道
这个版本应该能正确处理MIME多部分消息而不会出现死锁。当Python脚本输出完整的MIME消息时,multipart读取器会正确解析各个部分,主程序会依次打印每个部分的内容。


