たくさんの処理を効率よく行うための手法として並列処理があります。
処理の単位をスレッドと呼び、並列で処理することをマルチスレッドと呼びます。
Goではマルチスレッド処理を行うための機能が標準で提供されています。
本記事では、Goのマルチスレッド処理について解説します。
プログラミング初心者の方の学習や、忘れてしまった方の復習として、参考にしていただければ幸いです。
記載しているプログラムは、go1.15.2を使って動作確認をしています。
Goのマルチスレッド処理
Goのマルチスレッド処理について、以下の内容で解説します。
- マルチスレッドを扱うgoroutineとは
- goroutineの作成と起動
- チャネルを使ったメッセージの送受信
- 複数チャネルの操作
マルチスレッドを扱うgoroutineとは
Goでマルチスレッド処理を扱う機能として、goroutine が提供されています。
goroutineは非常に軽量で、1スレッドあたりに使うメモリ量もかなり小さくなっています。
goroutineは標準機能として提供されているので、パッケージのimportなどは不要です。
goroutineの作成と起動
goroutineでスレッドを作成して起動するには、goステートメントに関数を指定します。
func test() { fmt.Println("goroutine test.") } func main() { go test() }
上記のようにスレッド化する関数として定義することも出来ますし、無名関数としてgoステートメントに指定することも可能です。
上記のプログラムを実行すると、スレッドが起動してメッセージが表示されることを期待しますが、実際にはスレッドの起動後に処理を待たずに、メインスレッドが終了してしまうので何も表示されることはありません。
スレッドが終了するまで待機する時に用いるのが、syncパッケージのWaitGroupです。
func threadA(name string, wg *sync.WaitGroup) { fmt.Println(fmt.Sprintf("%s start.", name)) rand.Seed(time.Now().UnixNano()) time.Sleep(time.Duration(rand.Intn(3000)) * time.Millisecond) fmt.Println(fmt.Sprintf("%s end.", name)) defer wg.Done() } func main() { var wg sync.WaitGroup wg.Add(3) fmt.Println("スレッド開始") go threadA("スレッドA-1", &wg) go threadA("スレッドA-2", &wg) go threadA("スレッドA-3", &wg) wg.Wait() fmt.Println("スレッド終了") }
上記のプログラムでは、goroutineで起動するスレッド threadA を定義して、中で3秒以内ランダム時間スリープする処理を行います。
main関数で起動するスレッドを3つ作成し、それぞれにWaitGroupを引数として渡しています。
3つのスレッドを起動した後に、Waitを呼び出すことにより、スレッドが終了するまで待機しています。
各スレッドの最後の処理で、Doneを呼び出すことにより、スレッドの終了を伝えています。
上記のプログラムを実行すると以下の結果となります。
スレッド開始 スレッドA-3 start. スレッドA-2 start. スレッドA-1 start. スレッドA-2 end. スレッドA-1 end. スレッドA-3 end. スレッド終了
3つのスレッドが同時に起動し、終了するまで待機していることが分かります。
チャネルを使ったメッセージの送受信
各スレッドとの情報の受け渡しは、チャネルを通じて行うことができます。
チャネルはやり取りする情報の型に合わせて、makeで作成します。
c := make(chan string)
このチャネルを各スレッドに渡し、送信する側は「c <-」によって送り、受信する側は「<- c」によって受け取ります。
func threadB(name string, c chan<- string) { fmt.Println(fmt.Sprintf("%s start.", name)) rand.Seed(time.Now().UnixNano()) sleep_time := time.Duration(rand.Intn(3000)) * time.Millisecond time.Sleep(sleep_time) fmt.Println(fmt.Sprintf("%s end.", name)) c <- fmt.Sprintf("%s %s", name, sleep_time) } func main() { c := make(chan string) fmt.Println("スレッド開始") go threadB("スレッドB-1", c) go threadB("スレッドB-2", c) go threadB("スレッドB-3", c) fmt.Println(<-c) fmt.Println(<-c) fmt.Println(<-c) fmt.Println("スレッド終了") }
上記のプログラムでは、goroutineで起動するスレッド threadB を定義して、先程と同様に3秒以内ランダム時間スリープする処理を行います。
スリープが終わるとチャネルに対してメッセージを送信します。
main関数では、3つのスレッドを起動した後にメッセージを受信します。
メッセージが受信できるまで待機状態となります。
上記のプログラムを実行すると以下の結果となります。
スレッド開始 スレッドB-3 start. スレッドB-1 start. スレッドB-2 start. スレッドB-1 end. スレッドB-1 1.116s スレッドB-2 end. スレッドB-2 2.348s スレッドB-3 end. スレッドB-3 2.962s スレッド終了
スレッドが終了したものからメッセージを取得していることが分かります。
複数チャネルの操作
チャネルは複数作成し、様々なスレッド間でメッセージの送受信を行うことができます。
複数のチャネルから同時に受信したい場合に使用するのが、selectステートメントです。
func main() { c1 := make(chan string) c2 := make(chan string) c3 := make(chan string) fmt.Println("スレッド開始") go threadB("スレッドC-1", c1) go threadB("スレッドC-2", c2) go threadB("スレッドC-3", c3) for i := 0; i < 3; i++ { select { case r1 := <-c1: fmt.Println(fmt.Sprintf("%s %s", "チャネル1: ", r1)) case r2 := <-c2: fmt.Println(fmt.Sprintf("%s %s", "チャネル2: ", r2)) case r3 := <-c3: fmt.Println(fmt.Sprintf("%s %s", "チャネル3: ", r3)) } } fmt.Println("スレッド終了") }
上記のプログラムでは、3つのチャネルを作成し、それぞれのチャネルを持つスレッドを起動した後に、selectによって3つのチャネルを受信しています。
ループによって3回受信したら終了となるようにしています。
ループの回数はメッセージをやり取りする回数に合わせたり、無限ループにして特定のメッセージを受け取ったらループを抜けるなど様々なやり方に応用できます。
上記のプログラムを実行すると以下の結果となります。
スレッド開始 スレッドC-3 start. スレッドC-1 start. スレッドC-2 start. スレッドC-2 end. チャネル2: スレッドC-2 1.741s スレッドC-3 end. チャネル3: スレッドC-3 2.02s スレッドC-1 end. チャネル1: スレッドC-1 2.267s スレッド終了
それぞれのチャネルからメッセージを受信出来ていることが分かります。
時間のかかる処理はマルチスレッド処理で効率よく実行しよう
UI内での処理や連携するAPIでの処理は、待ち時間が長くなると、利用者に不便な印象を与えてしまいます。
時間がかかる処理はマルチスレッドで実施できないか検討し、効率よく処理を実行すると利用者の印象も良くなります。
今回はGoのマルチスレッド処理を解説しました。
以上、参考になれば幸いです。
コメント