いろいろ備忘録日記

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

Goメモ-121 (ゴルーチンの完了検知方法あれこれ)

概要

以下、個人的なメモです。

ゴルーチンはGCの対象とならないので、きっちり停止させることがとても大事。

unbufferedなチャネルを使っていて、他方が受信してくれていないので、ずっと送信できずにスタックしてるとかが要注意。

で、同じくらい大事なのが、完了したことを検知すること。(エラー発生時の伝播も大事だけどこれは別の機会にメモ)

いろいろやり方があるけど、とりあえず自分用にメモ。

投げっぱなし(完了検知なし)

あまりしないけど、起動するゴルーチンの生存期間がアプリケーションの生存期間と同じ場合とかだと投げっぱなしにするときもある。

package goroutines

import "github.com/devlights/gomy/output"

// NonStop -- ゴルーチンを待ち合わせ無しで走らせるサンプルです.
//
// 投げっぱなしのゴルーチンを作る場合に使います。
// 通常待ち合わせ無しの非同期処理は行うべきではありません。
func NonStop() error {
    go func() {
        output.Stdoutl("[goroutine] ", "This line may not be printed")
    }()

    // 上記のゴルーチンは待ち合わせをしていないので出力されない可能性がある。
    // (出力する前にメインゴルーチンが終わる可能性がある)

    return nil
}

実行すると以下のようになります。

$ make run
go run github.com/devlights/try-golang/cmd/trygolang -onetime -example ""
ENTER EXAMPLE NAME: goroutines_nonstop
[Name] "goroutines_nonstop"


[Elapsed] 29.906µs

doneチャネルを使って完了検知

doneチャネル、つまり、chan struct{} を使って、完了検知。チャネルをクローズすることで完了をブロードキャストする。

package goroutines

import "github.com/devlights/gomy/output"

// WithDoneChannel -- doneチャネルを用いて待ち合わせを行うサンプルです.
func WithDoneChannel() error {
    done := func() <-chan struct{} {
        done := make(chan struct{})

        go func() {
            defer close(done)
            output.Stdoutl("[goroutine]", "This line is printed")
        }()

        return done
    }()

    <-done

    return nil
}

実行すると以下のようになります。

$ make run
go run github.com/devlights/try-golang/cmd/trygolang -onetime -example ""
ENTER EXAMPLE NAME: goroutines_with_done_channel
[Name] "goroutines_with_done_channel"
[goroutine]          This line is printed


[Elapsed] 50.388µs

sync.WaitGroup を使って完了検知

手軽に使える方法。起動するゴルーチンの数が不定の場合とか、とりあえず全部完了するまで待ちたいときとか便利。

package goroutines

import (
    "sync"

    "github.com/devlights/gomy/output"
)

// WithWaitGroup -- sync.WaitGroupを用いて待ち合わせを行うパターンです.
func WithWaitGroup() error {
    var (
        wg sync.WaitGroup
    )

    wg.Add(1)
    go func() {
        defer wg.Done()
        output.Stdoutl("[goroutine]", "This line is printed")
    }()

    wg.Wait()

    return nil
}

実行すると以下のようになります。

$ make run
go run github.com/devlights/try-golang/cmd/trygolang -onetime -example ""
ENTER EXAMPLE NAME: goroutines_with_waitgroup
[Name] "goroutines_with_waitgroup"
[goroutine]          This line is printed


[Elapsed] 75.565µs

context.Context を用いて完了検知(context.Context.WithCancel)

最近のGoではデフォルトで利用されることが多い context.Context 。context.Done() が返すのは done チャネルになっている。

package goroutines

import (
    "context"

    "github.com/devlights/gomy/output"
)

// WithContextCancel -- context.Contextを用いて待ち合わせを行うサンプルです.
func WithContextCancel() error {
    var (
        rootCtx             = context.Background()
        mainCtx, mainCancel = context.WithCancel(rootCtx)
    )

    defer mainCancel()

    ctx := func(pCtx context.Context) context.Context {
        ctx, cancel := context.WithCancel(pCtx)

        go func() {
            defer cancel()
            output.Stdoutl("[goroutine]", "This line is printed")
        }()

        return ctx
    }(mainCtx)

    <-ctx.Done()

    return nil
}

実行すると以下のようになります。

$ make run
go run github.com/devlights/try-golang/cmd/trygolang -onetime -example ""
ENTER EXAMPLE NAME: goroutines_with_context_cancel
[Name] "goroutines_with_context_cancel"
[goroutine]          This line is printed


[Elapsed] 44.575µs

context.Context を用いてタイムアウト付きで完了検知(context.Context.WithTimeout)

実務では、完了するまでずっと動いていて良い処理というのはあまり無い。実際には何らかの生存期間を設けて、その中で動作させることが多い。

doneチャネルだけで処理しようとすると、別途タイムアウト用のチャネルなどを設けないといけないのでちょっと面倒。

その点、context.Contextだとサクッと出来るので楽。

package goroutines

import (
    "context"
    "time"

    "github.com/devlights/gomy/output"
)

// WithContextTimeout -- context.Contextを用いてタイムアウト付きで待ち合わせを行うサンプルです
func WithContextTimeout() error {
    // 処理内で利用する共通関数
    var (
        iter = func(n int) []struct{} { return make([]struct{}, n) }
        now  = func() int64 { return time.Now().UTC().Unix() }
    )

    // コンテキスト定義
    var (
        rootCtx             = context.Background()
        mainCtx, mainCancel = context.WithTimeout(rootCtx, 2*time.Second)
        procCtx, procCancel = context.WithTimeout(mainCtx, 1*time.Second)
    )

    defer mainCancel()
    defer procCancel()

    // ---------------------------------------------------
    // 以下の仕様とする
    //   - アプリケーション全体の生存期間は2秒間
    //   - 非同期起動するゴルーチン全体の生存期間は1秒間
    // ---------------------------------------------------

    ctx := func(pCtx context.Context) context.Context {
        ctx, cancel := context.WithCancel(pCtx)

        go func() {
            defer cancel()

            for i := range iter(10) {
                select {
                case <-ctx.Done():
                    output.Stdoutl("[ctx inside]", "done", now())
                    return
                default:
                }

                output.Stdoutl("[goroutine]", i)
                time.Sleep(200 * time.Millisecond)
            }
        }()

        return ctx
    }(procCtx)

    // 待ち合わせしながら経過出力
    var (
        doneCtx, doneProc, doneMain = ctx.Done(), procCtx.Done(), mainCtx.Done()
    )

LOOP:
    for {
        select {
        case <-doneCtx:
            output.Stdoutl("[ctx]", "done", now())
            doneCtx = nil
        case <-doneProc:
            output.Stdoutl("[proc]", "done", now())
            doneProc = nil
        case <-doneMain:
            output.Stdoutl("[main]", "done", now())
            break LOOP
        }
    }

    return nil
}

実行すると以下のようになります。

$ make run
go run github.com/devlights/try-golang/cmd/trygolang -onetime -example ""
ENTER EXAMPLE NAME: goroutines_with_context_timeout
[Name] "goroutines_with_context_timeout"
[goroutine]          0
[goroutine]          1
[goroutine]          2
[goroutine]          3
[goroutine]          4
[proc]               done 1605492499
[ctx]                done 1605492499
[ctx inside]         done 1605492499
[main]               done 1605492500


[Elapsed] 2.000258303s

おすすめ書籍

自分が読んだGo関連の本で、いい本って感じたものです。

Go言語による並行処理

Go言語による並行処理

スターティングGo言語 (CodeZine BOOKS)

スターティングGo言語 (CodeZine BOOKS)

  • 作者:松尾 愛賀
  • 発売日: 2016/04/15
  • メディア: 単行本(ソフトカバー)

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)


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

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

devlights.github.io

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

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

github.com

github.com

github.com