LoginSignup
55
48

【Go】基本文法⑦(並行処理)

Last updated at Posted at 2018-08-30

概要

A Tour of Goの順番に沿ってGoの基本で個人的に学習したことをまとめています。

No 記事
1 【Go】基本文法①(基礎)
2 【Go】基本文法②(フロー制御文)
3 【Go】基本文法③(ポインタ・構造体)
4 【Go】基本文法④(配列・スライス)
5 【Go】基本文法⑤(Maps・ Range)
6 【Go】基本文法⑥(インターフェース)
7 〜〜 【Go】基本文法⑦(並行処理)「今ココ」〜〜
8 【Go】基本文法総まとめ

今回は Goroutine(ゴルーチン)Channel に焦点を当てて学習しました。

goroutine(ゴルーチン)とは

goroutine(ゴルーチン)とは、Go言語のプログラムで並行に実行されるもののことです。

goroutine(ゴルーチン)の起動

Goの場合、関数 (またはメソッド) の呼び出しの前に go を付けると、異なる goroutine で関数を実行することができます。

go 関数名(引数, ...)

goは新しいgoroutineを生成して、そのgoroutine内で指定された関数を実行します。
新しいgoroutineは並行に動作するので、関数の実行終了を待つことなく、goのあとに記述されているプログラムを実行します。

goroutine(ゴルーチン)の終了条件

goroutine(ゴルーチン)は下記の条件で終了します。

  • 関数の処理が終わる。
  • returnで抜ける。
  • runtime.Goexit() を実行する

存在するgoroutine(ゴルーチン)の数の取得方法

runtime.NumGoroutine()を使用する事で現在起動しているgoroutine(ゴルーチン)の数を知ることができます。

import( 
 "fmt"
 "log"
 "runtime"
)

func main() {
     log.Println(runtime.NumGoroutine())
}

goroutine(ゴルーチン)の実例

実際のコードを見る事でよりgoroutine(ゴルーチン)を具体的に捉えます。
以下のプログラムは1秒間隔でstrnum回表示します。

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("Start!")
	 process(2,"A")
	 process(2,"B")
	fmt.Println("Finish!")
}

func process(num int, str string) {
	for i := 0; i <= num; i++ {
	 time.Sleep(1 * time.Second)
	 fmt.Println(i, str)
	}
}

この例ではgoroutine(ゴルーチン)も何も使用していないので実行結果は以下の様になります。

Start!
0 A
1 A
2 A
0 B
1 B
2 B
Finish!

そこで、新しいgoroutineを2つ生成して関数process()を実行します

func main() {
	fmt.Println("Start!")
	go process(2,"A") //goキーワードで関数実行するとgoroutineが生成される
	go process(2,"B")
	fmt.Println("Finish!")
}

func process(num int, str string) {
	for i := 0; i <= num; i++ {
	 time.Sleep(1 * time.Second)
	 fmt.Println(i, str)
	}
}

すると実行結果は以下の様になりました。

Start!
Finish!

作成されたgoroutineの終了を待たずmainが終了しプログラム全体の処理を終了したため、 関数process()の実行結果は得られませんでした。
関数process()の実行結果を得るためには、maingoroutineが終了するまで待たなければいけません。
これはchannel(チャネル)を使う事で簡単に実現できます。

channel(チャネル)とは

ゴルーチン間(main含む)で連携するには、channel(チャネル)と呼ばれる機能を利用します。
channel(チャネル)は、値の交換および同期という通信機能を兼ね備えており、2つの計算処理(ゴルーチン)が予期しない状態とならないことを保証します。

channel(チャネル)の生成方法

チャネルは Go 言語に標準で用意されているデータの一つで、スライスと同じタイプのデータ構造です
スライスの様に組み込み関数 make()を使用する事で生成できます。

ch := make(chan )
ch := make(chan , バッファサイズ)

チャネルに指定するバッファサイズは、チャネルにバッファ可能な容量です。このサイズが送信データの上限となります。

#channel(チャネル)での値の送受信方法
channel(チャネル)を使用するには、チャネル型の変数を作成し、送信側・受信側ともに、その変数に対して何らかのデータを送受信します。
以下の例の様にチャネルオペレータの <- を用いる事で値の送受信ができます。

ch <- data    //dataをchへ送信する(vをchに書き込む)
arg := <-ch  //chから受信した変数をargへ割り当てる(chの値を読み込む)

送信がchannel<-valueで受信が<-channelです。
以下のコードがchannel(チャネル)での値の送受信の実例です。

func main() {
	//channelの作成
    messages := make(chan string)
	
	//作成されたchannelに値(str)を送信
    go func() { messages <- "str" }()
	
	//channelから値を受信
    msg := <-messages
    fmt.Println(msg) //=> str
}

ゴルーチンの同期

Goでは__受信側では常に、受信可能なデータが来るまでブロックされます。__
また、送信側はチャネルがバッファリングしていないときは、受信側が値を受信するまでブロックされます。
これにより、Goでは明確なロックや条件変数がなくても、goroutineの同期を可能にします。

ゴルーチンの同期(例①)

いくつかの実例から上記の意味を理解します。

func main() {
  ch := make(chan bool)  //bool型のchannelを作成

	// ゴルーチンとして以下の関数を起動。完了時にchannelの型であるboolの値を送信する事で、チャネルへ通知。
	go func() {
		fmt.Println("Hello")
		ch <- true  // 通知を送信。値は何でも良い(boolの型であれば)
	}()

  <-ch //=>Hello   
	// channelの型であるboolの値を受け取るまでの完了待ち。送られてきた値は破棄
}

上記ではbool型のchannelchを作成し、ゴルーチンとして関数func()を起動しています。
func()内ではchの型であるboolの値を与えています。
Goでは受信側では常に受信可能なデータが来るまでブロックされるので、main内でchの型であるboolの値を受け取るまで完了待ちしています。

ゴルーチンの同期(例②)

次に以下の例もみてみます。

func hello(done chan bool) {  
    fmt.Println("Hello world goroutine")
    done <- true
}

func main() {  
         //bool型のchannelのdoneを生成する。
    done := make(chan bool)
	
	//生成したdoneを関数helloに渡す
	go hello(done)
    
	<-done
	//main
    fmt.Println("main function")

} 

上記の例ではbool型のchanneldoneを作成し、関数hello()に渡しています。
これによってmain内で<-doneが呼ばれた際に、関数hello()でbool型の要素が渡されるまで完了待ちをしています。

ゴルーチンの同期(例③)

ここで再びgoroutine(ゴルーチン)の実例で作成した、以下のコードについてみていきます。

func main() {
    fmt.Println("Start!")
    go process(2,"A") //goキーワードで関数実行するとgoroutineが生成される
    go process(2,"B")
    fmt.Println("Finish!")
}

func process(num int, str string) {
    for i := 0; i <= num; i++ {
     time.Sleep(1 * time.Second)
     fmt.Println(i, str)
    }
}

現状では、作成されたgoroutineの終了を待たずmainが終了しプログラム全体の処理を終了したため、 関数process()の実行結果は得られませんでした。
これをchannelに関して学んだ知識を踏まえて以下の様に書き換えます。

func main() {
	ch1 := make(chan bool)
	ch2 := make(chan bool)
   
	fmt.Println("Start!")
	
    go func() {
		process(2,"A")
    	ch1 <- true
    }()
	
	go func(){
		process(2, "B")
		ch2 <- true
	}()
	
	<-ch1
    <-ch2
	
    fmt.Println("Finish!")
}

func process(num int, str string) {
    for i := 0; i <= num; i++ {
     time.Sleep(1 * time.Second)
     fmt.Println(i, str)
    }
}

上記ではbool型のchannelであるch1ch2を作成し、main<-ch1<-ch2が呼ばれると、bool型の要素を受信するまで完了待ちをしています。
すると、関数内でprocess()が評価されるので、期待通りの実行結果を得る事ができました。

Start!
0 A
0 B
1 B
1 A
2 A
2 B
Finish!

参考

My Journey of Go ⑦
Goの並列処理の動作を理解する
Go の並行処理
お気楽 Go 言語プログラミング入門
Goの並行処理 -基本のキ-
Go by Example
An introduction to using and visualizing channels in Go
GOLANGBOT.COM Part 22: Channels
GoのChannelを使いこなせるようになるための手引
ゴルーチンと並行性パターン
実践Go言語(part12)

55
48
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
55
48