いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-39 (ゴルーチン, Goroutines, Tour of Go)

概要

Tour of Go の - Goroutines についてのサンプル。

tour.golang.org

ついにゴルーチンまできましたね。Goといえば ゴルーチン といえるくらいGoを有名にした機能と思います。

私も例にもれず、このゴルーチンを使ってみたいので、Go言語を勉強し始めました。

上のTour of Goのページにはゴルーチンについて以下のように記載されています。

A goroutine is a lightweight thread managed by the Go runtime.

ゴルーチンはGoランタイムによって管理される軽量なスレッドです。(拙訳)

Goランタイムで管理されるので、OSのネイティブスレッドではないということですね。

スレッドというより、名前が示すとおり コルーチン (coroutine)の一種 と考える方が分かりやすいと思います。

PythonとかC#やってる人だと、おなじみのワードかもしれませんね。

一時停止したり再開したりすることができる関数と思っていると分かりやすいと思われます。

Goでは、このゴルーチンを使って非同期処理を実現します。

Goでは、非同期処理を開始するのは、めっちゃ簡単です。

go 関数

って書くだけです。これ、最初みたとき本当に驚きました。こんな簡単なの!?って感じでw

例えば、なんの変哲もない以下のクソ関数があったとします。

func say() {
    fmt.Println("hello world")
}

これを普通に呼び出すと当然

say()

ですね。これはいつもどおりで同期呼び出しです。これを

go say()

って変えると、Goではsay()が非同期処理となります。驚きでした簡単すぎて。

ゴルーチンを起動するのに必要なことはこれで全てです。goをつけたら非同期処理になる。これだけです。

いやー便利だわ・・・って、おじさんは思いましたw

だからって非同期処理が簡単になると言う訳ではない

上に記載した通り、Goでは go をつけるとゴルーチンとして処理され、非同期処理としてGoランタイムがスケジューリングしてくれることは分かりました。

とても便利!ですが、かといって非同期処理の扱いが簡単になるわけではありません。非同期処理の起動が簡単になっているだけです。非同期処理で発生する問題である デッドロックやライブロック、リソース枯渇、メモリリーク などは同然ゴルーチン使っていても発生する可能性があります。競合状態も気にしないといけないし、特定のコンテキストにおいてアトミック性を保つことも依然として必要です。

Goはこの点に関して、チャネルとselect という概念を用意してくれています。これとゴルーチンを組み合わせることによって、上記の問題を出来るだけ発生しないように処理を書くことが可能となります。他の言語でも当然同じような処理は書けますが、Goは言語機能で、ゴルーチンとチャネルとselect という道具を用意しているところが特徴だなと個人的に思っています。

どの言語使ってもそうですが、「非同期処理のコードは正しく動作させることが著しく難しい」です。

何年、非同期処理を仕事でもプライベートで書いてても、やっぱり難しいです。

非同期処理周りに関しては、そのうちに自分用のメモを書いていく予定なので、今回はゴルーチンを起動するサンプルだけとしてます。

サンプル

package tutorial

import (
    "fmt"
    "sync"
    "time"
)

// Goroutine は、 Tour of Go - Goroutines (https://tour.golang.org/concurrency/1) の サンプルです。
func Goroutine() error {
    // ------------------------------------------------------------
    // goroutine (ゴルーチン) は、Goのランタイムで管理される軽量スレッドのこと。
    // 軽量スレッドと書いているが、実際はグレーンスレッドではなく
    // pythonのコルーチンと同じイメージを持ったほうが分かりやすい。
    // コルーチンなので、割り込みされることがない。(ノンプリエンプティブ)
    // コルーチンなので、一時停止及び再開可能。
    // 停止と再開はGoのランタイム側で管理される。
    // goroutineは、Goのプログラムでの最も基本的な構成単位である。
    //
    // Go言語は、言語のコア機能の一部として並行処理機能を提供している。
    // goroutine は、OSスレッドではないので、とても軽量。goroutineを一つ起動するのに
    // かかるメモリフットプリントは 数KB 程度とされている。
    // (runtime/stack.go の _StackMin の値は Go1.13 で 2048 と設定されている)
    //
    // 書籍「Go言語による並行処理(オライリー・ジャパン)」でも
    //   "Goの並行処理における哲学は以下のようにまとめられます。
    //    簡潔さを求め、チャネルをできる限り使い、ゴルーチンを湯水のように使いましょう。" (P.35)
    // と記載されており、基本的に起動する非同期処理の数は気にしなくても良いレベル。
    //
    // 通常の関数は、そのままだともちろん「同期」処理となる。
    // goroutine にすることで、「非同期」に出来る。
    //
    // 特定の関数を「非同期」にすることは、Go言語ではとても簡単で
    //   go func1(x, y)
    // と、関数の前に 「go」 とつけるだけである。これで func1 の実行が非同期になる.
    // (つまり、goroutine が生成される)
    //
    // 大事な点として、以下がある。
    //   - func1, x, および y は、実行元(current)の goroutine で評価される
    //   - func1の実行は新しい goroutine で実行される
    //
    // 他の言語と同じ考え方となるが、Goでも実行時には最低でも一つの goroutine が
    // 動作している。それが メインゴルーチン となる。
    //
    // goroutine は、スレッドと同じで同じアドレス空間で実行されるため
    // 共有メモリを利用してアクセスする場合は、必ず同期する必要がある。
    //
    // Go言語では、この点をチャネル (channel) という概念を用いて
    // 扱いやすいようになるようにしてくれている。共有メモリを扱う場合は
    // syncパッケージに関連する型や関数があるので、それらを利用する。
    //
    // 非同期処理を書いていると、複数のチャネルを扱う必要性がどうしても
    // 出てきてしまうが、Goでは、この点を select ステートメントを利用することで
    // 扱いやすいようにできている。
    //
    // - チャネルは goroutine を束ねる糊のようなもの
    // - select は チャネル   を束ねる糊のようなもの
    //
    // と考えるとわかりやすい。
    //
    // なので、Goの非同期処理は
    //   - goroutine
    //   - channel
    //   - select
    // を使って処理を行うのが基本パターン。
    // 必要な場合に、syncパッケージにあるライブラリも利用して処理する。
    //
    // Go言語でも共有メモリを利用することは勿論できるが、推奨される方法は
    // 共有メモリを使わずに、チャネルを利用してデータを通信して行く方法。
    //
    // Do not communicate by sharing memory;
    // instead, share memory by communicating.
    //    -- Effective Go (http://bit.ly/2NUdLRA))
    //
    // Go言語の並行処理の考え方の元となった
    // のは、CSP (http://bit.ly/2NOw8Y5) という理論。
    //
    // REFERENCES::
    //   - https://golang.org/doc/effective_go.html#concurrency
    //   - https://golang.org/doc/faq#Concurrency
    //   - https://golang.org/ref/spec#Go_statements
    //   - https://qiita.com/niconegoto/items/3952d3c53d00fccc363b
    // ------------------------------------------------------------
    // 普通の関数を定義
    f := func(prefix string) {
        fmt.Printf("[%-5s] こんにちわ世界\n", prefix)
    }

    // これをそのまま呼ぶと当然「同期」呼び出し
    f("sync")

    // Goでは、非同期呼び出しするには goroutine を使う
    // 関数を goroutine にするには、go 関数名 とする
    // これだけで、元々普通の関数だったものが非同期呼び出しに変わる
    // (つまり、Goランタイムによって非同期処理されるようにスケジューリングされる)
    go f("async")

    // ------------------------------------------------------------
    // sync.WaitGroup をつかって終了待機
    //   補足:チャネルを使っても同じような事は可能だが複数の非同期処理を
    //        待機する場合は、sync.WaitGroupの方が楽
    // ------------------------------------------------------------
    var (
        wg sync.WaitGroup
    )

    run := func(wait time.Duration, prefix string, wg *sync.WaitGroup) {
        if wg != nil {
            defer wg.Done()
        }

        fmt.Printf("[%-5s] func begin\n", prefix)
        time.Sleep(wait)
        fmt.Printf("[%-5s] func end\n", prefix)
    }

    // 非同期呼び出し
    wg.Add(1)
    go run(2*time.Second, "async", &wg)

    // 同期で呼び出し
    run(1*time.Second, "sync", nil)

    // 非同期処理の終了待機
    wg.Wait()

    return nil
}

try-golang/tutorial_gotour_26_goroutine.go at master · devlights/try-golang · GitHub

実行すると以下な感じ。

[Name] "tutorial_gotour_goroutine"
[sync ] こんにちわ世界
[sync ] func begin
[async] こんにちわ世界
[async] func begin
[sync ] func end
[async] func end

過去の記事については、以下のページからご参照下さい。

  • いろいろ備忘録日記まとめ

devlights.github.io

サンプルコードは、以下の場所で公開しています。

  • いろいろ備忘録日記サンプルソース置き場

github.com

github.com

github.com