0
Help us understand the problem. What are the problem?

posted at

【A Tour of Go】 並行処理編

公式の A Tour of Go ではじめて Go 言語を触ってみました。

基本編

メソッド編

インターフェース編

以下、並行処理について学んだことのメモ。

Goは言語のコア機能の一部として並行処理機能を提供する(!)

goroutine

  • Go のランタイムによって管理される軽量なスレッド
    • ここでいう”軽量なスレッド”とは、OSによって提供されるスレッドそのものではなく、Goのランタイムが独自に実現している並行処理の仕組み
    • 並行処理における性能の序列
      • 子プロセス << OSのスレッド << goroutine
  • 新しい goroutine の実行: go f(x, y, z)
    • f, x, y, z の評価: 実行元の goroutine
    • f の実行: 新しい goroutine
package main

import (
	"fmt"
	"time"
)

func say(s string) {
	for i := 0; i < 3; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println(s)
	}
}

func main() {
	go say("goroutine") // 新しい"軽量スレッド(goroutine)"での実行
	say("current")
	
	// 出力結果(実行する度に異なる)
	
	// goroutine
	// current
	// current
	// goroutine
	// goroutine
	// current
	
	// current
	// goroutine
	// current
	// goroutine
	// current
	//           <-- 実行元の goroutine が終了(新しい goroutine の終了を待たない)
}

チャネル

チャネルの用途

  • goroutine は同じアドレス空間で実行されるため、共有メモリへのアクセスは必ず同期する必要がある
  • チャネルは goroutine 間での同期を可能にする
  • チャネルの宣言: ch := make(chan int)
    • chan 型がある
  • チャネルを介した値の送受信
    • v をチャネル ch へ送信する: ch <- v
    • ch から受信した変数を v へ割り当てる: v := <-ch
  • 片方の goroutine が準備できるまで、送受信はブロックされる
    • 明確なロックや条件変数なしに goroutine 間の同期を可能にする
package main

import "fmt"

func sum(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	c <- sum // 合計結果をチャネルに送信
}

func main() {
	s := []int{7, 2, 8, -9, 4, 0}

	// int の chan 型を生成
	c := make(chan int)
	
	go sum(s[:len(s)/2], c) // 前方半分の合計処理を担当する goroutine
	go sum(s[len(s)/2:], c) // 後方半分の合計処理を担当する goroutine
	
	x, y := <-c, <-c // チャネル経由でそれぞれの合計結果を受信する

	fmt.Println(x)	// -5 (後方半分の合計結果)
	fmt.Println(y)	// 17 (前方半分の合計結果)
	fmt.Println(x+y)
}

バッファ付きチャネル

  • チャネルはバッファとして使うことができる
  • バッファ付きチャネルの宣言: ch := make(chan int, 100)
    • 第2引数がバッファの長さ
  • バッファが詰まった時 => チャネルへの送信をブロック
  • バッファが空になった時 => チャネルからの受信をブロック
package main

import "fmt"

func main() {
	ch := make(chan int, 2)
	
	ch <- 1	// 送信+1 => 計1
	ch <- 2 // 送信+1 => 計2
	
	fmt.Println(<-ch) // 受信-1 => 計1
	
	ch <- 3 // 送信+1 => 計2
	
	ch <- 4 // deadlock! (バッファが詰まってしまった)
}

チャネルの close

  • 値の送信側は、これ以上の送信する値がないことを示すために、チャネルを閉じることができる: close(c)
  • 値の受信側は、2つ目の戻り値の真偽値をチェックすることで、チャネルが閉じているかどうかを知ることができる: v, ok := <-ch
    • 受信する値がない かつ チャネルが閉じている => false
    • それ以外 => true
  • for i := range c は、チャネルが閉じられるまで繰り返し値を受信し続けようとする
  • 注意
    • 送信側だけがチャネルを閉じるべきであって、受信側は決してチャネルを閉じてはいけない
      • 閉じたチャネルに値を送信すると panic が生じる
    • チャネルはファイルと違って閉じる必要はない
      • これ以上の値が来ないことを受信側が知る必要のあるときに閉じる(例: for i:= range c を終了したい場合)
package main

import (
	"fmt"
)

// 送信側
func fibonacci(n int, c chan int) {
	x, y := 0, 1
	for i := 0; i < n; i++ {
		c <- x
		x, y = y, x+y
	}
	close(c) // ここで close し忘れると panic!
}

func main() {
	c := make(chan int, 10)
	
	go fibonacci(cap(c), c)
	
	// 受信側
	for i := range c { // close するまで値を受信し続ける
		fmt.Println(i)
	}
}

select

  • 送受信の準備ができているチャネルの case を実行する
  • 複数のチャネルで送受信の準備ができている場合、ランダムに1つの case が選ばれる
  • チャネルで送受信の準備ができていると判定できる例:
    • 送信
      • バッファが詰まっていない
    • 受信
      • チャネルから値を取り出し可能
      • チャネルが閉じられた
package main

import "fmt"

func fibonacci(c, quit chan int) {
	x, y := 0, 1
	
	for {
		select {
			
		case c <- x: // x を c チャネルに送信
			x, y = y, x+y
			
		case <-quit: // quit チャネルから値を受信
			fmt.Println("quit")

			return // 無限ループ終了
		
		}
	}
}

func main() {
	c := make(chan int)
	quit := make(chan int)

	// goroutine
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)
		}
		quit <- 0  // quitチャネルに値を送信 => `case <-quit`準備完了
	}()
	
	fibonacci(c, quit)
}

// 出力結果
// 0
// 1
// 1
// 2
// 3
// 5
// 8
// 13
// 21
// 34
// quit
  • どのチャネルも準備できていない場合、default が実行される
package main

import (
	"fmt"
	"time"
)

func main() {
	tick := time.Tick(100 * time.Millisecond)
	boom := time.After(500 * time.Millisecond)
	for {
		select {
		case <-tick:
			fmt.Println("tick.")
		case <-boom:
			fmt.Println("BOOM!")
			return
		default:
			fmt.Println("    .")
			time.Sleep(50 * time.Millisecond)
		}
	}
}

//     .
//     .
// tick.
//     .
//     .
// tick.
//     .
//     .
// tick.
//     .
//     .
// tick.
//     .
//     .
// BOOM!

おまけ: "goroutine" は「ゴルーチン」?

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
0
Help us understand the problem. What are the problem?