go での同時実行を理解する


Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once - Rob Pike



Go 言語自体は、開発者がより優れた、より高速で効率的なコードを記述できるように、すべての複雑さを隠して、箱から出して同時実行を処理するためのいくつかの機能を提供します.これらの機能は次のとおりです.

ゴルーチン: ゴルーチンは、Go ランタイムによって管理される軽量スレッドです.

関数呼び出しの前に go を追加してゴルーチンに変換するだけで、同時実行性を利用できます.

func main() {
    go say("hello") // concurrently executes func say
}

// you might also see the following pattern
func main() {
    go func(greeting string){ // concurrently executes anonymous func and 
        say(greeting) // call our function inside it
    }("hello")
}


チャネル: チャネルは、チャネル演算子 <- で値を送受信できる型付きコンジットです.

ch := make(chan int) // unbuffered channel
ch := make(chan int, 30) // buffered channel
ch := make(<-chan int) // Receive only channel
ch := make(chan<- int) // Send only channel

ch <- 1 // sends value to channel
x = <-ch // assign value from channel to x

close(ch) // close channel and clean memory


WaitGroups: WaitGroup は、ゴルーチンのコレクションが終了するのを待ちます.

var wg sync.WaitGroup

func main() {
    wg.Add(1) // Add to set the number of goroutines to wait for
    go func(asyncFuncParam string) {
        // each of the goroutines runs and calls Done when finished
        defer wg.Done() // executed after functions returns
        asyncFunc(asyncFuncParam)
    }("USING WAIT GROUP")
    wg.Wait() // block until all goroutines have finished
}


進行中の同時実行



ゴルーチンを利用していない場合は、その理由を自問してみてください.つまり、コードを faaassstttt にしたくないのはなぜですか?コードの順次実行が重要な場合はたくさんありますが、ここでは実際には何もできません.同時実行を利用できる場合でも、制御されていない同時実行は有害な場合があります.例: 複数の同時 API 要求を行うと、レート制限された HTTP 429 - 要求が多すぎるという問題が発生する可能性があります.そのような場合でも、チャネルなどの言語機能を利用し、そのプロパティを使用して同時実行を制限できます.

この投稿では、同時実行を処理する 2 つの異なる方法を共有しました.

制御されていない同時実行



これにより、システムが処理できる数のゴルーチンが並行モードで実行されるようにキューに入れられます.プログラムに互いに独立した多くの自律的な部分がある場合にこれを使用します

var wg sync.WaitGroup

for _, item := range items {
    wg.Add(1)
    go func(asyncFuncParam string) {
        defer wg.Done()
        asyncFunc(asyncFuncParam)
    }("USING WAIT GROUP")
}
wg.Wait()


制御された同時実行



バウンデッド キューを作成し、設定された秒あたりの制限に従ってゴルーチンを制限し、バケット化/短いバースト パターンに従って同時実行モードで実行します.プログラムに互いに独立した多くの自律的な部分があるが、何らかのボトルネックのためにレートが制限されている場合に、これを使用します.

const (
    CONCURRENCY_LIMIT = 30 // max 30 items in a channel per second 
)

guard := make(chan struct{}, CONCURRENCY_LIMIT)

for _, item := range items {
    guard <- struct{}{}
    go func(asyncFuncParam string) {
        defer func() { <-guard }()
        asyncFunc(asyncFuncParam)
    }("USING CHANNELS FOR CONTROLLED CONCURRENCY")
}


ベンチマーク



200 以上の API リクエスト


モード
実行時間
結果


同期
約1分20秒
すべての API が成功しました

制御されていない同時実行
〜5秒
多くの API がエラーで失敗しました

制御された同時実行
〜10秒
すべての API が成功しました


間違いなく、制御されていない同時実行がジョブを完了するのに最速ですが、最終的にすべての応答を正常に取得できませんでした.同時実行を制御することで、手動でパフォーマンスを微調整し、レート制限と実行時間の適切なバランスを見つけることができます.

学習リソース


  • A Tour of Go の同時実行セクション - 最高の公式リソースの 1 つ https://tour.golang.org/concurrency/1
  • ゴルーチンの内部動作を知りたい場合は、次の記事を読むことを強くお勧めします https://medium.com/the-polyglot-programmer/what-are-goroutines-and-how-do-they-actually-work-f2a734f6f991