web-dev-qa-db-ja.com

Golangで一度に実行されるゴルーチンのプールをどのように定義しますか?

TL; TR:最後の部分に行って、この問題の解決方法を教えてください。

私は今朝PythonからGolangを使い始めました。 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を使用していますが、それを利用したいと思います。それでは、ゴルーチンとの並行性を追加しましょう。

ゴルーチン、またはプログラムを並列化する方法

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

何もない!何が問題ですか?すべてのゴルーチンは一度に実行されます。 zenityが実行されない理由はよくわかりませんが、zenity外部プログラムが初期化される前にGoプログラムが終了しました。これは、_time.Sleep_を使用することで確認されました。数秒間待機するだけで、zenityの8個のインスタンスを起動できました。ただし、これがバグと見なされるかどうかはわかりません。

さらに悪いことに、実際に呼び出したい実際のプログラムは、それ自体を実行するのに時間がかかります。このプログラムの8つのインスタンスを4コアCPUで並列実行すると、多くのコンテキストスイッチングを行うのに時間を浪費することになります...単純なGoゴルーチンの動作がわかりませんが、_exec.Command_willは、8つの異なるスレッドでzenityを8回起動します。さらに悪いことに、このプログラムを100,000回以上実行したいと思います。ゴルーチンですべてを一度に行うのは効率的ではありません。それでも、4コアCPUを活用したいと思います!

b)2回目の試行:ゴルーチンのプールを使用する

オンラインリソースは、この種の作業には_sync.WaitGroup_の使用を推奨する傾向があります。そのアプローチの問題は、基本的にゴルーチンのバッチで作業していることです:4メンバーのWaitGroupを作成すると、Goプログラムはallを待機します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の概念は大好きですが、作成したダミーチャネルから値をポップするために関数(ラムダでも)を宣言する必要はありません。ああ、もちろん、ダミーチャネルを使用すること自体はbyいです。

c)3回目の試み:すべての子供が死んだときに死ぬ

これでほぼ完成です。さらに別の副作用を考慮する必要があります。すべてのZenityポップアップが閉じる前にGoプログラムが閉じます。これは、ループが(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がゴルーチンを内部で管理する方法は関係ありません。私は本当に一度に起動されるゴルーチンの数を制限することを意味します:_exec.Command_は呼び出されるたびに新しいスレッドを作成するので、呼び出される回数を制御する必要があります。

  • そのコードはあなたに見栄えが良いですか?
  • その場合、ダミーチャンネルの使用を避ける方法を知っていますか?

私はそのようなダミーチャンネルが進むべき道であると自分自身を納得させることはできません。

64
user1940040

共通のチャネルからタスクを読み取る4つのワーカーゴルーチンを生成します。他のグループよりも速いゴルーチン(スケジュールが異なるか、たまたま単純なタスクを取得するため)は、このチャネルから他のタスクよりも多くのタスクを受け取ります。それに加えて、 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()
}

他の方法も考えられますが、これは非常にクリーンなソリューションであり、理解しやすいと思います。

87
tux21b

スロットルへの単純なアプローチ(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()から返されたエラーを無視していることに注意してください。

34
zzzz

これを試してください: https://github.com/korovkin/limiter

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