您如何定义要立即执行的goroutine池?


67

TL; DR:请转到最后一部分,告诉我如何解决此问题。

我今天早上开始使用来自Python的Go语言。我想用不同的命令行参数多次调用Go的封闭源可执行文件,并发一点。我得到的代码工作得很好,但是我想得到您的意见,以便进行改进。由于我处于早期学习阶段,因此我还将解释我的工作流程。

为了简单起见,在此假定此“外部封闭源程序”是zenityLinux命令行工具,可以从命令行显示图形消息框。

从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关键字:

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:等待几秒钟足以让zenity的8个实例启动。我不知道这是否可以视为错误。

更糟糕的是,我实际上想调用的真实程序需要一段时间才能执行。如果我在4核CPU上并行执行该程序的8个实例,这将浪费一些时间进行大量上下文切换……我不知道普通Go goroutine的行为如何,但exec.Command 在8个不同线程中启动zenity 8次。更糟糕的是,我希望执行此程序超过100,000次。在goroutines中一次执行所有这些操作根本不会有效。尽管如此,我还是想利用我的4核CPU!

b)第二次尝试:使用goroutines池

在线资源倾向于推荐使用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每次调用都会创建一个新线程,因此我应该控制它的调用次数。

  • 该代码对您来说看起来不错吗?
  • 您知道在这种情况下如何避免使用虚拟通道吗?

我不能说服自己,要走这样的虚拟渠道。

Answers:


88

我会生成4个工作程序,它们从一个公共通道读取任务。比其他程序更快的Goroutine(因为它们的排定时间不同或碰巧得到简单的任务)将从该通道接收的任务比其他程序更多。除此之外,我将使用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
我刚刚做了一个类似的版本。这样更好!谢谢。

4
不,不是。上面的代码使用单个SPMC(单个生产者/多个消费者)队列将任务分配给不同的工作人员。每个命令只能从任务通道接收一次。
tux21b

2
“范围任务”将重复进行,直到任务通道没有更多消息为止?
克里斯

该解决方案有两个小问题:(1)wg.Done()最好包装在一个延迟wg.Done()中。(2)即使您修复了(1),在某些goroutine被cmd.Run()恐慌杀死之后,您仍可能无法充分利用最大并发性。
fubupc

+1作为答案,但是,我有一个问题,如果我想将i输出存储在变量中并打印出来,该怎么办?
AndreaM16

34

一种简单的节流方法(执行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频道称为“虚拟”。恕我直言,这是一种优雅的方式(这当然不是我的发明),如何限制并发。

顺便说一句:请注意,您忽略的是返回的错误cmd.Run()


这个例子是错误的。您应该先在throttle通道中填充maxConcurrency项目,然后交换接收和发送操作,以便正确同步数据。请看以下主题以获取更多信息:groups.google.com/d/msg/golang-nuts/MDvnk1Ax7UQ/eQGkJJmOxc4J
tux21b 2013年

3
@ tux21b:欢迎您正式证明它是错误的;-)同时,请让我尝试证明相反的意思:开始新的goroutine之前,必须在(throttle <- 1)中插入“令牌” throttle channel<-throttle f()完成任何处理后,才会从同一通道中删除“令牌” 。由于通道的固定容量为maxConcurrency,因此maxConcurrency排队的令牌数量绝不能超过令牌。因此,只能有maxConcurrency并发的f()处理数据实例。顺便说一句:上面没有“同步数据”发生。
zzzz

1
@ tux21b:否。在所示的架构中,没有任何数据竞争是相关的,因为没有数据被并发共享。模式只是一种限制并发执行功能(如果需要,也称为工作程序)数量的机制。该模式正是这样做的(仅此而已)。到目前为止,即使声称“错误”而没有任何证据……
zzzz

1
@ tux21b:很抱歉,我已经正式得出了为什么我的示例可行,但是到目前为止,您还没有提供任何证明,无论为什么不可行。挥舞着“只是想像”就没有资格。你真的了解我的推论吗?如果是这样,您能就失败的地方提出一个合理的观点吗?我的主张是:“该示例将并发执行的次数限制f()maxConcurrency。您的主张是“此示例错误”。我们不能都对,对吗?;-)
zzzz

1
对于其他任何人,TL; DR zzzz都是正确的,尽管有一段时间tux21b是正确的。请参阅以下内容: golang.org/doc/go1.3#memory codereview.appspot.com/75130045 golang.org/doc/effective_go.html#channels
威廉·金

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.