Skip to content

Goの並行処理と並列処理

Goは、**ゴルーチン(goroutines)とチャネル(channels)**という独自の機能を通じて、並行処理を簡単に扱えるよう設計されています。これにより、複雑なマルチスレッドプログラミングの課題を回避し、シンプルかつ効率的なプログラムを書くことができます。

1. 並行処理(Concurrency)と並列処理(Parallelism)

Section titled “1. 並行処理(Concurrency)と並列処理(Parallelism)”

この2つの概念は混同されがちですが、Goでは明確に区別されます。

  • 並行処理: 複数のタスクを同時に進行させること。タスクは交互に実行され、見かけ上同時に動いているように見えます。これは、シングルコアCPUでも実現可能です。
  • 並列処理: 複数のタスクを同時に実行すること。これは、マルチコアCPUでのみ可能です。Goは、Goランタイムが自動的にゴルーチンを複数の論理プロセッサに割り当てることで並列処理を実現します。

Goの哲学は、「並行処理は並列処理とは異なる。並行処理は多くのことを同時に扱うことであり、並列処理は多くのことを同時に実行することである。」というものです。

ゴルーチンは、Goにおける並行処理の基本単位です。OSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを簡単に生成できます。

  • 生成: goキーワードを関数の前に置くだけで、その関数は新しいゴルーチンとして実行されます。
  • 通信の哲学: Goの並行処理は、**「共有メモリによる通信」ではなく、「通信によるメモリ共有」**という設計思想に基づいています。これにより、従来のマルチスレッドプログラミングで問題になりがちな競合状態(race conditions)を回避できます。

コード例: ゴルーチンの簡単な利用

Section titled “コード例: ゴルーチンの簡単な利用”
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 3; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("hello") // ゴルーチンとして実行
say("world") // 通常の関数として実行
}

このコードを実行すると、helloworldが交互に出力されることがわかります。これは、2つのタスクが並行して実行されていることを示しています。

チャネルは、ゴルーチン間で安全にデータを送受信するためのパイプです。これにより、ゴルーチン間の同期と通信をシンプルに行えます。

  • 作成: make(chan Type)でチャネルを作成します。
  • 送受信:
    • ch <- valueでチャネルに値を送信します。
    • <-chでチャネルから値を受信します。

コード例: ゴルーチンとチャネルの連携

Section titled “コード例: ゴルーチンとチャネルの連携”
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 結果をチャネルに送信
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
// スライスの半分をそれぞれ別のゴルーチンで計算
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 2つのチャネルから結果を受信
fmt.Println(x, y, x+y) // 実行ごとにxとyの順番が変わる可能性がある
}

この例では、2つのゴルーチンがそれぞれ計算した結果をチャネルに送信し、main関数がそれらを受信して合計を求めています。

  • select: 複数のチャネルの操作を待機し、準備ができた最初の操作を実行します。非同期処理でタイムアウトを設定したり、複数のリソースからデータを処理したりする際に非常に便利です。
  • syncパッケージ: チャネルが適さない場合、syncパッケージ(sync.Mutex, sync.WaitGroupなど)を使って従来の共有メモリ同期を実装できます。

Goの並行処理は、これらのシンプルで強力なツールによって、複雑なマルチタスク処理を安全かつ効率的に記述することを可能にしています。