soarflat の技術メモ

主にフロントエンドに関する技術メモ

【Go】semaphore とは? 〜semaphore を利用してゴルーチンの同時実行数を制御する〜

記事概要

semaphoreに関しての備忘録。

  • semaphore はどんなパッケージなのか
  • そもそも semaphore(セマフォ)という言葉の意味に関して
  • semaphore の利用例

semaphore とは

ゴルーチンの同時実行数を制御するパッケージ。

https://godoc.org/golang.org/x/sync/semaphore

読み方はセマフォ

そもそも semaphore(セマフォ)という言葉の意味は?

  • 同時に実行されているプログラム間でのリソースの排他制御や同期を行う仕組みのこと。
  • 現在利用可能なリソースの数のこと。

以下の記事でなんとなく仕組みは理解できる。とりあえず「同時実行を制御するための仕組み・手法」ぐらいな認識をしておけば問題ないと思う。

仕組みをざっくり理解できたら、コードを書いて動きを確認した方が理解が深まると思う。

利用例

パッケージを取得していなければ、go getで取得しておく。

$ go get golang.org/x/sync/semaphore

ゴルーチンの同時実行数を1つにする

以下は3つのゴルーチンが並列で実行されるコード。

package main

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

func longProcess(ctx context.Context) {
    fmt.Println("Wait...")
    time.Sleep(1 * time.Second)
    fmt.Println("Done")
}

func main() {
    ctx := context.TODO()
    go longProcess(ctx)
    go longProcess(ctx)
    go longProcess(ctx)
    time.Sleep(5 * time.Second)
}

以下のように並列で実行される。

ゴルーチンを並列に実行するプログラムを動作させた時の出力

semaphore を利用して、上記のゴルーチンを1つずつ実行させる場合、以下のようになる。

package main

import (
    "context"
    "fmt"
    "golang.org/x/sync/semaphore"
    "time"
)

// 引数に渡した数が最大カウント(同時アクセス数)として指定されたセマフォを作成する。
// 今回は NewWeighted(1) なので同時に実行できるゴルーチンは1つ。
var s *semaphore.Weighted = semaphore.NewWeighted(1)

func longProcess(ctx context.Context) {
    // s.Acquire(ctx, 1) でセマフォを取得する。セマフォを取得することで、セマフォのカウントが1つ減る。
    // 今回のセマフォの最大カウントは1なので、セマフォのカウントは0になり、他のゴルチーンはブロックされて待機状態になる。
    if err := s.Acquire(ctx, 1); err != nil {
        fmt.Println(err)
        return
    }
    // セマフォをリリースする。セマフォのカウントは1に戻り、待機状態のゴルーチンが実行される。
    defer s.Release(1)
    fmt.Println("Wait...")
    time.Sleep(1 * time.Second)
    fmt.Println("Done")
}

func main() {
    ctx := context.TODO()
    go longProcess(ctx)
    go longProcess(ctx)
    go longProcess(ctx)
    time.Sleep(5 * time.Second)
}

以下のように同時実行数が1つになった。

ゴルーチンの同時実行数を1つにしたプログラムを動作させた時の出力

セマフォが取得できなかった場合(待機状態になる場合)、待機せずにゴルーチンを終了する

package main

import (
    "context"
    "fmt"
    "golang.org/x/sync/semaphore"
    "time"
)

// 引数に渡した数が最大カウント(同時アクセス数)として指定されたセマフォを作成する。
// 今回は NewWeighted(1) なので同時に実行できるゴルーチンは1つ。
var s *semaphore.Weighted = semaphore.NewWeighted(1)

func longProcess(ctx context.Context) {
    // s.TryAcquire(1) でセマフォを取得する。
    // 成功すれば true を返し、失敗(他のゴルーチンがセマフォを取得しており、セマフォを取得できないなどが原因で失敗)すれば false を返す。
    isAcquire := s.TryAcquire(1)
    if !isAcquire {
        fmt.Println("Could not get lock")
        return // ゴルチーンを終了
    }
    // セマフォをリリースする。セマフォのカウントは1に戻り、待機状態のゴルーチンが実行される。
    defer s.Release(1)
    fmt.Println("Wait...")
    time.Sleep(1 * time.Second)
    fmt.Println("Done")
}

func main() {
    ctx := context.TODO()
    go longProcess(ctx)
    go longProcess(ctx)
    go longProcess(ctx)
    time.Sleep(5 * time.Second)
}

以下のように1つのゴルーチンだけ最後まで実行され、他のゴルーチンは途中で終了した。

セマフォが取得できなかった場合(待機状態になる場合)、待機せずにゴルーチンを終了するプログラムを動作させた時の出力