[Go入門] 並列処理をざっくり理解する

2021/02/08に公開

こんにちは。
Golang に入門しました。
今回は Go の目玉機能でもある goroutine を理解するために、「並列処理」というものについて考えていきたいと思います😊
なお、この記事では「並列処理(プログラミング)」と「並行処理(プログラミング)」は同じ意味とお考えください。

なぜこの記事を書こうと思ったのか

これまでのプログラミング言語経験は、主にRuby や JS で、静的型付け言語の経験はありませんでした。TypeScript は別として。。。
そして、「へいれつぷろぐらみんぐ」とやらも馴染みが無かったのです。

Rubyや JS からプログラミングを始める方は多いと思いますが、そういう方が Go に入門した時に、「???」となるのを少しでも緩和できたら良いなと思っております(私も勉強中です)😎

並列プログラミングって何が嬉しいの?

端的に言うと、逐次処理を重ねるとトータルで見た時に時間がかかる処理だとしても、それらを同時(並列)に行うことでトータルの処理時間を短くできることです。

そもそもプロセスとかスレッドとかってなに?

本題です。
Go に限らず、並列実行を理解する上で、プロセスとスレッドの違いは避けて通れないものです。
どちらもプログラムを処理していく道中の 処理の単位 です。

  • プロセス → カーネルが管理する処理の単位
  • スレッド → 「プロセスの中の」処理の単位
    です。

記述した通り、1つのスレッドの 子ども とも言えるのが「スレッド」です。
スレッドは1つのプロセスに割り当てられたメモリ内で動作します。子どもなので、親に割り当てられたメモリ容量を超えて処理を行うことはできません。

スレッドがいくつあるかは、そのプログラムが動いているマシン(今使っている MacBook でも、AWS で構築した EC2 インスタンスでも同じです)によって決まります。

今お使いの Mac もおそらく「マルチスレッド」です。
マルチスレッドがあれば「シングルスレッド」もあります。
そのままですが、「スレッドが1つしかないマシン」ということなので、しょぼいマシンだと考えてください。

さて、マルチスレッドで動かすことが 「可能な」 マシン上で動いているプログラムですが、そもそもプログラムが「マルチスレッド」を上手く利用するように書かれていなければ、宝の持ち腐れです。マシンはあくまでもマルチスレッドを「利用可能」なだけです。

では、マルチスレッドを利用するにはどうすれば良いのでしょうか?
マルチスレッドを利用できるプログラミング言語と利用できないプログラミング言語があります。
有名な所だと、JavaScript は「シングルスレッド」で処理を行う言語ですので、どうやってもマルチスレッドで動かすようにコードを書くことはできません。
https://qiita.com/KickJune/items/3e980d00102c5c7cf600

Ruby はと言いますと、通常は逐次処理(Aが終わってからBを処理する)ですが、 Thread クラスを利用することで並列処理が可能です。

https://docs.ruby-lang.org/ja/latest/class/Thread.html
公式からの抜粋ですが、Ruby で並列処理を書くと以下のようになります。

Thread.new do
  (1..3).each{|i|
    p i
    Thread.pass
  }
  exit
end

loop do
  Thread.pass
  p :main
end

#=>
1
:main
2
:main
3
:main

限られた箇所で使うのならまだしも、Rails のコードにいちいちこういうものが書いてあるのはあまり見たことが無いのではないでしょうか?
ちょっと面倒ですよね。。。

Golang の並列処理

ここまで、

  • 並列処理とは何のか
  • 並列処理の何が嬉しいのか
  • プロセスとスレッドの関係
  • 他のプログラミング言語における並列処理

について見ていきました。
今度こそ Golang についてです。

このように他のプログラミング言語でも並列処理はやろうと思えば可能なのですが、どうして Go の場合にはことさら「目玉機能」ともてはやされるのでしょうか?
その理由の1つが並列処理を簡潔に書けることです。
Go の公式チュートリアルからです(インデント崩壊していてすみませんが)。

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

func main() {
	go say("world")
	say("hello")
}

この例では main 関数の中から同じ関数 say を2回読んでおり、それぞれの結果を出力します。
何が違うかというと、1回目の呼び出しでは先頭に go というキーワードがついており、2回目はついていません。

先頭に go というキーワードを付けて関数を呼び出すと、たったこれだけで並列処理の設定が完了します!!
つまり、メインスレッドとは別のスレッド(サブスレッドと呼びます)で処理されるということです。

しかし Go で並列処理を書こうと思うと避けて通れないキーワードがもう1つあります。それは、 channel です。
https://go-tour-jp.appspot.com/concurrency/2

並列処理ということは、複数のスレッドでプログラムが同時に処理されるということですが、このスレッド間の値の受け渡しはどう行うのでしょうか?
スレッド間は独立しており、何もしなければスレッド間で値の受け渡しはできません。

並列に処理は進めるものの、「3つの処理全てが終わってから次の処理に進みたい」という場合にはどうしたら良いでしょうか?
どうしてもスレッドごとに処理が終わるタイミングはバラバラになってしまいますが、全部終わってから次に行きたい場合です。
スレッド間は独立しており、何もしなければメインスレッドの終了と共にプログラム自体が終了します。

このような問題を解決するのが channel です。
チャンネルとはデータの通り道として機能する、スレッド間に渡る橋のようなものです。

ch <- v    // v をチャネル ch へ送信する
v := <-ch  // ch から受信した変数を v へ割り当てる

チャンネルを使うことで、スレッド間で値の受け渡しが可能になります。
チャンネルを使うことで、全てのスレッドの処理が終わるタイミングを待つことができます。

今回は goroutinechannel についてはざっくりした説明に止めました。
残りは公式ドキュメントを参考にして頂けたらと思います(私も細かいところまでは分かっていないので。。。)🙇‍♂️
https://golang.org/doc/


今回はざっくりと並列処理について書いてみました。
間違いなどあればご指摘頂けると嬉しいです!!
どなたかの役に立てば幸いです😎

Discussion