TL;DR: 最後の部分に進んで、この問題をどのように解決するか教えてください。
今朝、Python から Go を使い始めました。Go からクローズド ソースの実行可能ファイルを複数回呼び出したいのですが、少し並行して、さまざまなコマンド ライン引数を使用します。私の結果のコードはうまく機能していますが、改善するためにあなたの意見を求めたいと思います。私は学習の初期段階にあるので、ワークフローについても説明します。
簡単にするために、ここでは、この「外部のクローズド ソース プログラム」がzenity
、コマンド ラインからグラフィカル メッセージ ボックスを表示できる Linux コマンド ライン ツールであると仮定します。
Go から実行可能ファイルを呼び出す
したがって、Go では、次のようになります。
package main
import "os/exec"
func main() {
cmd := exec.Command("zenity", "--info", "--text='Hello World'")
cmd.Run()
}
これは正しく機能するはずです。は、その後に続く.Run()
と機能的に同等であることに注意してください。これは素晴らしいことですが、このプログラムを 1 回だけ実行したい場合、プログラミング全体を行う価値はありません。だから、それを何度もやってみましょう。.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
、 8 つの異なるスレッドで zenity を 8 回起動します。 . さらに悪いことに、このプログラムを 10 万回以上実行したいと考えています。これらすべてをゴルーチンで一度に行うのは、まったく効率的ではありません。それでも、4コアCPUを活用したい!
b) 2 回目の試行: ゴルーチンのプールを使用する
sync.WaitGroup
オンライン リソースでは、この種の作業に を使用することを推奨する傾向があります。このアプローチの問題点は、基本的にゴルーチンのバッチで作業していることです。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
ますが、作成したダミー チャネルから値を取り出す関数 (ラムダであっても) を宣言する必要はありません。ああ、もちろん、ダミー チャネルを使用すること自体が醜いことです。
c) 3 回目の試行: すべての子供が死亡した時点で死亡
これでほぼ完成です。もう 1 つの副作用を考慮に入れる必要があります。すべての 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
ことです。呼び出されるたびに新しいスレッドが作成されるため、呼び出される回数を制御する必要があります。
- そのコードは問題ないように見えますか?
- その場合、ダミーチャンネルの使用を避ける方法を知っていますか?
そのようなダミーチャンネルが道であるとは思えません。