Help us understand the problem. What is going on with this article?

goroutine を安全に止める方法

More than 1 year has passed since last update.

メインの処理から、無限ループするgoroutineを止める、といった例で考えます。

func main() {
    go loop()

    // loopが終わるのを待つ何らかの処理
    ...
}

// 無限ループする関数
func loop() {
    for {
        println("loop...")
        time.Sleep(1 * time.Second)
    }
}

1つの goroutine を止める

まずは、goroutineが1つの場合から考えます。

goroutineに「止まれ」と命令を送っても、その瞬間安全に止まるとは限らないので、「無事に止まったよ」という通知をgoroutineから貰うまで待つ必要があります。

ここでは、「止まれ」命令=stop、 「止まったよ」通知=done という名前で扱います。

順序 call場所
1 main goroutine 開始
2 main stop 通知送る
3 goroutine stop 通知受け取り、ループから脱出
4 goroutine 脱出時に done 通知を送る
5 main done 通知を待ってから、終了する

という流れになるため、2つのchannelをつくります

  • (main) -> (goroutine) に停止通知を送るchannel
  • (goroutine) -> (main) に完了通知を送るchannel
package main

import (
    "time"
)

func main() {
    stopCh := make(chan struct{})
    doneCh := make(chan struct{})

    go loop(stopCh, doneCh)

    time.Sleep(3 * time.Second)
    println("request stop.")
    close(stopCh)

    // loopが完了するまで待つ
    <-doneCh
    println("loop done.")
}

// stopCh: 外部からこのloopを止めたい という通知をするchannel
// doneCh: 内部からこのloopが完了した という通知をするchannel
func loop(stopCh, doneCh chan struct{}) {
    // リソースの解放が必要であれば、ここでする
    defer func() { close(doneCh) }()

    // 無限ループ
    for {
        println("loop...")
        time.Sleep(1 * time.Second)

        select {
        case <- stopCh:
            println("stop request received.")
            return
        default:
            // default: 重要!!
            // これがないと、stopChを待ち続けてしまうので、loopが続行できなくなる
        }
    }
}

出力

(goroutine) loop...
(goroutine) loop...
(goroutine) loop...
(main) stop request sent.
(goroutine) stop request received.
(main) loop done.

ポイント

  • 完了通知を送る際、 channel にダミーの値を送る必要はなく、 close() でよい
  • selectdefault: をつけるとブロッキングしない select になる

複数の goroutine を止める

goroutine の数が1つであれば、channel でも良いですが、複数の goroutine の完了を待つのであれば、 sync.WaitGroup が良いです。

順序 call場所
1 main goroutine 1 開始 (waitGroup +1)
2 main goroutine 2 開始 (waitGroup +1)
3 main stop 通知送る
4 goroutine 1 stop 通知受け取り、ループから脱出 (waitGroup -1)
5 goroutine 2 stop 通知受け取り、ループから脱出 (waitGroup -1)
6 main WaitGroupが 0 になったので終了
package main

import (
    "time"
    "sync"
)

func main() {
    stopCh := make(chan struct{})
    wg := sync.WaitGroup{}

    wg.Add(1)
    go loop(stopCh, &wg)

    wg.Add(1)
    go loop(stopCh, &wg)

    time.Sleep(3 * time.Second)
    println("(main) request stop.")
    close(stopCh)

    // loopが完了するまで待つ
    wg.Wait()
    println("(main) all loops done.")
}

// stopCh: 外部からこのloopを止めたい という通知をするchannel
// wg: このloopが完了したら Done() を呼び出す
func loop(stopCh chan struct{}, wg *sync.WaitGroup) {
    // リソースの解放が必要であれば、ここでする
    defer func() { wg.Done() }()

    // 無限ループ
    for {
        println("(goroutine) loop...")
        time.Sleep(1 * time.Second)

        select {
        case <- stopCh:
            println("(goroutine) stop request received.")
            return
        default:
            // default: 重要!!
            // これがないと、stopChを待ち続けてしまうので、loopが続行できなくなる
        }
    }
}

出力

(goroutine) loop...
(goroutine) loop...
(goroutine) loop...
(goroutine) loop...
(goroutine) loop...
(goroutine) loop...
(main) request stop.
(goroutine) stop request received.
(goroutine) stop request received.
(main) all loops done.

エラーを取りたい場合

Wait() した際に、どこかの goroutine 内部で最後に起こったエラーを取得したい場合
errGroup パッケージ が使えます。

package main

import (
    "time"
    "golang.org/x/sync/errgroup"
    "errors"
)

func main() {
    stopCh := make(chan struct{})
    eg := errgroup.Group{}

    eg.Go(func() error {
        return loop(stopCh)
    })

    eg.Go(func() error {
        return loop(stopCh)
    })

    time.Sleep(3 * time.Second)
    println("(main) request stop.")
    close(stopCh)

    // loopのどれかが最初にエラーを返した時点でWaitが完了する
    if err := eg.Wait(); err != nil {
        println("(main) error!")
    }
    println("(main) all loops done.")
}

// stopCh: 外部からこのloopを止めたい という通知をするchannel
func loop(stopCh chan struct{}) error {
    // リソースの解放が必要であれば、ここでする

    // 無限ループ
    for {
        println("(goroutine) loop...")
        time.Sleep(1 * time.Second)

        select {
        case <- stopCh:
            println("(goroutine) stop request received.")
            // エラーを返した時点で errGroup に通知される
            return errors.New("stop")
        default:
            // default: 重要!!
            // これがないと、stopChを待ち続けてしまうので、loopが続行できなくなる
        }
    }
}

context パッケージ

最近導入されたcontext パッケージは、非同期処理をキャンセルする方法を提供します。
この仕組みを使って、goroutine に対してキャンセルを送信することで通知することもできます。

contextパッケージについては、以下のサイト等を読むとわかりやすいです。

参考にしたページ

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした