Goの並行処理について学ぶ

Goプログラミングにおける並行処理について紹介したいと思います。以下の流れで紹介します。

  • Goの並列処理機能について知る
    • ゴルーチンとは
    • チャネルとは
  • 並列処理機能を使ってみる
    • ゴルーチンを使う
    • チャネルを使う

Goの並列処理機能について知る


並行処理とは、複数の処理を同時に実行することです。この処理を実現する機能として、Goにはゴルーチンチャネルがあります。

ゴルーチンとは


ゴルーチンは、複数の処理を同時に実行する関数です。関数を呼び出す処理の先頭にgoをつけることで、呼び出された関数はゴルーチンとして実行されます。

go 関数名

チャネルとは


チャネルは、ゴルーチン間でデータの受け渡しを行う変数の型です。キュー(FIFO)の考え方で、送信側が送った順に受信側がデータを受け取ります。言い換えると、受信側がデータを受け取らない限りデータは溜まっていきます。変数の型の前にchanをつけることで利用することができます。

chan 変数の型

並列処理機能を使ってみる


では、ゴルーチンとチャネルを使って並列処理を体感します。

ゴルーチンを使ってみる


まずはゴルーチンを使ったプログラムを作成します。

数字とアルファベットを26個ずつ表示する関数を作成し、逐次処理とゴルーチンを使った並行処理の出力結果を比較します。

package main

import "fmt"
import "time"

func createNum(){
  for i := 0; i <= 26; i++ {
    fmt.Printf( "%d, " , i )
    time.Sleep( 1000 )
  }
}

func createChar(){
  for i := 'A'; i <= 'Z'; i++ {
    fmt.Printf( "%c, " , i )
    time.Sleep( 1000 )
  }
}

func main(){
  fmt.Println( "--------逐次処理--------" )
  createNum()
  createChar()
  fmt.Println( "\n------------------------" )
  fmt.Println( "--------並行処理--------" )
  go createNum()
  go createChar()
  fmt.Println( "\n------------------------" )
}

実行してみますが、何も表示されません。。ゴルーチンの処理が実行される前にプログラム自体が終了してる可能性があります。

$ go run main.go 
--------逐次処理--------
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, 
------------------------
--------並行処理--------

------------------------

処理を待たせるためには、sleepかWaitGroupを使うのがいいとのことでした。せっかくなのでWaitGroup使って以下のようにしました。逐次処理にWaitGroup入れる意味はありませんが。。。

package main

import "fmt"
import "sync"
import "time"

func createNum(wg *sync.WaitGroup){
  for i := 0; i <= 26; i++ {
    fmt.Printf( "%d, " , i )
    time.Sleep( 1000 )
  }
  wg.Done()
}

func createChar(wg *sync.WaitGroup){
  for i := 'A'; i <= 'Z'; i++ {
    fmt.Printf( "%c, " , i )
    time.Sleep( 1000 )
  }
  wg.Done()
}

func main(){
  var wg sync.WaitGroup
  fmt.Println( "--------逐次処理--------" )
  wg.Add( 2 )
  createNum( &wg )
  createChar( &wg )
  wg.Wait()
  fmt.Println( "\n------------------------" )
  fmt.Println( "--------並行処理--------" )
  wg.Add( 2 )
  go createNum( &wg )
  go createChar( &wg )
  wg.Wait()
  fmt.Println( "\n------------------------" )
}

今度は正しく表示されました。

$ go run main.go 
--------逐次処理--------
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, 
------------------------
--------並行処理--------
0, A, 1, B, 2, C, 3, D, E, 4, F, 5, G, 6, H, 7, I, J, K, 8, L, M, 9, N, O, 10, P, 11, 12, Q, 13, 14, 15, R, 16, S, 17, 18, 19, T, U, V, W, X, 20, Y, 21, Z, 22, 23, 24, 25, 26, 
------------------------

チャネルを使ってみる


チャネルを使ってデータの受け渡しをしてみたいと思います。

createNum関数から受け取った値をcreateChar関数で表示するソースコードを作成しました。

package main

import "fmt"
import "sync"

func createNum(c chan int, wg *sync.WaitGroup){
  for i := 1; i <= 5; i++ {
    c <- i
    fmt.Printf( "create %d\n" , i )
  }
  wg.Done()
}

func createChar(c chan int, wg *sync.WaitGroup){
  for i := 'A'; i <= 'E'; i++ {
    num := <-c
    fmt.Printf( "%d = %c\n" , num, i )
  }
  wg.Done()
}

func main(){
  var wg sync.WaitGroup
  c := make( chan int )
  fmt.Println( "--------並行処理--------" )
  wg.Add( 2 )
  go createNum( c, &wg )
  go createChar( c, &wg )
  wg.Wait()
  fmt.Println( "\n------------------------" )
}

実行してみると、下記のようになりました。createNumが送った値が、createChar関数で正しい順序で表示されていることがわかります。これはFIFOの性質で、どんなに並列実行しようとも最初に格納された値を抜き出しています。出力されるタイミングが前後しているのは誤差なので、あまり気にしなくて大丈夫そうです。

--------並行処理--------
create 1
1 = A
2 = B
create 2
create 3
3 = C
4 = D
create 4
create 5
5 = E

------------------------

以上です。パッケージとか色々準備する必要がなかったので、簡単に使える印象でした。皆様もお試しあれ〜。