你如何定义一个在Golang中一次执行的goroutine池?

时间:2013-08-23 14:14:29

标签: multithreading go goroutine

TL; TR:请转到最后一部分告诉我你将如何解决这个问题。

我今天早上开始使用Golang来自Python。我想从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 over 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()
}

什么也没有!问题是什么?所有的goroutine都会立即执行。我真的不知道为什么zenity没有执行但是AFAIK,Go程序在zenity外部程序甚至可以初始化之前退出。使用time.Sleep证实了这一点:等待几秒就足以让8个zenity实例自行启动。我不知道这是否可以被视为一个错误。

更糟糕的是,我真正喜欢调用的真实程序需要一段时间才能执行。如果我在我的4核CPU上并行执行该程序的8个实例,那么浪费一些时间进行大量的上下文切换......我不知道Go goroutines有多么简单,但是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()
}

完成。

我的问题

  • 您是否知道限制一次执行的goroutines数量的其他正确方法?

我不是指线索; Go如何在内部管理goroutines是不相关的。我的意思是限制一次启动的goroutine的数量:exec.Command每次调用时都会创建一个新线程,所以我应该控制它被调用的时间。

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

我无法说服自己这样的虚拟通道是可行的。

5 个答案:

答案 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
}

Playground

我不太可能将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()
}

? 最佳常规量

  • 如果操作是CPU 受限:runtime.NumCPU()
  • 否则测试:time go run *.go

? 配置

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