📘

Go ゴールーチン、チャネル、コンテキスト

2023/09/10に公開

並行と並列

並行処理(Concurrency)

同時にいくつかの質の異なることを扱うことを指す。
(一人がファイルを読み込んでいて、一人がファイルに書き込んでいる イメージ。)

並列処理(Parallelism)

同時にいくつかの質の同じことを扱うことを指す。
(全員がファイルに書き込んでいる イメージ。)

https://zenn.dev/hsaki/books/golang-concurrency/viewer

ゴールーチン と 並行処理(Concurrency)

ゴールーチンは軽量なスレッドのようなもの。(※正確ではない)
Linux や Unix のスレッドよりもコストが低い。(→ 軽い)
1 つのスレッドの上で複数のゴールーチンが動く

複数のゴールーチンで、同時に複数のタスク(質の異なること)を行う。

使うには、goキーワードをつけて関数を呼び出すだけ。

ゴールーチンの作り方
go f()

チャネル

複数のゴールーチン間で値を共有したいとき、片方からもう片方へチャネル(経路)を通して共有する

チャネルはファーストクラスオブジェクト。(string とか int とか同じ)
→ 変数への代入や、引数に渡すこと、返り値にすることができる。

<基本>

定義方法

  • チャネルの作成 : ch = make(chan int)
    送受信できる値の型を定義する。
  • 送信 : ch<-100
  • 受信 : n := <-ch

ブロック

(チャネルの)送信側はその値をだれかが受け取ってくれるまで
受信側チャネルから何かを受け取るまで以降の処理には進まない

<select チャネル>

select チャネルによって、複数のチャネルの先に受信したほうのデータを使うという処理が書ける。

func main() {
  ch1 := make(chan int)
  ch2 := make(chan string)
  go func() { ch1 <- 100 }()
  go func() { ch2 <- "hi" }()

  select {
  case v1 := <-ch1:
    fmt.Println("v1:", v1)
  case v2 := <-ch2:
    fmt.Println("v2:", v2)
  }
}

<nil チャネル>

チャネルのゼロ値は nil。
nil のチャネルから受信しようとすると、永遠にブロックされる。

func main() {
  ch1 := make(chan int)
  var ch2 chan string // ゼロ値 nil
  go func() { ch1 <- 100 }()
  go func() { ch2 <- "hi" }()

  select {
  case v1 := <-ch1:
    fmt.Println("v1:", v1)
  case v2 := <-ch2:
    // ch2がnilである間は、この処理は実行されない
    fmt.Println("v2:", v2)
  }
}

<単方向チャネル>

チャネルは双方向なので、送信用として作ったつもりでも誤って受信に使ってしまったりする可能性がある。

単方向(受信 or 送信)チャネルによってそういった誤った使い方ができなくなるよう制限できる。

func plusOne(recv <-chan int) int {
  // recvは受信用なので、送信には使えない
  v := <-recv + 1
  return v
}

func main() {
  ch := make(chan int)
  go func(ch chan<- int) {
    // chは送信用なので、受信には使えない
    ch <- 100
  }(ch)
  fmt.Println(plusOne(ch))
}

コンテキスト

コンテキストとは、下記を行うためのもの。

  • ゴールーチンをまたいだキャンセル処理
  • ゴールーチンをまたいだ値の共有(チャネルでなくコンテキストで行いたい場合)

コンテキストは木構造みたいになっていて、必ずrootが存在し、新たなコンテキストはその上にラップしている。

<コンテキストでキャンセル>

func main() {
  gen := func(ctx context.Context) <-chan int {
    dst := make(chan int)
    n := 1

    go func() {
      for {
        select {
        // 引数に受け取っているコンテキストの状態をctx.Done()で見ることによって、
        // それがキャンセルされたのかを確認
        case <-ctx.Done():
          return
        case dst <- n:
          n++
        }
      }
    }()
    return dst
  }

  // rootコンテキスト
  bc := context.Background()
  // rootコンテキストから作成した、キャンセル機能を持つコンテキスト
  ctx, cancel := context.WithCancel(bc)

  // 下のforループが終わったら、コンテキストをキャンセルして、チャネルを閉じる
  defer cancel()

  for n := range gen(ctx) { // forループには、チャネル(gen()の返り値)も取れる
    fmt.Println(n)
    if n == 5 {
      break
    }
  }
}

Discussion