Go言語

【Go言語】「バッファなしチャネル(channel)」を理解したいんじゃ!!

バッファなしチャネル(channel)とは

チャネル(channel)はゴルーチン(goroutine)間で値を受け渡しすることができる通信の仕組みで、チャネルは要素型と呼ばれる特定の型の値のみ送信できる。

「バッファありチャネル」と「バッファなしチャネル」があるが、今回は「バッファなしチャネル」についてまとめる。

チャネル(channel)の宣言、初期化

例えば、int型の値を送受信するチャネルは次のように宣言、初期化する。

var ch chan int
ch = make(chan int)

もちろん := でもOK。ch := make(chan int)

チャネルへの値の送受信

チャネルへ値を送受信するときは、<- 演算子を使う。

  • 送信:ch <- 値
  • 受信:<- ch
    • 変数に代入する場合 hoge := <- ch

↓↓簡単な例↓↓

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan string)
	go func() {
		time.Sleep(3 * time.Second)
		// チャネルに値を送信
		ch <- "Hello, Channel!!"
	}()

	// チャネルから値を受信
	fmt.Println(<-ch) // => Hello, Channel!!
}

 

チャネルのブロック

バッファなしチャネルの場合、

ポイント

  • チャネルに送信するとき、別のゴルーチンが受信するまで送信側のゴルーチンは待たされる(ブロック)
  • チャネルから受信するとき、別のゴルーチンが送信するまで待たされる(ブロック)

先程のサンプルが上手く動くのは、このブロックが働いているから。

  • メインゴルーチンでは、チャネルから受信しようとしているため、無名ゴルーチンから送信されるまで待機
  • 無名ゴルーチンでは、チャネルに値を送信しようとしているためメインゴルーチンが受信するまで待機
package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan string)
	go func() {
		time.Sleep(3 * time.Second)
		// チャネルに値を送信
		ch <- "Hello, Channel!!"
	}()

	// チャネルから値を受信
	// チャネルに値が入るまで待機する
	// => 無名ゴルーチンが終わるまで、メインゴルーチンは終了しない
	fmt.Println(<-ch)
}

このような特徴があるため、バッファなしチャネルは「同期チャネル」とも言われる。

デッドロック

先程のサンプルを、チャネルに送信しないように書き換えるとデッドロックが起こる。

※ サンプルでは送信しないパターンですが、受信しない場合にもデッドロックは起こる。

package main

import (
	"fmt"
)

func main() {
	ch := make(chan string)
	go func() {
		// 必ずreturn
		// チャネルに送信することなく、ゴルーチンは終了する
		if true {
			return
		}
		// チャネルに値を送信
		ch <- "Hello, Channel!!"
	}()

	// チャネルから値を受信
	fmt.Println(<-ch)
}

実行するとfatal error: all goroutines are asleep - deadlock!

何が起こっているかというと、

  • メインゴルーチンでチャネルから受信しようとしている → 送信されるまで待機
  • しかし、無名ゴルーチンはチャネルに送信することなく終了
  • 無名ゴルーチンが終了したとき、Goはすべてのゴルーチンが休止していることを検知
    = チャネルに送信操作が実行される可能性ゼロ

デッドロック!!

↓の例の方がわかりやすいかも。

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan string)
	go func() {
		// デッドロックを起こす
		// 必ずreturn。チャネルに送信することなく、ゴルーチンは終了する
		if true {
			fmt.Println("goroutine1 done")
			return
		}
		// チャネルに値を送信
		ch <- "Hello, Channel!!"
	}()

	go func() {
		time.Sleep(3 * time.Second)
		fmt.Println("goroutine2 done")
	}()

	// チャネルから値を受信
	// 2つのゴルーチンが終了したとき、チャネルに送信するゴルーチンが存在しないためデッドロックが起こる
	fmt.Println(<-ch)
}

実行結果は次のようになる。

goroutine1 done
goroutine2 done
fatal error: all goroutines are asleep - deadlock!
  • 1つ目の無名ゴルーチンがすぐに終了
  • 2つ目の無名ゴルーチンが3秒スリープしたのち終了
  • すべてのゴルーチンが終了し、チャネルに送信される可能性がなくなったのでデッドロック

 

チャネルを閉じる(close)

チャネルを閉じると、もうこれ以上チャネルに値が送信されない状態を示すことができる。

チャネルの閉じ方は close(ch) とするだけ。

チャネルが閉じているかは、受信時の2つ目の戻り値で判別可能
閉じられたチャネルから受信したとき、2つ目の戻り値が false になる。

package main

import "fmt"

func main() {
	ch1 := make(chan string)
	ch2 := make(chan string)

	go func() {
		ch1 <- "A"
		close(ch2)
	}()

	value1, ok1 := <-ch1
	value2, ok2 := <-ch2
	fmt.Printf("value1: %s, ok1: %t\n", value1, ok1) // value1: A, ok1: true
	fmt.Printf("value2: %s, ok2: %t\n", value2, ok2) // value2: , ok2: false
}

rangeを使ったforループと組み合わせると便利。

チャネルから値を受信して、閉じられたらループを自動的に終了してくれる!!

package main

import "fmt"

func main() {
	ch := make(chan int)
	go func() {
		defer close(ch)
		for i := 0; i < 10; i++ {
			ch <- i
		}
	}()

	for v := range ch {
		fmt.Println(v)
	}
}

チャネルに値を送信したとき、その値を受信できるのは1つのゴルーチンのみだが、閉じられたチャネルは何度でも受信できる

この特徴を利用して、複数のゴルーチンにシグナルを送る仕組みも作れる。

package main

import (
	"fmt"
	"sync"
)

func main() {
	begin := make(chan struct{})
	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1)
		// ループでゴルーチンを5つ実行
		go func(i int) {
			defer wg.Done()
			// チャネルを受信
			<-begin
			// なにか処理をする
			fmt.Printf("%d do somthing...\n", i)
		}(i)
	}

	fmt.Println("close channel")
	close(begin)
	wg.Wait()
	// ↓↓実行結果↓↓

	// close channel
	// 1 do somthing...
	// 0 do somthing...
	// 2 do somthing...
	// 3 do somthing...
	// 4 do somthing...
}

チャネルのclose

すべてのチャネルを閉じる必要はない。受信しているゴルーチンへすべてのデータが送信されたことを通知する必要があるときのみチャネルを閉じる必要がある。

ガベージコレクタが到達不可能と判断したチャネルは、閉じられているかに関わらず資源が回収される。

 

バッファなしチャネル まとめ

ポイント

  • チャネルに送信するとき、別のゴルーチンが受信するまで送信側のゴルーチンは待たされる(ブロック)
  • チャネルから受信するとき、別のゴルーチンが送信するまで待たされる(ブロック)
  • 他のゴルーチンが終了しても、送受信が実行されなかったらデッドロックが起こる
  • チャネルの受信には range を使うと便利
  • チャネルを閉じることで、これ以上送信されないことを示せる
  • チャネルを閉じることで複数のゴルーチンにシグナルを送ることができる

 

参考図書

Tour of Goを終えた人にオススメです!!

イチオシ記事

1

自己紹介 フリーランスエンジニアをしているヨノと申します。 独学でプログラミングを学び、ソシャゲ・SaaS開発などを経て、2018年からフリーランスエンジニアとして活動しています。 主にバックエンド中 ...

2

はじめまして、フリーランスエンジニアのヨノと申します。 自己紹介 独学でプログラミングを学び、ソシャゲ・SaaS開発などを経て、2018年からフリーランスエンジニアとして活動しています。 主にバックエ ...

3

ネット上で色々言われているフリーランスエンジニア....。「本当はどうなの?」と思っている人は多いでしょう。 そこで本記事ではフリーランスエンジニア5年生の私が、ネット上の意見も引用しながら実態を解説 ...

-Go言語