NU:LOGiC Logo

【Go言語】非同期プログラミング入門:Goルーチン(goroutine)と並行処理の基礎

【Go言語】非同期プログラミング入門:Goルーチン(goroutine)と並行処理の基礎

この記事では、Go言語の非同期プログラミングにおいて重要な、goroutineと、チャネルを介してgoroutine間でデータを安全にやり取りし、非同期処理のパターンとトラブルシューティングのテクニックを紹介します!

3行で要約すると

  • Go言語の非同期プログラミングにおいて、goroutineは軽量なスレッドのような役割を果たします。
  • チャネルを用いてgoroutine間でデータを安全にやり取りする方法を解説します。
  • 非同期処理のパターンとトラブルシューティングのテクニックを実例を交えて紹介。

goroutineとは?

基本的な概念

goroutineとは、Go言語で非同期タスクを実行するための仕組みです。イメージとしては、軽量なスレッドと考えてもらって構いません。一つのgoroutineが何か重い処理をしている間も、他のgoroutineは止まることなく動作します。

使い方

goroutineを作成するには、関数呼び出しの前にgoキーワードをつけます。例えば、doSomething()という関数があった場合、その関数をgoroutineとして実行するには以下のようにします。

package main

import (
 "fmt"
 "time"
)

func doSomething() {
 fmt.Println("Doing something...")
 time.Sleep(2 * time.Second)
 fmt.Println("Done!")
}

func main() {
 go doSomething()
 fmt.Println("Doing other stuff...")
 time.Sleep(3 * time.Second)
}

出力

Doing other stuff...
Doing something...
Done!

"Doing other stuff..."が最初に出力される点に注意してください。これはdoSomething()関数が非同期で動作しているためです。

チャネルでのデータのやり取り

チャネルの基本

チャネル(channel)は、goroutine間でデータを送受信するパイプのようなものです。チャネルはchanキーワードで型を指定して作成します。

ch := make(chan int)

データの送受信

データは<-演算子を使用して送受信します。

package main

import "fmt"

func main() {
 ch := make(chan int)
 go func() {
  ch <- 42
 }()
 value := <-ch
 fmt.Println("Received:", value)
}

出力

Received: 42

この例では、無名関数( go func(){} )をgoroutineとして実行し、42 をチャネルchに送信しています。その後、 value := <-ch で受信しています。

非同期処理のパターン

ワーカープール

複数のgoroutineを管理して、効率的にタスクを処理する方法として「ワーカープール」があります。これは、一定数のgoroutineが並列でタスクを取得して実行する仕組みです。

package main

import (
 "fmt"
 "time"
)

func worker(id int, tasks chan int) {
 for task := range tasks {
  fmt.Printf("Worker %d processing task %d\n", id, task)
  time.Sleep(time.Second)
 }
}

func main() {
 tasks := make(chan int, 100)

 for i := 1; i <= 3; i++ {
  go worker(i, tasks)
 }

 for i := 0; i < 10; i++ {
  tasks <- i
 }
 close(tasks)
}

出力

Worker 1 processing task 0
Worker 2 processing task 1
Worker 3 processing task 2
...

ここでは、3つのワーカーgoroutineがタスクを非同期に処理しています。

タイムアウトの設定

Goのcontextパッケージを使うと、goroutineにタイムアウトを設定できます。

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

タイムアウトの例

以下のコードは、goroutineに2秒のタイムアウトを設定する例です。

package main

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

func main() {
 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
 defer cancel()

 go func() {
  time.Sleep(3 * time.Second)
  fmt.Println("Task completed")
 }()

 select {
 case <-ctx.Done():
  fmt.Println("Timeout reached")
 }
}

出力

Timeout reached

タスクが3秒かかるのに対し、タイムアウトは2秒後に設定されています。そのため、Timeout reached と出力されます。

トラブルシューティングとデバッグ

デッドロック

デッドロックはgoroutineが永遠に動作を停止する現象です。これを避けるためには、チャネルを適切に閉じる、goroutineを適切に終了するなどの対策が必要です。

レースコンディション

複数のgoroutineが同時に同じデータにアクセスすると、予期せぬ動作(レースコンディション)が起こる可能性があります。これを防ぐためには、syncパッケージのMutexを使いましょう。

var mu sync.Mutex
mu.Lock()
// critical section
mu.Unlock()

まとめ

この記事では、Go言語の非同期プログラミングにおける基礎から応用テクニックまでを解説しました。goroutineとチャネルをうまく使いこなして、効率的なコードを書いていきましょう!