Smile Engineering Blog

ジェイエスピーからTipsや技術特集、プロジェクト物語を発信します

GoでGoな非同期処理を試してみよう!

非同期処理、皆さんどうコードを書いてますか?

少し前のこと。弊社の若手(?)の方が、『非同期処理めんどくさい!』みたいなことを騒いでいたのですが、そもそも皆さんどうやって非同期処理のソースコードを書いているのだろう?とふと思ったわけです。そもそも『非同期』の定義ってなんだろうという話もでてきそうですが、ぱっと思いつくのは以下のような書き方でしょうか?

  • スレッド作成してそこで実行
  • async/await を使用して実行

最近の主流は async/await でしょうかね。こちらは多くの言語に実装されています。
ただ私の場合は仕事ではほとんどPythonを書いているのですが、Pythonでのasync/awaitはやや手順が面倒です。他の言語でも似たりよったりでしょうか。

非同期処理が得意なプログラミング言語、それがGo!

ところが、そんなめんどくさい非同期処理を簡単に実現してしまうプログラミング言語が存在するんです。
それは、言わずとしれたGoogle製、Goですね!

golang.org

場合によっては、『Go言語』とか『GoLang』などとも呼ばれています。検索する場合はそうしないと全然別のものが引っかかってしまいそうですし。。

今回はそのGoを使用して、どのように非同期処理を書いていくかを紹介していきます。

さっそく簡単なソースコードを紹介

まずは非同期処理を実行するための簡単なソースコードを紹介します。

package main

import (
    "fmt"
    "time"
)

func worker(id string, ch chan string) {
    x := 0
    for x < 3 {
        fmt.Printf("%s: hello (%d)\n", id, x)
        time.Sleep(time.Second * 1)
        x++
    }
    ch <- id + ": Task Done!"
}

func main() {
    ch := make(chan string)
    go worker("a", ch)
    go worker("b", ch)
    go worker("c", ch)
    fmt.Println(<-ch)
}

わずか24行のソースコードですね!
worker() に文字列とチャネルをパラメータとして渡して、その文字列を繰り返し表示させるという簡単なコードです。
はて、チャネルとはなんぞや!?という話は後述するとしまして、これを実行すると、下記の実行結果が得られます。

c: hello (0)
a: hello (0)
b: hello (0)
b: hello (1)
c: hello (1)
a: hello (1)
c: hello (2)
b: hello (2)
a: hello (2)
c: Task Done!

非同期処理が実行されたということはとりあえず理解していただけたでしょうか?
一体これだけのコードで何が起きたかわからない!と思われる方もいらっしゃると思いますので、少しずつ紐解いてソースコードを読んでみましょう。

Goの非同期処理は『go』で実行!

まず肝となるのは、下記の部分です。

    go worker("a", ch)
    go worker("b", ch)
    go worker("c", ch)

本来であれば "worker()" と書けばそのまま worker() という関数が実行されそうな気もしますが、その前に "go" という記述がありますね。
つまり、呼び出す関数名の前に "go" をつけることで、その関数は非同期で実行されます。
その前に何一つ準備する必要ありません。これこそが、Goで非同期処理が簡単に書ける所以でしょう。

await/async でめげそうになっていた方も、これなら簡単に非同期処理が実現できそうですよね?

ところでチャネルって何ぞ!?

さて、ここまでは非同期処理が簡単に実行できることを見てきました。
そしたらもう一つの疑問、 変数 "ch" というものは何をしているのでしょうか?

試しに、 main() 内にある "fmt.Println(<-ch)" の箇所を、下記のようにコメントアウトして実行してみましょう。
するとどうなるでしょうか?

func main() {
    ch := make(chan string)
    go worker("a", ch)
    go worker("b", ch)
    go worker("c", ch)
    // fmt.Println(<-ch)
}

・・・はい、何も表示されませんね。 これは特にGoがバグってるとかそういう話でもなく、普通に処理を行い、一瞬で処理が終わってしまったので、何も表示されなかっただけです。

つまりこれは、worker() でのタスクの終了を待っているわけです。 worker() にはパラメータとして変数 "ch" が渡されていましたよね。これを使って worker() が終了するのを main() 側で待っています。
肝となるのはこの箇所ですね。

    ch <- id + ": Task Done!"

main() 側の "<-ch" は、worker() 側の "ch <-" が実行されるのを待っているということになります。
ちなみにこの場合、ch には " id + ": Task Done!"" という文字列を代入しています。その文字列を main() で表示させているわけです。
つまりこのチャネルというものには、非同期処理の実行結果を変数として返す機能があるわけです。

但し、ここでは一点注意が必要です。
全ての非同期処理終了を待つには、実行した非同期処理数分だけ待つ必要があります。 どういうことかと言いますと、下記の出力結果をもう一度見てみましょう。

c: Task Done!

頭に "c" というものが書かれていますね。ソースコードを振り返ると、これはつまり id に当たる部分で、つまり "c" と名付けられた worker が非同期処理を実行して、そのままソースコードを終了したことを意味します。
"a" と "b" については待つこともせず、そのままプログラムが終了してしまったというわけですね・・・。
もちろん、"fmt.Println(<-ch)" という行を3回書くと、"a" と "b" の終了も待ってくれます。

Go、とりあえず学んでおきたい言語???

Goによる非同期処理について書いてきましたが、いかがでしたでしょうか?
非同期処理については簡単に書くことができるというのは理解して戴けたのではないかと思います。

ところでつい先日、とある場所でこんな質問(?)を受けたことがありました。

「Goってよく聞くけど、あまりプロダクトでは聞かない気がする・・・?」

その質問(?)に対する私の答えは "No" です!
なぜなら、皆さん Docker ってプロダクトでも普通に使っていますよね?

Dockerは実はほぼGoで書かれているんです!

実際、Dockerコンテナ内のアプリを書く場合は、Goで書くと多くのメリットを享受できます。(予めビルドしておくとライブラリが不要になるなど)
そのため、一度は学んでおきたい言語という位置づけには変わらないかもしれません。

が、筆者はあまりGoを好きではありません!

・・・いやいや、それはないでしょ!というオチを書かせていただきました。。。

私は稀にC++なども書いてたりするため、あまりGoは好きになれないんですよね。
なぜC++プログラマがGoを好きになれないかと言うと、それは下記の一言かなと。

Goにはクラスという概念が存在しない!

どういうことかというと、C++だと当たり前のようにクラス設計などすると思うのですが、それをそのままそっくりGoに持ち込むことができないのです。
いや別に『そんなにオブジェクト指向で設計する必要があるのか?』という宗教論争っぽいことをしたいわけではなくてですね、単純にC++のライブラリがそのままGoで利用できないことが面倒だなと感じているだけです。せっかく目の前に使用したいC++のライブラリがあるのに、それをGoで使う必要があるとわざわざCでラッパーを作成する必要があるとか・・・さすがに泣きたくなりますよね。。。

プログラミング言語が乱立してしまうのも仕方ないかなとも思うのですが、せめてI/Fくらいは統一してほしいな〜と思ってたりします。