グローバルナビゲーションへ

本文へ

フッターへ

お役立ち情報Blog



goroutineリークを排除して安全に並行処理を行う方法

goroutineはgoキーワードを関数の前に書くことで簡単に起動することができます。
しかしながら、goroutineはランタイムによってガベージコレクションされないため、正常に終了させていない場合はリークしていきます。 野放しになったgoroutineたちによってプロセスごと停止にならないよう、正常に終了させましょう。

goroutineリークしている例

それでは、意図的にgoroutineリークを発生させてみましょう。
起動しているgoroutineの数をカウントするため、runtime.NumGoroutine()を使用して現在のgoroutine数を標準出力に表示させています。

package main

import (
	"fmt"
	"runtime"
	"time"
)

func main() {
	fmt.Printf("before leak:\t%d\n", runtime.NumGoroutine())

	leak(nil)

	time.Sleep(3 * time.Second)
	fmt.Printf("after leak:\t%d\n", runtime.NumGoroutine())
}

func leak(c <-chan string) {
	go func() {
		for cc := range c {
			fmt.Println(cc)
		}
	}()

	fmt.Printf("in leak:\t%d\n", runtime.NumGoroutine())
}

出力結果は以下のようになりました。

before leak:    1
in leak:        2
after leak:     2

この処理では leak 関数の引数に nil チャネルを渡しているため、子goroutineの処理が永遠に終わらず、goroutine数が2のまま終了しています。
管理不能なgoroutineが解き放たれてしまいました。
この例ではプロセスがすぐに終了しますが、webアプリケーションなど長期間起動するプログラムではメモリが永遠に消費されてしまいます。

チャネルを使用してタイムアウトさせる

goroutineを適切に処理する方法の1つとして、チャネルを使用してgoroutineをキャンセルさせてみます。
 done チャネルを作成して、1秒経過後にタイムアウトさせます。

package main

import (
	"fmt"
	"runtime"
	"time"
)

func main() {
	fmt.Printf("before useDone:\t%d\n", runtime.NumGoroutine())

	done := make(chan interface{})
	useDone(done, nil)

	// 1秒経過後にタイムアウト
	time.Sleep(1 * time.Second)
	close(done)

	time.Sleep(3 * time.Second)
	fmt.Printf("after useDone:\t%d\n", runtime.NumGoroutine())
}

func useDone(done <-chan interface{}, c <-chan string) {
	go func() {
		for {
			select {
			case s := <-c:
				fmt.Println(s)
			case <-done:
				fmt.Println("done!")
				return
			}
		}
	}()

	fmt.Printf("in useDone:\t%d\n", runtime.NumGoroutine())
}

出力結果は以下のようになりました。

before useDone:	1
in useDone:     2
done!
after useDone:	1
 done! を出力し、最後のgoroutine数も1になっていることから適切にgoroutineをキャンセルできていることが確認できました。
引数に nil を渡していますが、今回はgoroutineが無事に終了しました。

contextパッケージを使用してタイムアウトさせる

先ほどの例では done チャネルを作成しましたが、contextパッケージを使用するとより単純に記述することができます。
それでは、contextパッケージを使用してタイムアウトさせてみます。

package main

import (
	"context"
	"fmt"
	"runtime"
	"time"
)

func main() {
	fmt.Printf("before useCtx:\t%d\n", runtime.NumGoroutine())

	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	useCtx(ctx, nil)

	time.Sleep(3 * time.Second)
	fmt.Printf("after useCtx:\t%d\n", runtime.NumGoroutine())
}

func useCtx(ctx context.Context, c <-chan string) {
	go func() {
		for {
			select {
			case s := <-c:
				fmt.Println(s)
			case <-ctx.Done():
				fmt.Println(ctx.Err())
				return
			}
		}
	}()

	fmt.Printf("in useCtx:\t%d\n", runtime.NumGoroutine())
}

出力結果は以下のようになりました。

before useCtx:  1
in useCtx:      2
context deadline exceeded
after useCtx:   1

contextのエラーを出力し、最後のgoroutine数も1になっていることから、チャネル同様goroutineをキャンセルできていることが確認できました。
contextパッケージにはまだまだ便利な機能が備わっているので、活用していきましょう。

まとめ

goroutineは軽量なスレッドとはいえ、リークしてしまうとプロセスが終了するまで永遠にメモリを使用し続けます。
今回は使用していませんが、検証用にuber-go/goleak のようなパッケージをテストに組み込むのもよさそうです。
goroutineは簡単に使用できますが、安易に使用しないように気をつけていきましょう。

この記事を書いた人

アーティス
アーティス
創造性を最大限に発揮するとともに、インターネットに代表されるITを活用し、みんなの生活が便利で、豊かで、楽しいものになるようなサービスやコンテンツを考え、創り出し提供しています。
この記事のカテゴリ

FOLLOW US

最新の情報をお届けします