この記事は最終更新日から1年以上が経過しています。
この記事の目的
6秒かかる直列処理を並行処理に改修し、3秒で終わるようにする
準備
まずは、適当にフォルダを作る
mkdir ~/Desktop/goroutine
cd ~/Desktop/goroutine/
つぎに、main.goファイルを作る
まず直列実行するプログラムを書く
- 1秒待つコマンド
- 2秒待つコマンド
- 3秒待つコマンド
上記を順番に実行していくプログラムを main.go
の中身を書く。
つまり、合計で6秒かかってしまう。
main.go
package main
import (
"log"
"time"
)
func main() {
log.Print("started.")
// 1秒かかるコマンド
log.Print("sleep1 started.")
time.Sleep(1 * time.Second)
log.Print("sleep1 finished.")
// 2秒かかるコマンド
log.Print("sleep2 started.")
time.Sleep(2 * time.Second)
log.Print("sleep2 finished.")
// 3秒かかるコマンド
log.Print("sleep3 started.")
time.Sleep(3 * time.Second)
log.Print("sleep3 finished.")
log.Print("all finished.")
}
ともあれ、実行してみる
% go run main.go
2013/12/05 15:02:09 started.
2013/12/05 15:02:09 sleep1 started.
2013/12/05 15:02:10 sleep1 finished.
2013/12/05 15:02:10 sleep2 started.
2013/12/05 15:02:12 sleep2 finished.
2013/12/05 15:02:12 sleep3 started.
2013/12/05 15:02:15 sleep3 finished.
2013/12/05 15:02:15 all finished.
やはり、6秒かかった。
これを並行化したい。
Goルーチンを使って並行化し、3秒で終わるようにする
Go言語にはGoルーチンという処理の並行化を簡単にできる仕組みがある。クロージャーや関数に go
をつけるだけなのでシンプル。
ただし、Goルーチンだけでは、並行化した処理が終わる前に、メインの処理が終わってしまう。つまり、待ってくれない。Goルーチンが終わるまで待つには、「チャネル」という仕組みを使う。
なお、各ルーチンの実行結果を、呼び出し元に戻すときにもチャネルを使う。今回は、特に実行結果は必要ないので、チャネルに適当な値を入れることにする。ここでは、とりあえずbool値にする。
以上を踏まえて、さきほどの main.go
を改修する:
main.go
package main
import (
"log"
"time"
)
func main() {
log.Print("started.")
// チャネル
sleep1_finished := make(chan bool)
sleep2_finished := make(chan bool)
sleep3_finished := make(chan bool)
go func() {
// 1秒かかるコマンド
log.Print("sleep1 started.")
time.Sleep(1 * time.Second)
log.Print("sleep1 finished.")
sleep1_finished <- true
}()
go func() {
// 2秒かかるコマンド
log.Print("sleep2 started.")
time.Sleep(2 * time.Second)
log.Print("sleep2 finished.")
sleep2_finished <- true
}()
go func() {
// 3秒かかるコマンド
log.Print("sleep3 started.")
time.Sleep(3 * time.Second)
log.Print("sleep3 finished.")
sleep3_finished <- true
}()
// 終わるまで待つ
<- sleep1_finished
<- sleep2_finished
<- sleep3_finished
log.Print("all finished.")
}
実行してみよう
% go run main.go
2013/12/05 15:14:58 started.
2013/12/05 15:14:58 sleep1 started.
2013/12/05 15:14:58 sleep2 started.
2013/12/05 15:14:58 sleep3 started.
2013/12/05 15:14:59 sleep1 finished.
2013/12/05 15:15:00 sleep2 finished.
2013/12/05 15:15:01 sleep3 finished.
2013/12/05 15:15:01 all finished.
並行化ができて、3秒で終わるようになった\(^o^)/
チャネルが冗長なので1つにしたい…
sleep1_finished
、sleep2_finished
、sleep3_finished
の3つのチャネルを作ったが、もっとエレガントにチャネルしたい。
main.go
package main
import (
"log"
"time"
)
func main() {
log.Print("started.")
// チャネル
finished := make(chan bool)
go func() {
// 1秒かかるコマンド
log.Print("sleep1 started.")
time.Sleep(1 * time.Second)
log.Print("sleep1 finished.")
finished <- true
}()
go func() {
// 2秒かかるコマンド
log.Print("sleep2 started.")
time.Sleep(2 * time.Second)
log.Print("sleep2 finished.")
finished <- true
}()
go func() {
// 3秒かかるコマンド
log.Print("sleep3 started.")
time.Sleep(3 * time.Second)
log.Print("sleep3 finished.")
finished <- true
}()
// 終わるまで待つ
<-finished
<-finished
<-finished
log.Print("all finished.")
}
チャネルはひとつにできたが、待つところで3回待つ必要がある…。
10並行したら10回 <-finished
を書かないといけないのは面倒だし、エンバグしそうなので、回数を指定したい。
「終わるまで待つ」ところを回数指定にする
とりあえず無骨に for
で3回 <-finished
を実行すれば良いようだ
main.go
package main
import (
"log"
"time"
)
func main() {
log.Print("started.")
// チャネル
finished := make(chan bool)
go func() {
// 1秒かかるコマンド
log.Print("sleep1 started.")
time.Sleep(1 * time.Second)
log.Print("sleep1 finished.")
finished <- true
}()
go func() {
// 2秒かかるコマンド
log.Print("sleep2 started.")
time.Sleep(2 * time.Second)
log.Print("sleep2 finished.")
finished <- true
}()
go func() {
// 3秒かかるコマンド
log.Print("sleep3 started.")
time.Sleep(3 * time.Second)
log.Print("sleep3 finished.")
finished <- true
}()
// 終わるまで待つ
for i := 1; i <= 3; i++ {
<-finished
}
log.Print("all finished.")
}
実行してみる:
% go run main.go
2013/12/05 15:32:40 started.
2013/12/05 15:32:40 sleep2 started.
2013/12/05 15:32:40 sleep1 started.
2013/12/05 15:32:40 sleep3 started.
2013/12/05 15:32:41 sleep1 finished.
2013/12/05 15:32:42 sleep2 finished.
2013/12/05 15:32:43 sleep3 finished.
2013/12/05 15:32:43 all finished.
回数指定じゃなくて、ルーチンの数だけ待ちたい
<-finsihed
を列挙するよりも、回数指定のほうが並行数の増減に対応しやすいが、ルーチンの数に応じて待つようにしたい。
どうやってやるかだが、クロージャーを配列にして、要素の数だけGoルーチンを開始し、要素の数だけ <-finsihed
を実行するようにする。この変更を加えたコードが下記になる。
main.go
package main
import (
"log"
"time"
)
func main() {
log.Print("started.")
finished := make(chan bool)
// 配列
funcs := []func(){
func() {
// 1秒かかるコマンド
log.Print("sleep1 started.")
time.Sleep(1 * time.Second)
log.Print("sleep1 finished.")
finished <- true
},
func() {
// 2秒かかるコマンド
log.Print("sleep2 started.")
time.Sleep(2 * time.Second)
log.Print("sleep2 finished.")
finished <- true
},
func() {
// 3秒かかるコマンド
log.Print("sleep3 started.")
time.Sleep(3 * time.Second)
log.Print("sleep3 finished.")
finished <- true
},
}
// 並行化する
for _, sleep := range funcs {
go sleep()
}
// 終わるまで待つ
for i := 0; i < len(funcs); i++ {
<-finished
}
log.Print("all finished.")
}
実行してみる:
% go run main.go
2013/12/05 16:30:18 started.
2013/12/05 16:30:18 sleep1 started.
2013/12/05 16:30:18 sleep2 started.
2013/12/05 16:30:18 sleep3 started.
2013/12/05 16:30:19 sleep1 finished.
2013/12/05 16:30:20 sleep2 finished.
2013/12/05 16:30:21 sleep3 finished.
2013/12/05 16:30:21 all finished.
まとめ
6秒かかる処理を、Goルーチンとチャネルを組み合わせて並行化し、3秒で終わるようになった。
課題
最後の for
あたりをもっとシンプルにする方法はないものか?
UPDATE 2013/12/06 チャネルを使わずに待つ方法
調べてみたら sync.WaitGroup というモジュールがあることがわかった。これを使うとチャネルを宣言しなくても、処理を待つことができる。
main.go
package main
import (
"log"
"sync"
"time"
)
func main() {
log.Print("started.")
// 配列
funcs := []func(){
func() {
// 1秒かかるコマンド
log.Print("sleep1 started.")
time.Sleep(1 * time.Second)
log.Print("sleep1 finished.")
},
func() {
// 2秒かかるコマンド
log.Print("sleep2 started.")
time.Sleep(2 * time.Second)
log.Print("sleep2 finished.")
},
func() {
// 3秒かかるコマンド
log.Print("sleep3 started.")
time.Sleep(3 * time.Second)
log.Print("sleep3 finished.")
},
}
var waitGroup sync.WaitGroup
// 関数の数だけ並行化する
for _, sleep := range funcs {
waitGroup.Add(1) // 待つ数をインクリメント
// Goルーチンに入る
go func(function func()) {
defer waitGroup.Done() // 待つ数をデクリメント
function()
}(sleep)
}
waitGroup.Wait() // 待つ数がゼロになるまで処理をブロックする
log.Print("all finished.")
}
実行結果:
% go fmt main.go && go run main.go
2013/12/06 16:14:40 started.
2013/12/06 16:14:40 sleep1 started.
2013/12/06 16:14:40 sleep2 started.
2013/12/06 16:14:40 sleep3 started.
2013/12/06 16:14:41 sleep1 finished.
2013/12/06 16:14:42 sleep2 finished.
2013/12/06 16:14:43 sleep3 finished.
2013/12/06 16:14:43 all finished.
0 件のコメント:
コメントを投稿