TL; TR:请转到最后一部分告诉我你将如何解决这个问题。
我今天早上开始使用Golang来自Python。我想从Go调用一个闭源可执行文件几次,使用 bit 并发,使用不同的命令行参数。我生成的代码工作正常,但我想得到你的输入以改进它。由于我处于早期学习阶段,我还将解释我的工作流程。
为简单起见,假设这个“外部闭源程序”是zenity
,这是一个Linux命令行工具,可以从命令行显示图形消息框。
所以,在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 over Python的优势......这段代码实际上是以串行方式执行的。我有一个多核CPU,我想利用它。所以让我们用goroutines添加一些并发性。
让我们重写我们的代码,以便更容易调用和重用,并添加着名的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()
}
什么也没有!问题是什么?所有的goroutine都会立即执行。我真的不知道为什么zenity没有执行但是AFAIK,Go程序在zenity外部程序甚至可以初始化之前退出。使用time.Sleep
证实了这一点:等待几秒就足以让8个zenity实例自行启动。我不知道这是否可以被视为一个错误。
更糟糕的是,我真正喜欢调用的真实程序需要一段时间才能执行。如果我在我的4核CPU上并行执行该程序的8个实例,那么浪费一些时间进行大量的上下文切换......我不知道Go goroutines有多么简单,但是exec.Command
会在8个不同的线程中启动zenity 8次。更糟糕的是,我想要执行这个程序超过100,000次。在goroutines中一次完成所有这些工作根本不会有效。不过,我还是想利用我的4核CPU!
在线资源倾向于建议使用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)来从我创建的虚拟通道中弹出一个值。哦,当然,使用虚拟通道本身就是丑陋的。
现在我们差不多完成了。我只想考虑另一个副作用: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()
}
完成。
我不是指线索; Go如何在内部管理goroutines是不相关的。我的意思是限制一次启动的goroutine的数量:exec.Command
每次调用时都会创建一个新线程,所以我应该控制它被调用的时间。
我无法说服自己这样的虚拟通道是可行的。
答案 0 :(得分:86)
我会产生4个工作器goroutine,它们从公共通道读取任务。 Goroutines比其他人更快(因为他们的安排不同或碰巧得到简单的任务)将从这个渠道获得比其他任务更多的任务。除此之外,我会用sync.WaitGroup等待所有工人完成。剩下的部分就是创建任务。您可以在此处查看该方法的示例实现:
package main
import (
"os/exec"
"strconv"
"sync"
)
func main() {
tasks := make(chan *exec.Cmd, 64)
// spawn four worker goroutines
var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
for cmd := range tasks {
cmd.Run()
}
wg.Done()
}()
}
// generate some tasks
for i := 0; i < 10; i++ {
tasks <- exec.Command("zenity", "--info", "--text='Hello from iteration n."+strconv.Itoa(i)+"'")
}
close(tasks)
// wait for the workers to finish
wg.Wait()
}
可能有其他可能的方法,但我认为这是一个非常简洁易懂的解决方案。
答案 1 :(得分:32)
一种简单的限制方法(同时执行f()
N次但最多maxConcurrency
),只是一个方案:
package main
import (
"sync"
)
const maxConcurrency = 4 // for example
var throttle = make(chan int, maxConcurrency)
func main() {
const N = 100 // for example
var wg sync.WaitGroup
for i := 0; i < N; i++ {
throttle <- 1 // whatever number
wg.Add(1)
go f(i, &wg, throttle)
}
wg.Wait()
}
func f(i int, wg *sync.WaitGroup, throttle chan int) {
defer wg.Done()
// whatever processing
println(i)
<-throttle
}
我不太可能将throttle
频道称为“虚拟”。恕我直言,这是一种优雅的方式(当然不是我的发明),如何限制并发。
BTW:请注意,您忽略了cmd.Run()
返回的错误。
答案 2 :(得分:1)
试试这个: https://github.com/korovkin/limiter
limiter := NewConcurrencyLimiter(10)
limiter.Execute(func() {
zenity(...)
})
limiter.Wait()
答案 3 :(得分:1)
package main
import (
"fmt"
"github.com/zenthangplus/goccm"
"math/rand"
"runtime"
)
func main() {
semaphore := goccm.New(runtime.NumCPU())
for {
semaphore.Wait()
go func() {
fmt.Println(rand.Int())
semaphore.Done()
}()
}
semaphore.WaitAllDone()
}
export GOPATH="$(pwd)/gopath"
go mod init *.go
go mod tidy
find "${GOPATH}" -exec chmod +w {} \;
rm --recursive --force "${GOPATH}"
答案 4 :(得分:0)
您可以使用 here in this post 描述的工作池模式。 这就是实现的样子......
package main
import (
"os/exec"
"strconv"
)
func main() {
NumEl := 8
pool := 4
intChan := make(chan int)
for i:=0; i<pool; i++ {
go callProg(intChan) // <--- launch the worker routines
}
for i:=0;i<NumEl;i++{
intChan <- i // <--- push data which will be received by workers
}
close(intChan) // <--- will safely close the channel & terminate worker routines
}
func callProg(intChan chan int) {
for i := range intChan{
cmd := exec.Command("zenity", "--info", "--text='Hello from iteration n." + strconv.Itoa(i) + "'")
cmd.Run()
}
}