TL; TR:最後の部分に行って、この問題の解決方法を教えてください。
私は今朝PythonからGolangを使い始めました。 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がPythonより優れているという点はまだわかりません…。このコードは、実際にはシリアル形式で実行されます。マルチコアCPUを使用していますが、それを利用したいと思います。それでは、ゴルーチンとの並行性を追加しましょう。
呼び出しと再利用を簡単にするためにコードを書き直し、有名な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を活用したいと思います!
オンラインリソースは、この種の作業には_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いです。
これでほぼ完成です。さらに別の副作用を考慮する必要があります。すべての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
_は呼び出されるたびに新しいスレッドを作成するので、呼び出される回数を制御する必要があります。
私はそのようなダミーチャンネルが進むべき道であると自分自身を納得させることはできません。
共通のチャネルからタスクを読み取る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()
}
他の方法も考えられますが、これは非常にクリーンなソリューションであり、理解しやすいと思います。
スロットルへの単純なアプローチ(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()
から返されたエラーを無視していることに注意してください。
これを試してください: https://github.com/korovkin/limiter
limiter := NewConcurrencyLimiter(10)
limiter.Execute(func() {
zenity(...)
})
limiter.Wait()