Slide 1

Slide 1 text

並行処理入門 Goで遊ぶ すとんりばー

Slide 2

Slide 2 text

自己紹介 名前:すとんりばー Twitter : @strvert Facebook : strvert Web : strvworks.github.io 2

Slide 3

Slide 3 text

並行処理とは What is the concurrent processing

Slide 4

Slide 4 text

並行処理とは  並行処理と聞いて、「並列処理じゃないの?」と 思った方がいるんじゃないでしょうか。  並行処理と並列処理は字面こそ似ていますが、それ ぞれ異なる定義がなされている用語です。 4

Slide 5

Slide 5 text

並行処理とは  ざっくりと言うと、並行処理は「複数状態の並行性」 に焦点が置かれ、並列処理は「複数処理の並行性」に焦 点が置かれた概念であると言えます。 次ページから概念図を示します。 5

Slide 6

Slide 6 text

並列処理(parallel processing)  並列処理は実際に複数の 処理が複数のプロセッサな どによって行われている処 理を指します。 6

Slide 7

Slide 7 text

並行処理(concurrent processing)  対して並行処理は、処理 ごとの状態が並行して存在 し、人間の視点からみると 同時に実行されているよう に見える物も含みます。 逐次処理を高速にスイッチ ングしていると言うことも できます。 7

Slide 8

Slide 8 text

並行処理とは  などと説明すると、並列処理と並列処理にあらゆる処 理が分類可能なように思われてしまうかもしれません。  しかし、プログラマの書くソースコード上で、両者に 大きな差はありません。(GPGPUなどは特殊) 8

Slide 9

Slide 9 text

並行処理とは  つまり、並行性を持つソースコードをプログラマが記 述した時、それが実際に並行と並列の実行されるかはそ の実行環境に依存すると言うことができます。  同じ並行性を持ったコードであっても、その環境に よって利用可能なコアの数だけスケールして並列実行さ れたり、シングルコアで並行に実行されたりするという ことです。 9

Slide 10

Slide 10 text

並行処理とは  しかし、どちらで実行されるにせよ、一般的な逐次処 理を記述したソースコードでは効率的な並行/並列実行は 行われません。  少なくとも現状では、プログラマが意識して並行性を 持ったソースコードを記述することが求められていま す。 10

Slide 11

Slide 11 text

並行処理の必要性 Necessity of concurrent processing

Slide 12

Slide 12 text

並行処理の必要性  そもそもなぜ並行処理のコードを記述する必要がある のでしょうか。  なお、ここでいう並行処理のコードとは実行環境に よって並行実行される場合と並列実行される両方の可能 性があります。 12

Slide 13

Slide 13 text

並行処理の必要性  身近な並行処理のコード記述に、GUIソフトウェアが 挙げられます。  例えば画像処理などの比較的重い処理を行うGUIソフ トウェアにおいて、ソフトウェア内部で画像処理が行わ れている途中にGUIがフリーズしないのは、画像処理の スレッドとGUIのスレッドが並行して動作しているから です。 13

Slide 14

Slide 14 text

並行処理の必要性  もっと重要で普段からお世話になっている並行処理を 実現しているソフトウェアとして、OSも挙げることがで きます。  普段利用するOS上で、CPUのコア数よりも多く並行し てソフトウェア(プロセス)を起動できるのは、OSがCPU の計算資源を複数のソフトウェアに分配してくれている からです。→コンテキストスイッチ 14

Slide 15

Slide 15 text

並行処理の必要性  また、並列して処理を実行可能な環境において、複数 の演算器(コアなど)を用いて演算を高速化することを目 的とした並列アルゴリズムも考案されています。  容易に並列化が可能で、並列化された処理同士の依存 性をほぼ無くすことが可能な問題を、「驚異的並列」と 呼んだりします。 →円周率計算/画像の幾何学変換/データベース検索/数値計算の区間分割 15

Slide 16

Slide 16 text

 今回は、Goのサンプルコードと共に、並行処理の基礎 についてお話します。  Goの軽い解説はしますが、並行処理の例としてですの で細部を理解する必要はありません。 並行処理の必要性 16

Slide 17

Slide 17 text

ひとくちGo HITOKUTI Go

Slide 18

Slide 18 text

ひとくちGo  Goは並行処理に強い言語であり、他の言語と比較して 簡単に並行処理を記述することができます。(後述)  とりあえずサンプルコードが読めればいいので、ざっ くりとした話だけをします。まずは関数です。 18

Slide 19

Slide 19 text

関数 Goは通常の関数以外に無名 関数を利用することができ ます。無名関数は一般の関 数と同等に扱えます。 出力はどうなるでしょうか 19 // hello関数宣言 func hello() { fmt.Println("Hello.") } // main関数宣言(エントリポイント) func main() { // 無名関数を変数に bye := func() { fmt.Println("Bye.") } // 呼び出し hello() bye() // 無名関数呼び出し func() { fmt.Println("Hai.") }() }

Slide 20

Slide 20 text

関数 こうなります(自明) 脳死ですね。 20 Hello. Bye. Hai.

Slide 21

Slide 21 text

ゴルーチン(簡易説明) ゴルーチンは、Goのスレッ ドのようなものです。(後述) なんと関数呼び出しに"go"を つけるだけで、独立して実行 されます。 検証のため、sleepを入れて います。 21 func hello() { time.Sleep(time.Second * 2) // 2秒 sleep fmt.Println("Hello.") } func main() { bye := func() { time.Sleep(time.Second) // 1秒 sleep fmt.Println("Bye.") } // ゴルーチンとして関数呼び出し go hello() go bye() go func() { time.Sleep(time.Second * 3) // 3秒 sleep fmt.Println("Hey.") }() time.Sleep(time.Second * 4) // 4秒 sleep } 出力はどうなるでしょうか

Slide 22

Slide 22 text

ゴルーチン(簡易説明) 1秒おきに、ゴルーチンの 起動順とは異なる順で出力 されました。 ゴルーチンが独立して非同 期に実行されていることが わかります。 なお、mainもゴルーチンで す。 22 Bye. Hello. Hey.

Slide 23

Slide 23 text

ひとくちGo  とりあえずこれだけでOKです。あとは感覚で読めるは ずです。  後ほどGoの並行処理についてもう少し掘り下げた話を します。 23

Slide 24

Slide 24 text

並行処理の難しさ Concurrent processing difficulty

Slide 25

Slide 25 text

並行処理の難しさ  並行処理のプログラミングには、逐次処理の場合には 表面化しづらい多くの壁が存在します。  その多くが、並行して実行されるスレッド同士の競合 や、それらの実行・終了タイミングによるものです。  開放されずに残ったスレッドのリークが問題となるこ ともあります。  試しに1つ、簡単な例を出してみます。 25

Slide 26

Slide 26 text

実行タイミング ここに、並行処理を用いた サンプルコードを用意しま した。 先程紹介したGoの機能の範 疇ですが、ある問題が存在 します。 26 func main() { // ゴルーチンとして関数を起動 go func() { fmt.Println("CALL GOROUTINE!!") }() fmt.Println("MAIN GOROUTINE!!") } 出力はどうなるでしょうか

Slide 27

Slide 27 text

実行タイミング 正解は、 「MAIN GOROUTINE!!」 のみが出力されるです。 わかりましたか? 27 MAIN GOROUTINE!!

Slide 28

Slide 28 text

実行タイミング これは、並行処理が起動す るまでの時間が、直後の main関数の終了よりも遅 かったことで、標準出力が 行われるよりも前にプロセ ス自体が終了してしまった ことが原因です。 sleepを入れてみます。 28 func main() { // ゴルーチンとして関数を起動 go func() { fmt.Println("CALL GOROUTINE!!") }() fmt.Println("MAIN GOROUTINE!!") time.Sleep(time.Second) // 1秒 sleep } 出力はどうなるでしょうか

Slide 29

Slide 29 text

実行タイミング 想定したであろう出力が得ら れました。 しかし、Sleepで処理をブロッ キングすることはあくまで対 処療法であり、正解ではあり ません。環境によってゴルー チンの起動時間は異なるた め、本来は処理の終了を待っ て判断する必要があります。 29 CALL GOROUTINE!! MAIN GOROUTINE!!

Slide 30

Slide 30 text

並行処理の難しさ  これは勿論Goに限った話ではなく、(ピンポイントに対 策されていなければ)どんな並行処理でも起こりうるもの です。  次ページから、並行処理に関するさまざまな特性を紹 介します。 30

Slide 31

Slide 31 text

アトミック性  アトミック性とは、ある操作について、それを操作し ている場所から見て不可分である操作の性質のことで す。 (??????)    自分でも言っていてよくわからないので、次に例を示 します。 31

Slide 32

Slide 32 text

アトミック性 あるCPUの命令セットでは、メモリ値のインクリメント を行う時、以下の操作が行われるとします。 1. 値の読み込み 2. 読み込んだ値に1を加算 3. 結果を元の位置に書き込み 32

Slide 33

Slide 33 text

アトミック性 このインクリメント操作が並行して2つ呼び出され、以 下の順で実行されたとします。 1. Aによる値の読み込み 2. Aが読み込んだ値に1を加算 3. Bによる値の読み込み 4. Aが結果を元の位置に書き込み 5. Bが読み込んだ値に1を加算 6. Bが結果を元の位置に書き込み 33

Slide 34

Slide 34 text

アトミック性  この命令を実行したエンジニアは、インクリメント命 令を2つ並行で呼んだことで値が2加算されると考えたか もしれませんが、そうはなっていません。  AとBがそれぞれ0を読み出して同じ場所に1を重ね書き しただけです。結果として、Aの計算の結果は一切反映 されていません。 34

Slide 35

Slide 35 text

アトミック性  これは「インクリメント」という1つの操作に思われ る操作が、このコンピュータでは分割可能な複数の命令 で構成されていたことから、並行処理を行った場合に偶 然意図しない動作を引き起こしたとうことになります。  このような、ある操作のまとまりが分割可能で外部か ら介入や参照が可能な操作にはアトミック性がありませ ん。 35

Slide 36

Slide 36 text

アトミック性  今度は、CPUの命令セットに以下の1命令でインクリメ ントを行う命令がある場合を考えます。 1. あるメモリ位置の値をインクリメント 36

Slide 37

Slide 37 text

アトミック性 このインクリメント操作が並行して2つ呼び出され、以 下の順で実行されたとします。 1. Aがメモリ位置の値をインクリメント 2. Bがメモリ位置の値をインクリメント 37

Slide 38

Slide 38 text

アトミック性  この命令を実行したエンジニアは、インクリメント命 令を2つ並行で呼んだことで値が2加算されると考え、事 実どのような場合でも想定した動作が行われることがわ かります。  このような場合に、この操作はアトミック性があると 言えます。 38

Slide 39

Slide 39 text

クリティカルセクション  ここからわかる通り、並行処理を行う場合において並 行して実行される命令はアトミック性を持つことが重要 なのです。  そして、アトミック性が無く、並行して実行されると 破綻をきたしてしまう操作区間のことを、クリティカル セクションといいます。 39

Slide 40

Slide 40 text

クリティカルセクション このコードにはアトミック 性が無く、クリティカルセ クションが存在します。 実行してみましょう。 40 func main() { var wg sync.WaitGroup val := 0 // インクリメント無名関数 inc := func() { fmt.Println(val) val++ } const routinenum = 20 wg.Add(routinenum) // ゴルーチン起動数を登録 for i := 0; i < routinenum; i++ { // ゴルーチン呼び出し go func() { // ゴルーチン終了時に終了報告 defer wg.Done() inc() }() } wg.Wait() // 登録ゴルーチンの終了待ち fmt.Println("Finish.") } 出力はどうなるでしょうか

Slide 41

Slide 41 text

クリティカルセクション なんだかカオスなことになり ました。 本来は0~19が順不同で出力さ れるはずですが、重複する数 字も存在します。 出力その時の場合によって変 動し、不安定です。 確率でうまく行きます! 41 0 1 2 3 4 5 6 7 6 8 8 10 8 8 8 8 8 11 10 16 Finish.

Slide 42

Slide 42 text

クリティカルセクション このコードのクリティカルセ クションはここです。 ゴルーチンとして呼び出され る無名関数の中で同じスコー プの変数にアクセスが行われ ています。 メモリアクセス競合の可能性 があり、アトミック性があり ません。 42 func main() { var wg sync.WaitGroup val := 0 inc := func() { // クリティカルセクション fmt.Println(val)                val++                      } const routinenum = 20 wg.Add(routinenum) // ゴルーチン起動数を登録 for i := 0; i < routinenum; i++ { go func() { defer wg.Done() inc() }() } wg.Wait() // 登録ゴルーチンの終了待ち fmt.Println("Finish.") }

Slide 43

Slide 43 text

クリティカルセクション ここで、クリティカルセク ションの間に他のゴルーチン のメモリアクセスをロックす ることでアトミック性を持た せてみます。 43 func main() { var lock sync.Mutex var wg sync.WaitGroup val := 0 inc := func() { // スコープに存在する変数への他アクセスをロック lock.Lock()              defer lock.Unlock()              fmt.Println(val) val++ } const routinenum = 20 wg.Add(routinenum) for i := 0; i < routinenum; i++ { go func() { defer wg.Done() inc() }() } wg.Wait() fmt.Println("Finish.") }

Slide 44

Slide 44 text

クリティカルセクション アトミック性を保証するよう にすると、正常に動作するこ とがわかります。 実装上、出力が順番に並ぶと は限りません。 44 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Finish.

Slide 45

Slide 45 text

Mutex  ここではアトミック性の保証のためにGoのsync.Mutexに よるロックを利用しました。  Mutexは多くの言語から利用可能な排他制御/同期機構 で、Goではチャネルの利用が推奨されていますが、簡単な 例では理解しやすく、チャネルよりも高速です。  Goにおいても、パフォーマンスの要求されるクリティカ ルセクションではMutexの利用が推奨されています。 45

Slide 46

Slide 46 text

デッドロック  メモリのロックを利用することでアトミック性は保証 できましたが、メモリのロックによって発生する問題も 存在します。  デッドロックはその1種で、並行する処理がお互いの ある処理を無限に待機し合ってしまう状況を指します。 46

Slide 47

Slide 47 text

デッドロック  デッドロックにはCoffman conditionsという以下の4つ の発生条件が存在します。 ● 相互排他 ● 保持&待ち 47 ● 横取り不能 ● 循環待ち

Slide 48

Slide 48 text

デッドロック sumは、引数を2つ取って、 アトミック性を確保するため に受け取った第1引数を排他 的権利でロックします。 特定の引数で並行して起動す ると、お互いのロックする変 数を待ち合います。 これは外部から止めることが できません。 48 type value struct { mu sync.Mutex value int } func main() { var wg sync.WaitGroup sum := func(v1, v2 *value) { defer wg.Done() v1.mu.Lock() defer v1.mu.Unlock() time.Sleep(time.Second * 2) // 戦犯 v2.mu.Lock() defer v2.mu.Unlock() fmt.Printf("%d+%d=%d\n", v1.value, v2.value, v1.value+v2.value) } a := value{value: 10} // 値初期化 b := value{value: 20} wg.Add(2) go sum(&a, &b) // sum起動その1 go sum(&b, &a) // sum起動その2 wg.Wait() }

Slide 49

Slide 49 text

デッドロック 今回の場合、sleepによって変 数がロックされる時間が伸び ていることで、デッドロック の確率が上昇しています。 そして、これらの状況はデッ ドロックの条件に一致し、 デッドロックだという事がで きます。 49

Slide 50

Slide 50 text

ライブロック  無限に処理が待ち合って停止するデッドロックに対し て、無限に処理同士が避け合うことで結果として避けた 先で競合し続けるライブロックというものも存在しま す。  廊下で同じ方向に避け続けることで、動いている(生き ている)にもかかわらず先に進めない状況によく例えられ ます。 (実装例がちょっとだけ長いので割愛) 50

Slide 51

Slide 51 text

リソース枯渇  いかに処理が正常に動いていても、それが適切な形で 実装されているかどうかは別の話です。  ここまでメモリ同期(ロック)によってアトミック性を 保証する方法などを挙げてきましたが、ロックが行われ るということは他のスレッド(ゴルーチン)がそのメモリ 領域を利用する処理ができないということでもありま す。 51

Slide 52

Slide 52 text

リソース枯渇  これにより、ロックを広範囲に適用したコードは、こ まめにロックすべき箇所を分割して記述した場合と比較 して遥かに長くプロセッサ時間を専有してしまいます。  確かにアトミック性は重要ですが、その保証方法も考 える必要があるのです。 52

Slide 53

Slide 53 text

並行処理の難しさ  ここまで挙げてきたように、並行処理は便利な反面、 逐次処理の記述と比較して低いレイヤまでプログラマが 意識して記述する必要があります。結構なんとなく書い ても動いてしまったりするのですが、実はメモリリーク が発生していたり、正常に動いているように見えて実は メモリ競合で値が想定するものではなかったりなど、バ クの発見も一癖あったりします。 53

Slide 54

Slide 54 text

並行処理の難しさ  しかし!Goには、並行処理をより自然に記述するため の多くの機能が存在します。ここからはGoの布教です。  ここまでのコードは多くの言語に存在する機能を用い て記述してきましたが、ここからはGoに存在する並行処 理に関する機能を紹介します。 54

Slide 55

Slide 55 text

Goの並行処理 Concurrent processing of Go

Slide 56

Slide 56 text

Goの並行処理  多くのプログラミング言語には、OSスレッドに関する ライブラリが存在します。  そのため、それらを通してOSスレッドを操作し、並行 処理を用いたソフトウェアの作成が可能となっていま す。  つまり、一般的にはOSスレッドの概念を直接操作して 処理を実現しているということです。 →Python: threading, C: pthread, Java: Thread …… 56

Slide 57

Slide 57 text

Goの並行処理  対して、Goにはゴルーチンとチャネルという並行処理 のための概念が導入されており、それらが言語機能その ものにビルトインされています。  これにより、言語機能の上のライブラリとして構築さ れているOSスレッド管理よりも自然に、ライブラリ内の 複雑性を持つことなく並行処理を記述することができま す。 57

Slide 58

Slide 58 text

Goの並行処理  Goのゴルーチンとチャネルは、CSP(Communicating Sequential Processes)という並行処理の手法に基づいた概 念です。  一般的な並行処理では、スレッド間でデータをやりとり する時、共有メモリが用いられます。複数のスレッドが同 一のメモリ位置を参照することで、瞬時にデータをやりと りできるということです。前節までで利用してきたのもこ ちらです。 58

Slide 59

Slide 59 text

Goの並行処理  一方CSPでは、プロセス間のやりとりを通信に見立て ることで、通信用に確保した領域をトンネルのように用 いてデータを"送受信する"という考え方が採用されてい ます。  プロセスに対するI/Oであると捉えることもできます。 59

Slide 60

Slide 60 text

Goの並行処理  そして、CSPのプロセスに当たるのがゴルーチン、通 信経路に当たるのがチャネルです。 次ページから、いくつかGoの並行処理に関するコードを 見てみます。 60

Slide 61

Slide 61 text

ゴルーチン スレッドと類似のGo独自の 概念で、Goの最も基本的な 構成単位。 自動で環境に合わせてスケー ルし、並行や並列で動作す る。 "go"をつけるだけで起動可能 で非常に高機能。 61 func hello() { time.Sleep(time.Second * 2) // 2秒 sleep fmt.Println("Hello.") } func main() { bye := func() { time.Sleep(time.Second) // 1秒 sleep fmt.Println("Bye.") } // ゴルーチンとして関数呼び出し go hello() go bye() go func() { time.Sleep(time.Second * 3) // 3秒 sleep fmt.Println("Hey.") }() time.Sleep(time.Second * 4) // 4秒 sleep }

Slide 62

Slide 62 text

ゴルーチン 遅延の短い順に出ています 62 Bye. Hello. Hey.

Slide 63

Slide 63 text

ゴルーチンの特性  ゴルーチンはとても簡単に使えるだけでなく、非常に 軽量であるという特徴も持ち合わせています。なんとゴ ルーチンは1つにつき数キロバイトしかスタックメモリ を確保しません。  Windowsのスレッドが1MB、POSIXが2MBであること を考えるとかなり小さな値です。 63

Slide 64

Slide 64 text

ゴルーチンの特性  また、OSスレッドのOSコンテキストスイッチと比較し て、ゴルーチンのコンテキストスイッチは90%近く高速 に動作するため、コンテキストスイッチによってプロ セッサ時間を占領してしまう心配も小さく済みます。 64

Slide 65

Slide 65 text

チャネル チャネルは、主に並行プロセ スが通信に利用するための キューです。 makeの引数で格納可能数を指 定できます。 "<-"をチャネルの左右に置く ことで、チャネル内部の キューと値を送受信します。 65 func f(ch chan int) { defer close(ch) ch <- 10 ch <- 15 } func main() { ch := make(chan int, 1) go f(ch) fmt.Println(<-ch) fmt.Println(<-ch) // ゴルーチン起動前に終了してしまわないか? } 出力はどうなるでしょうか

Slide 66

Slide 66 text

チャネル "<-ch"のようにチャネルか ら値を受信しようとする と、値を受信するまでその 場で受信側のゴルーチンを 自動ブロックして待機して くれるため、タイミングを 考慮する必要がありませ ん。 66 10 30

Slide 67

Slide 67 text

チャネル 先程デッドロックを起こして いたコードをチャネルを使っ て実装してみました。 67 func main() { var wg sync.WaitGroup sum := func(ch chan int) { defer wg.Done() time.Sleep(time.Second * 2) v1 := <-ch v2 := <-ch fmt.Printf("%d+%d=%d\n", v1, v2, v1+v2) } ch := make(chan int, 2) wg.Add(2) a, b := 10, 20 go sum(ch) go sum(ch) ch <- b ch <- a ch <- a ch <- b wg.Wait() } 出力はどうなるでしょうか

Slide 68

Slide 68 text

チャネル 実行2秒後に一斉に右の出 力が得られました。 メモリを共有していないた めメモリをロックする必要 がなく、デッドロックを起 こしていたスリープを残し たまま想定する結果が得ら れました。 68 20+10=30 10+20=30

Slide 69

Slide 69 text

チャネル 通信に向けた機能を持つ キューであるため、普通に FIFOのキューとして利用する ことも可能。 69 func main() { ch := make(chan int, 10) defer close(ch) ch <- 1 ch <- 2 ch <- 3 ch <- 4 fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) } 出力はどうなるでしょうか

Slide 70

Slide 70 text

チャネル 最初に入れたやつからちゃ んと出てきました 70 1 2 3 4

Slide 71

Slide 71 text

select文 多数のチャネルの受信を管理 し、チャネルごとに処理を場 合分けできる。 switch文に似ているが、順序 は無くすべての場合が同時に 判定されている。 ある程度の規模になると必須 71 func main() { ch1 := make(chan int) ch2 := make(chan string) chend := make(chan struct{}) go func(chint chan<- int, chstr chan<- string, chend chan<- struct{}) { for i := 1; i < 16; i++ { if i % 5 + i % 3 == 0 { chstr <- "FizzBuzz" } else if i % 3 == 0 { chstr <- "Fizz" } else if i % 5 == 0 { chstr <- "Buzz" } else { chint <- i } } close(chend) }(ch1, ch2, chend) for { select { case val := <-ch1: fmt.Println(val) case str := <-ch2: fmt.Println(str) case <-chend: fmt.Println("finish.") return } } }

Slide 72

Slide 72 text

Goの並行処理  このように、他言語と比較してGoでは非常に自然に並 行処理が記述できる仕組みが整っています。  扱いやすいものを提供するが中身を隠してしまうわけ ではない抽象化の仕方がなされているため、大変実装が しやすいと感じています。   72

Slide 73

Slide 73 text

まとめ Summary

Slide 74

Slide 74 text

まとめ - 並行処理と並列処理は違う - 逐次処理の実装とは毛色が違う - ちょっと低レイヤ側の仕組みを把握する必要がある - Goには独自のゴルーチンとチャネルを用いた抽象化 概念があって実装の助けになる - みんなもやろう 74

Slide 75

Slide 75 text

おわり 75