RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

Go言語の平行処理をやってみよう!【goroutine】

f:id:tech-rakus:20220127133035p:plain

はじめに

おはようございます、こんにちは、こんばんは、rks_hrkwと申します。
もう1月も終わりですね。皆様いかがお過ごしでしょうか。

この記事はGo言語といえばの機能の一つである、ゴールーチン(goroutine)の入門記事となっております。
この記事は

  • Go触ってみたけどまだゴールーチンは勉強してないよ
  • Go触ったことないけどGoでの平行処理に興味があるよ

という方向けです。

※入門記事のため細かい箇所や複雑な箇所は説明を省いている可能性がありますご了承ください。

また、Goの環境構築については以下の記事をご参照ください。 tech-blog.rakus.co.jp

目次

並行処理って何?

並行処理とは、ある期間において質の違う複数の処理を同時に行うことです。
ある期間と書いてあるように全く同時にスタートする必要はありません。
ある期間内に複数の処理が行われていれば良いのです。
また、質の違う処理というのもミソです。
並行して〇〇やってるよ、という時に全く同じことをしていることは基本ありませんよね。
混同しやすいのが並列処理でこちらは質の同じ処理を同時に行うことを言います。

今回Goで行うのは、記事のタイトルの通り並行処理の方です。

Goでは、ゴールーチンという機能を使用して並行処理を実現します。

ゴールーチン

Go言語では、go文を使用してゴールーチンを作ることができます。
go文とは、関数呼び出しの前にgoを付けて呼び出すことを言います。
go文はゴルーチンを新たに生成して、並行して処理される新処理の流れをランタイムに追加します。

早速ですが・・・実際にゴールーチンを書いてみましょう。

※今後出てくるプログラムは、プログラム下に貼ってあるリンクからWeb上で実行できます。

ゴールーチンを書いてみよう

まずは、"おかえり"と返してくれるwelcomeBack関数を作成してgoを先頭につけて呼び出してみましょう。

package main

import (
    "fmt"
    "time"
)

func welcomeBack() {
    fmt.Println("おかえり")
}

func main() {
    fmt.Println("ただいま")
    go welcomeBack()
    time.Sleep(50 * time.Millisecond)
}

Go Playground - The Go Programming Language

[実行結果]
ただいま
おかえり

上手く実行できましたね。

"ただいま"の後に"おかえり"を返してくれる関数を呼んでいるだけなので当然です。
これでしたら、go文を使ってwelcomeBack関数を呼び出さずに普通に呼び出しても実行結果は同じですし、結局並行処理が何なのかよくわかりません。

ただ、main関数の最後に変な1文があります。
これを消してもう一度実行してみましょう。

func main() {
    fmt.Println("ただいま")
    go welcomeBack()
    // time.Sleep(50 * time.Millisecond)
}

Go Playground - The Go Programming Language

[実行結果]
ただいま

今度は、"おかえり"を返してくれませんでした。

どこかにバグがあり、"ただいま"と表示した時点でプログラムが終了してしまったのでしょうか?
結論を言ってしまえば、これはプログラムとして正しい動作をしています。
これが、並行処理を行っている何よりの証拠なのです。

動作を簡単に図にしてみましょう。
f:id:rks_hrkw:20220115184724p:plain
Go言語はmainのゴールーチンが終了したタイミングで、プログラム全体を終了させます。

今回のパターンだと、welcomeBack関数はgo文で呼び出されているのでmainとは別のゴールーチンで実行されます。
そのため、mainと新しく作られた"おかえり"を言うゴールーチンが並行に処理されていきます。
結論を言うと、welcomeBack関数側のゴールーチンで"おかえり"を出力する前にmainのゴールーチンが終了したと言うことになります。
先ほども述べたように、mainのゴールーチンが終了した時点でプログラム全体は終わってしまいます。

消した1文は、welcomeBack関数側のゴールーチンが終わるまでを待つための遅延処理だったわけですね。

以下に行った処理をまとめます。

1行消す前の処理順
  1. "ただいま"の出力

  2. 別のゴールーチンを作成してwelcomeBack関数を実行

  3. main関数はtime.Sleepで止まる

  4. "おかえり"の表示

  5. 指定の時間待ったmain関数が終了

  6. プログラム全体も終了

1行消した後の処理順
  1. "ただいま"の出力

  2. 別のゴールーチンを作成してwelcomeBack関数を実行

  3. main関数が終了

  4. プログラム全体も終了

syncパッケージ

先程のうまく行っている方のプログラムではtime.sleepを使用して、指定の時間main関数をストップさせていました。
ここで誰しもが一つの疑問を抱きます。

もし、新しく作成したゴールーチンの処理が30分かかったらどうするの?

当然処理は最後まで行かず、main関数が先に終了します。
だからと言って、ゴールーチンを呼び出すたびに取り敢えず30分止めていたらキリがありません。
処理速度も毎回変わってきますし、そもそも別のゴールーチンを作るたびにその部分の処理の実行時間計測をすることになってしまいます。
それでは、並行処理をする利点が無くなってしまいます。
理想としては、新しく作ったゴールーチンの処理が全て終了するまで待ってくれればそれでいいわけですよね。

そんな理想を叶えてくれる機能を次に紹介します。

sync.WaitGroup

WaitGroupは、複数のゴールーチンの完了を待つためのものです。
内部に数値(初期値0)を持っており、メソッドのwait()を呼ぶと、その数値が0になるまで待つことになります。
従って、別のゴールーチンを呼び出す数だけ内部の数値をインクリメントしてあげて、ゴールーチンの処理が終わるたびにデクリメントしてあげれば、wait()を呼んだmainゴールーチンは全ての並行処理が終わるまで待ってくれると言うわけです。

文で読んでも分かりづらいので、実際にコードを書いてみましょう。

package main

import (
    "fmt"
    "sync"
)

func welcomeBack() {
    fmt.Println("おかえり")
}

func main() {
    fmt.Println("ただいま")

    var wg sync.WaitGroup // WaitGroupの生成
    wg.Add(1)             // カウンタをインクリメント

    go func() {
        welcomeBack()
        wg.Done() // カウンタをデクリメント
    }()

    wg.Wait() // mainのゴールールは新規のゴールーチンが完了するのを待つ
}

Go Playground - The Go Programming Language

[実行結果]
ただいま
おかえり

今回は待つ時間を直書きで指定して止めなくても、ちゃんと想定通りにプログラムが動いてくれました。

このように、sync.WaitGroupを使うことでmain以外のゴールーチンが終了するまで待つといったような動きができるようになります。
さらに多くのゴールーチンを管理する場合も同様に、インクリメントとデクリメントを行うだけです。

処理の流れをまとめてみましょう。

  1. "ただいま"の出力

  2. WaitGroup wgの生成

  3. WaitGroup内のカウンタを並行処理したい別のゴールーチンの数だけインクリメントする(今回は1)

  4. go文で新しくゴールーチンを作成する

  5. 新しく作ったゴールーチン内の処理と並行でmainのゴールーチンの処理も進んでいくがwait()で止まる(カウンタが1のままなので)

  6. 新しく作ったゴールーチンで"おかえり"の出力

  7. こちらのゴールーチンで作業が終わったことを伝えるためDone()でカウンタをデクリメントする(1 → 0)

  8. カウンタが0になったのでmainのゴールーチンで呼んでいたwait()が終了する(待ちが終了する)

  9. mainのゴールーチンが一番最後まで行ったのでプログラム全体も終了

処理としては以上の流れになっています。
Add()した数だけDone()することを忘れないようにしましょう。

Goには他にも様々なゴールーチン間で同期を取る方法が存在しますので、気になった方は色々調べてみてください。

さいごに

本記事では、ゴールーチンの基本の基本に焦点を当てて紹介していきました。
Go言語の並行処理、ゴールーチンには欠かせないチャネルの説明などは敢えて省かせていただきました。
理由としては大体どの記事やサイトでもゴールーチンとチャネルがまとめて紹介されており、インプット量が多すぎて初心者に優しくないと思ったのと、別になっている記事があってもそれはそれで選択肢の一つとしていいだろうという考えに至ったためです。
また、タイミングがあればチャネルの説明やもう少し実践的な並行処理を行うような紹介記事が書ければと思います。
全部読んだよという方も一部だけ読んだよという方も、お読みいただきありがとうございました。
これでよりGoに興味を持っていただけたなら幸いです。 よきGoライフを!

参考文献


  • エンジニア中途採用サイト
    ラクスでは、エンジニア・デザイナーの中途採用を積極的に行っております!
    ご興味ありましたら是非ご確認をお願いします。
    20210916153018
    https://career-recruit.rakus.co.jp/career_engineer/

  • カジュアル面談お申込みフォーム
    どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。
    以下フォームよりお申込みください。
    rakus.hubspotpagebuilder.com

  • イベント情報
    会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください!
    rakus.connpass.com

Copyright © RAKUS Co., Ltd. All rights reserved.