【问题标题】:How would you define a pool of goroutines to be executed at once?您将如何定义要立即执行的 goroutine 池?
【发布时间】: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


    【解决方案1】:

    我会生成 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()
    }
    

    可能还有其他可能的方法,但我认为这是一个非常简洁且易于理解的解决方案。

    【讨论】:

    • 我刚刚做了一个类似的版本。这好多了!谢谢。
    • 不,它没有。上面的代码使用单个 SPMC(单个生产者/多个消费者)队列将任务分配给不同的工作者。每个命令只能从任务频道接收一次。
    • 'range tasks' 会一直迭代,直到任务通道没有更多消息?
    • 这个解决方案有两个小问题:(1) wg.Done() 最好包裹在延迟 wg.Done() 中。 (2) 即使你修复了 (1) ,在某些 goroutine 被 cmd.Run() 恐慌杀死后,你仍然可能无法充分利用最大并发。
    • +1 作为答案,但是,我有一个问题,如果我想将 i 输出存储在变量中并打印出来怎么办?
    【解决方案2】:

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

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

    【讨论】:

    • 这个例子是错误的。您应该先用maxConcurrency 项目填充throttle 通道并交换接收和发送操作,以便正确同步数据。请查看此线程以获取更多信息:groups.google.com/d/msg/golang-nuts/MDvnk1Ax7UQ/eQGkJJmOxc4J
    • @tux21b:欢迎你正式证明它是错误的 ;-) 同时,让我尝试证明相反的情况:开始一个新的 goroutine 之前是一个“令牌” " 必须在 throttle channel 中插入 (throttle &lt;- 1)。只有在 f() 完成其任何处理之后,才会从同一通道中删除“令牌”(&lt;-throttle)。由于通道的容量固定为maxConcurrency,因此排队的令牌永远不会超过maxConcurrency。因此,f() 处理数据的并发实例不能超过maxConcurrency。顺便说一句:上面没有发生“同步数据”。
    • @tux21b:不。在呈现的模式中,没有数据竞争是相关的,因为没有数据是同时共享的。模式只是一种限制并发执行功能(如果您愿意,也可以称为工作人员)数量的机制。架构正是这样做的(仅此而已)。即使到目前为止没有任何证据声称“错误”......
    • @tux21b:对不起,我已经正式推导出了我的示例为何有效,但到目前为止,您没有提供任何证据为什么它不起作用。 “想象一下” 挥手不符合条件。你真的了解我的推导吗?如果是这样,您能否就失败的地方提出一个有效的观点?我的主张是:“该示例将同时执行的f() 的数量限制为maxConcurrency。您的声明是“这个例子是错误的”。我们不可能都对,对吗? ;-)
    • 对于其他人来说,TL;DR zzzz 是正确的,尽管 tux21b 有一段时间是正确的。请参阅以下内容:golang.org/doc/go1.3#memorycodereview.appspot.com/75130045golang.org/doc/effective_go.html#channels
    【解决方案3】:

    试试这个: https://github.com/korovkin/limiter

     limiter := NewConcurrencyLimiter(10)
     limiter.Execute(func() {
            zenity(...) 
     })
     limiter.Wait()
    

    【讨论】:

      【解决方案4】:

      ? 模块


      ? 模板

      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}"
      

      【讨论】:

        【解决方案5】:

        您可以使用here in this post 中描述的 Worker Pool 模式。 这就是实现的样子......

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

        【讨论】:

          猜你喜欢
          • 2016-02-21
          • 1970-01-01
          • 2016-10-11
          • 1970-01-01
          • 1970-01-01
          • 2015-05-14
          • 2013-08-18
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多