Go言語の並行処理(Concurrency)を扱っていこうと思います。
ここからしばらく並行処理に関することを扱おうと思うのですが、これがなかなか奥深いテーマですので、『Go言語による並行処理』などでしっかりと取り組まないと十分な理解はできない分野なのかなと思います。
ここではゴルーチン(goroutine)について基本的な使い方を見ることで、並行処理の動きを見ていこうと思います。
ゴルーチン(goroutine)
並行処理を行う前に、2つの処理を行うコードを次のように書いてみます。
package main
import (
"fmt"
"time"
)
func operation1() {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println("処理1:", i)
}
}
func operation2() {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println("処理2:", i)
}
}
func main() {
operation1()
operation2()
}
operation1()、operation2()の2つの処理の関数を実行しています。
処理内容はどちらも同じfor文の処理で、time.Millisecondを使って100ms毎にループ処理を実行しています。
出力結果は次のようになります。
処理1: 0
処理1: 1
処理1: 2
処理1: 3
処理1: 4
処理2: 0
処理2: 1
処理2: 2
処理2: 3
処理2: 4
これは、operation1()を実行したあとに、operation2()を実行して出力しています。
実際の表示のタイミングは100ms毎に上から順に表示されているということを確認しましょう。
これは並行処理ではなく、順に処理しただけですね。
これをゴルーチン(goroutine)を使って並行処理のコードに変えてみます。
次のようにコードを修正してみます。
package main
import (
"fmt"
"time"
)
func operation1() {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println("処理1:", i)
}
}
func operation2() {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println("処理2:", i)
}
}
func main() {
go operation1()
operation2()
}
ゴルーチンをoperation1()の方に設定します。
main()関数で呼び出す時に、この頭に「go」を付けて処理します。
実行すると次のように出力されます。
処理2: 0
処理1: 0
処理1: 1
処理2: 1
処理2: 2
処理1: 2
処理1: 3
処理2: 3
処理2: 4
処理1: 4
こちらも100ms毎に上から順に表示されますが、違いとしては処理1と処理2が並行処理として出力されているのがわかります。
operation2()が実行されている間に、go operation1()が続けて実行されているということになります。
ただし、次のようなコードに変更してみると事情が変わってきます。
package main
import (
"fmt"
)
func operation1() {
for i := 0; i < 5; i++ {
fmt.Println("処理1:", i)
}
}
func operation2() {
for i := 0; i < 5; i++ {
fmt.Println("処理2:", i)
}
}
func main() {
go operation1()
operation2()
}
上のコードのoperation1()とoperation2()のtime.Sleep()の処理を削除しただけです。
これを実行するとこうなってしまいます。
処理2: 0
処理2: 1
処理2: 2
処理2: 3
処理2: 4
operation2()だけが実行されて、go operation1()の処理が出力されていません。(場合によっては、処理1の最初の一部分まで出力されているかもしれません)
time.Sleep()を削除したことで、operation2()の処理が終わる前にgo operation1()が実行できなかったために処理2の表示しかできなかったということになっています。
ゴルーチンの実行が行われる前に、処理が終わってしまったということになってしまったのですが、これを避けるためにWaitGroupというものを使います。
WaitGroup
上のコードをWiatGroupを使って書き換えて、処理が途中で終わらないようにしてみましょう。
次のようにしてみます。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func operation1() {
for i := 0; i < 5; i++ {
fmt.Println("処理1:", i)
}
wg.Done()
}
func operation2() {
for i := 0; i < 5; i++ {
fmt.Println("処理2:", i)
}
}
func main() {
wg.Add(1)
go operation1()
operation2()
wg.Wait()
}
syncパッケージをインポートして、変数wgとしてsync.WaitGroupとします。
ゴルーチンとして処理するoperation1()の処理に、wg.Done()を記入しています。
main()関数の中で、wg.Add(1)として、ゴルーチンとして並行処理するものが1つあるということを指定します。さらに、wg.Wait()を最後に加えて、wg.Done()が終わるまで処理の終了を待つということを指定しています。
実行するとこうなります。
処理2: 0
処理2: 1
処理2: 2
処理2: 3
処理2: 4
処理1: 0
処理1: 1
処理1: 2
処理1: 3
処理1: 4
今度は処理2だけで止まることなく、処理1も出力されているのがわかります。
再度、time.Sleep()を入れて書き換えてみます。
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func operation1() {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println("処理1:", i)
}
wg.Done()
}
func operation2() {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println("処理2:", i)
}
}
func main() {
wg.Add(1)
go operation1()
operation2()
wg.Wait()
}
実行結果は以下のとおりです。
処理1: 0
処理2: 0
処理1: 1
処理2: 1
処理1: 2
処理2: 2
処理2: 3
処理1: 3
処理1: 4
処理2: 4
このように、並行処理が行われているのがわかります。
このコードを次のように変えることもできます。
package main
import (
"fmt"
"sync"
// "time"
)
var wg sync.WaitGroup
func operation1(wg *sync.WaitGroup) {
for i := 0; i < 5; i++ {
// time.Sleep(100 * time.Millisecond)
fmt.Println("処理1:", i)
}
wg.Done()
}
func operation2() {
for i := 0; i < 5; i++ {
// time.Sleep(100 * time.Millisecond)
fmt.Println("処理2:", i)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(1)
go operation1(&wg)
operation2()
wg.Wait()
}
var wg sync.WaitGroup をmain()関数の中で宣言しています。operation1()関数の引数にこのwgのポインタをとり、ゴルーチンでこれを呼び出す時に&wgを渡して実行しています。
実行すると同じように並行処理が行われるのがわかります。
最後に
ここではGo言語の並行処理を、ゴルーチン(goroutine)を使って見てきました。
複数の処理を実行する時に、一方の処理の冒頭にgoを付けて関数を呼び出すことで、ゴルーチンの並行処理ができます。
ただし、処理の終了前にゴルーチンの処理が呼び出されないと並行処理が行われ無いことがあるので、これを避けるためにWaitGroupの処理を追加します。
Goの並行処理で特徴的なチャネルについては次で見ていきます。