【发布时间】:2013-08-26 15:13:45
【问题描述】:
TL;DR:请到最后一部分,告诉我你将如何解决这个问题。
我今天早上开始使用来自 Python 的 Go。我想从 Go 调用一个闭源可执行文件多次,具有 bit 的并发性,具有不同的命令行参数。我生成的代码运行良好,但我想获得您的意见以改进它。由于我处于早期学习阶段,我还将解释我的工作流程。
为了简单起见,这里假设这个“外部闭源程序”是zenity,一个可以从命令行显示图形消息框的Linux命令行工具。
从 Go 调用可执行文件
所以,在 Go 中,我会这样:
package main
import "os/exec"
func main() {
cmd := exec.Command("zenity", "--info", "--text='Hello World'")
cmd.Run()
}
这应该可以正常工作。请注意,.Run() 的功能等同于.Start(),后跟.Wait()。这很好,但如果我只想执行这个程序一次,整个编程的东西就不值得了。所以让我们多次这样做。
多次调用可执行文件
既然我已经完成了这项工作,我想使用自定义命令行参数多次调用我的程序(为了简单起见,这里只是 i)。
package main
import (
"os/exec"
"strconv"
)
func main() {
NumEl := 8 // Number of times the external program is called
for i:=0; i<NumEl; i++ {
cmd := exec.Command("zenity", "--info", "--text='Hello from iteration n." + strconv.Itoa(i) + "'")
cmd.Run()
}
}
好的,我们做到了!但是我仍然看不到 Go 相对于 Python 的优势……这段代码实际上是以串行方式执行的。我有一个多核 CPU,我想利用它。所以让我们用 goroutines 添加一些并发性。
Goroutines,或者一种让我的程序并行的方法
a) 第一次尝试:只需在任何地方添加“go”
让我们重写我们的代码,让事情更容易调用和重用,并添加著名的go 关键字:
package main
import (
"os/exec"
"strconv"
)
func main() {
NumEl := 8
for i:=0; i<NumEl; i++ {
go callProg(i) // <--- There!
}
}
func callProg(i int) {
cmd := exec.Command("zenity", "--info", "--text='Hello from iteration n." + strconv.Itoa(i) + "'")
cmd.Run()
}
没什么!问题是什么?所有的 goroutines 都会立即执行。我真的不知道为什么没有执行zenity,但是AFAIK,Go程序在zenity外部程序甚至可以初始化之前就退出了。使用time.Sleep 证实了这一点:等待几秒钟就足以让 8 个 zenity 实例自行启动。我不知道这是否可以被认为是一个错误。
更糟糕的是,我真正想要调用的真正程序需要一段时间才能自行执行。如果我在我的 4 核 CPU 上并行执行该程序的 8 个实例,会浪费一些时间进行大量上下文切换……我不知道普通 Go goroutine 的行为如何,但 exec.Command 会 在 8 个不同的线程中启动 zenity 8 次。更糟糕的是,我想执行这个程序超过 100,000 次。在 goroutines 中一次完成所有这些工作根本不会有效。不过,我还是想利用我的 4 核 CPU!
b) 第二次尝试:使用 goroutine 池
在线资源倾向于推荐使用sync.WaitGroup 进行此类工作。这种方法的问题在于,您基本上是在处理一批 goroutine:如果我创建了 4 个成员的 WaitGroup,Go 程序将等待 所有 4 个外部程序完成,然后再调用一个新的批处理4 个程序。这效率不高:又一次浪费了 CPU。
其他一些资源建议使用缓冲通道来完成这项工作:
package main
import (
"os/exec"
"strconv"
)
func main() {
NumEl := 8 // Number of times the external program is called
NumCore := 4 // Number of available cores
c := make(chan bool, NumCore - 1)
for i:=0; i<NumEl; i++ {
go callProg(i, c)
c <- true // At the NumCoreth iteration, c is blocking
}
}
func callProg(i int, c chan bool) {
defer func () {<- c}()
cmd := exec.Command("zenity", "--info", "--text='Hello from iteration n." + strconv.Itoa(i) + "'")
cmd.Run()
}
这看起来很难看。频道不是为此目的而设计的:我正在利用副作用。我喜欢defer 的概念,但我讨厌必须声明一个函数(甚至是 lambda)才能从我创建的虚拟通道中弹出一个值。哦,当然,使用虚拟通道本身就是丑陋的。
c) 第三次尝试:当所有孩子都死了时死
现在我们快完成了。我只需要考虑另一个副作用:Go 程序在所有 zenity 弹出窗口关闭之前关闭。这是因为当循环结束时(第 8 次迭代),没有什么能阻止程序结束。这一次,sync.WaitGroup 会很有用。
package main
import (
"os/exec"
"strconv"
"sync"
)
func main() {
NumEl := 8 // Number of times the external program is called
NumCore := 4 // Number of available cores
c := make(chan bool, NumCore - 1)
wg := new(sync.WaitGroup)
wg.Add(NumEl) // Set the number of goroutines to (0 + NumEl)
for i:=0; i<NumEl; i++ {
go callProg(i, c, wg)
c <- true // At the NumCoreth iteration, c is blocking
}
wg.Wait() // Wait for all the children to die
close(c)
}
func callProg(i int, c chan bool, wg *sync.WaitGroup) {
defer func () {
<- c
wg.Done() // Decrease the number of alive goroutines
}()
cmd := exec.Command("zenity", "--info", "--text='Hello from iteration n." + strconv.Itoa(i) + "'")
cmd.Run()
}
完成。
我的问题
- 你知道任何其他适当的方法来限制一次执行的 goroutine 的数量吗?
我不是指线程; Go 内部如何管理 goroutine 无关紧要。我的意思是限制一次启动的 goroutine 的数量:exec.Command 每次调用都会创建一个新线程,所以我应该控制它被调用的次数。
- 你觉得这段代码不错吗?
- 您知道在这种情况下如何避免使用虚拟通道吗?
我无法说服自己这样的虚拟渠道是可行的方法。
【问题讨论】:
标签: multithreading go goroutine