🤖

Golang goroutine

2021/09/01に公開

コンカレンシーへのアプローチ

2つのスタイル

Go には、コンカレンシー(並行プログラム)を作成するためのスタイルが 2 つある。

  • 一つは他の言語同様のスレッドを使用する従来のスタイル。
  • もう一つはgoroutine という独立したアクティビティ間で値が渡される Go独自のスタイル。

Go独自のスタイル goroutineの概念

コンカレンシー(並行プログラム)を作成する際の最大の問題は、プロセス間でデータを共有すること。
Goでは通信を介してデータがやり取りされ、データにアクセスできるのは 1 つのアクティビティ (goroutine) のみ。この為、設計上、競合状態は発生しない。
この概念は、Effective Goには以下の様に要約されている。

メモリを共有して通信しないでください。代わりに、通信してメモリを共有してください。

goroutineの構文

go some_func()

ラムダ関数で使用する事が好まれる

go func() {
    some_func()
}()

チャネル

goroutine から別の goroutine に値を送信するには、チャネルを使用する

構文

# make(chan <チャネルを通過するデータ型>)
ch := make(chan int)

送受信

チャネル内のデータ送信とデータ受信はブロック操作

ch <- x // チャネルを通してxを送信
x = <-ch // チャネルを通してデータを受信し、xで受け取る
<-ch // データを受け取るが破棄する

閉じる

close(ch)

閉じたチャネルからデータを受信しようとすると、送信されたすべてのデータを読み取ることができる

最低限の使用例

ch := make(chan string)

go func() {
	ch <- "bar"
}()

fmt.Println(<-ch)

実用的な使用例

package main

import (
	"fmt"
	"net/http"
	"time"
)

func main() {
	start := time.Now()

	apis := []string{
		"https://api.github.com",
		"https://api.rstliz.com/",
		"https://api.twitter.com/",
	}

	ch := make(chan string)

	for _, api := range apis {
		go callAPI(api, ch)
	}
	for i := 0; i < len(apis); i++ {
		fmt.Print(<-ch)
	}

	elapsed := time.Since(start)
	fmt.Printf("経過時間 %v 秒\n", elapsed.Seconds())
}

func callAPI(api string, ch chan string) {
	_, err := http.Get(api)
	if err != nil {
		ch <- fmt.Sprintf("ERROR: %s\n", api)
		return
	}

	ch <- fmt.Sprintf("SUCCESS: %s\n", api)
}

チャネルのバッファ

チャネルにはバッファのあるチャネルとないチャネルがある。
make()関数の既定の動作では、チャネルを作成するとバッファなしのチャネルが作成される。
バッファーなしのチャネルでは、送信操作は誰かがデータを受信する準備ができるまでブロックされる。
バッファーありのチャネルでは、プログラムがブロックされずにデータが送受信される。

バッファありチャネルの作成

# make(chan <チャネルを通過するデータ型>, <キューサイズ>)
ch := make(chan string, 10)
  • バッファーありのチャネルはキューのように動作する
  • チャネルに何かを送信するごとに、要素がキューに追加され、受信操作によって要素がキューから削除される。
  • チャネルの空き領域が少なくなると、データを保持する領域ができるまで、送信操作は待機される
  • チャネルが空で読み取り操作がある場合、読み取る対象が発生するまでチャネルはブロックされる。

チャネルはgoroutineと緊密に接続している

goroutineを使わずにチャネルを使用すると、キューに要素が追加されず、無限にブロックされる

size := 2
ch := make(chan string, size)
ch <- "one"
ch <- "two"
ch <- "three"

for i := 0; i < size; i++ {
    fmt.Println(<-ch)
}
//fatal error: all goroutines are asleep - deadlock!

goroutineを用いるとキューに要素が追加され、期待通りに動く

size := 2
ch := make(chan string, size)
ch <- "one"
ch <- "two"
go func() {
	ch <- "three"
}()

for i := 0; i < 3; i++ {
	fmt.Println(<-ch)
}

バッファーなしとバッファーありのチャネル

バッファーなしのチャネルの場合、同期的に通信される。 データを送信するたびに、チャネルから読み取られるまでプログラムがブロックされることが保証される
バッファーありのチャネルの場合、送信操作と受信操作は分離される。プログラムはブロックされないが、デッドロックが発生する可能性がある

チャネルの方向を定義する

関数パラメーターでチャネルを宣言するときに行う

chan<- int // 送信専用チャネル
<-chan int // 受信専用チャネル

func send(ch chan<- string, message string) {
   ch <- message
}
func read(ch <-chan string) {
   fmt.Printf("受信: %#v\n", <-ch)
}

受信チャネルに送信などの誤った使い方をするとコンパイルエラーとなる。

多重化

チャネル用のswitch構文、select。

func foo(ch chan string) {
    time.Sleep(3 * time.Second)
    ch <- "Foo"
}

func bar(ch chan string) {
    time.Sleep(1 * time.Second)
    ch <- "Bar"
}

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

    for i := 0; i < 2; i++ {
        select {
        case foo := <-ch1:
            fmt.Println(foo)
        case bar := <-ch2:
            fmt.Println(bar)
        }
    }
}
GitHubで編集を提案

Discussion