並行処理一覧

【Go入門】Goの並行処理(1)_ゴルーチンとチャネル

こんにちは。Go入門ブログの第17回です。

本記事から3回に渡り、Goにおける並行処理を解説していきます。

第1弾となる今回は、Goにおける並行処理を司るゴルーチン(goroutine)チャネルについて、基本的な知識を解説していきます。
また、ゴルーチンを生成するgoステートメントについてもあわせて説明します。

ゴルーチンとgoステートメント

ゴルーチン(goroutine)は、Goのランタイムに管理される、スレッドよりも軽量な処理単位です。
goステートメントを使用することで、ゴルーチンを生成、利用することができます。

goステートメントはdeferと同様に、関数実行のステートメントを受け取ります。
goステートメントは、独立した並列スレッドまたはゴルーチン(goroutine)として、関数の実行を同一アドレス空間で開始します。

次のサンプルを実行すると、main内の無限ループとsubLoop関数内の無限ループが並行して実行され、各々のループで出力される文字列が不規則に表示されます。

package main

import (
	"fmt"
)

// 新しく生成されるgoroutineが実行する関数
func subLoop() {
	for {
		fmt.Println("Sub Loop")
	}
}

func main() {
    // goroutineを生成してsubLoop関数を実行
	go subLoop()
	for {
		fmt.Println("Main Loop")
	}
}

また、無名関数を使用することで、関数の作成とゴルーチンの生成を同時に行うこともできます。
無名関数を使用すると、上記のサンプルは以下のように書き換えられます。

package main

import (
	"fmt"
)

func main() {
	go func() {
		for {
			fmt.Println("Sub Loop")
		}
	}()
	for {
		fmt.Println("Main Loop")
	}
}

チャネル

チャネルは、ゴルーチン間でのデータの受け渡しを実現します。

チャネルの型名は、chan [データ型]のように記述します。

var [変数名] chan [データ型]

また、チャネルは参照型のデータ構造ですので、マップやスライスのように、makeステートメントによって生成します。

それでは、実際にチャネルを生成してデータの受け渡しを行うサンプルを見てみましょう。
値の送受信には<-オペレータを使用します。

package main

import "fmt"

// 加算関数
func sum(nums []int, c chan int) {
	val := 0
	for _, v := range nums {
		val += v
	}
	c <- val // チャネルに値を送信
}

// 乗算関数
func multi(nums []int, c chan int) {
	val := 1
	for _, v := range nums {
		val = val * v
	}
	c <- val // チャネルに値を送信
}

func main() {
	// 数値配列
	nums := []int{1, 2, 3, 4}

	// 数値型のチャネルを生成する
	c := make(chan int)

	// 2つのゴルーチンを生成して関数を実行
	go sum(nums, c)
	go multi(nums, c)

	// チャネルから変数x, yに値を受信
	x, y := <-c, <-c

	fmt.Println(x, y)
}

11行目と20行目がチャネルへのデータ送信、35行目がチャネルからのデータ受信です。
このサンプルの実行結果は以下のようになります。

$ go run main.go
24 10

並行処理が行われ、チャネルcからそれぞれの処理結果を受信できていることが確認できます。

バッファ付きチャネル

makeステートメントでチャネルを生成する際に、バッファサイズを指定することができます。

[変数名] := make(chan [データ型], バッファサイズ)

バッファがいっぱいになった時は、チャネルへの送信はロックされます。
逆に、バッファが空の時には、チャネルからの受信がロックされます。

実際に例を見てみましょう。

package main

import "fmt"

func main() {
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
	x, y := <-ch, <-ch
	fmt.Println(x ,y)
}

この例では、バッファサイズ2のチャネルにデータを2回送信しています。
コードを実行すると、以下のように問題なくサンプルが実行されます。

$ go run main.go
1 2

次に、バッファがいっぱいになったチャネルに対してデータを送信するサンプルです。

package main

import "fmt"

func main() {
	ch := make(chan int, 2)
	// チャネルにバッファサイズを越えるデータを送信
	ch <- 1
	ch <- 2  //
	ch <- 3
	x, y := <-ch, <-ch
	fmt.Println(x, y)
}

このサンプルを実行すると、以下のようにdeadlockによるエラーが発生します。

$ go run main.go
fatal error: all goroutines are asleep – deadlock!

goroutine 1 [chan send]:
main.main()
/home/user/main.go:10 +0xa2
exit status 2

それでは、逆に空のバッファからデータを受信した場合の動作をみてみましょう。

package main

import "fmt"

func main() {
	ch := make(chan int, 2)
	// 空のチャネルからデータを受信
	x, y := <-ch, <-ch
	ch <- 1
	ch <- 2
	fmt.Println(x, y)
}

この場合も、同様にdeadlockによるエラーが発生します。

$ go run main.go
fatal error: all goroutines are asleep – deadlock!

goroutine 1 [chan receive]:
main.main()
/home/user/main.go:8 +0x75
exit status 2

len関数とcap関数

組み込み関数lencapをチャネルに使用することができます。
len関数ではチャネルのバッファ内のデータ個数を、cap関数ではチャネルのバッファサイズを取得できます。

package main

import "fmt"

func main() {
	ch := make(chan int, 2)
	// チャネル生成直後のlenおよびcapの値
	fmt.Println("データ数", len(ch))
	fmt.Println("バッファサイズ", cap(ch))
	ch <- 1
	ch <- 2
	// チャネルへデータ送信した後のlenおよびcapの値
	fmt.Println("データ数", len(ch))
	fmt.Println("バッファサイズ", cap(ch))
}

このサンプルの実行結果は以下の通りになります。

$ go run main.go
データ数 0
バッファサイズ 2
データ数 2
バッファサイズ 2

チャネルがデータを受信する前後で、len関数の結果は変化するのに対し、cap関数の結果は同じです。
一度生成したチャネルのバッファサイズが変更されることはありあせんので、cap関数を有効に利用できるケースはあまりないかもしれません。

終わりに

本記事では、Goの並行処理に関する基本的な知識を解説しました。
次回の記事でも引き続き、チャネル型の利用方法を紹介していきます。

ゴルーチンとチャネルにより、効率的かつ安全な並行処理をシンプルに書くことができるのは、Goを利用する大きなメリットです。
しっかり理解していきましょう。


スポンサーリンク
スポンサーリンク
スポンサーリンク