はじめに
Go言語は、その簡潔さや高いパフォーマンス、そして並行処理や並列処理を容易に実現できる機能によって、多くの開発者から支持されています。この記事では、Go言語での並行処理
と並列処理
の実現方法、特にゴルーチンとチャネルを利用した方法について解説します。
ゴルーチンとは
ゴルーチンは、Go言語独自の並行処理の仕組みで、軽量なスレッドとも呼ばれます。関数の前にgo
キーワードをつけるだけで、新しいゴルーチンを生成できます。ゴルーチンは、メモリ消費が少なく、数十万から数百万のゴルーチンを同時に実行することが可能です。
package main
import (
"fmt"
"time"
)
func printHello() {
fmt.Println("Hello, Go!")
}
func main() {
go printHello()
time.Sleep(1 * time.Second)
}
チャネル
チャネルは、ゴルーチン間でデータの受け渡しや同期を行うための機能です。チャネルは、make()
関数で作成し、<-
演算子を使ってデータを送受信します。
下記のコードのmessageChanne
は、Go言語のチャネル(channel
)という機能を使って作成されたもので、ゴルーチン間でデータのやり取りを行うためのパイプのようなものと考えることができます。
例えば、水を運ぶためのパイプがあるとします。一方のゴルーチンはパイプに水を送り込む役割(データの送信)を持ち、もう一方のゴルーチンはパイプから水を受け取る役割(データの受信)を持ちます。このとき、messageChannel
はそのパイプの役割を果たしています。
チャネルは、データの送信側ゴルーチンと受信側ゴルーチンの間で同期を取り、データの受け渡しを行うための仕組みを提供します。これにより、簡単かつ安全にゴルーチン間でデータのやり取りができるようになります。
package main
import "fmt"
func sendData(channel chan string) {
channel <- "Hello, Go!"
}
func main() {
messageChannel := make(chan string)
go sendData(messageChannel)
message := <-messageChannel
fmt.Println(message)
}
バッファ付きチャネル
バッファ付きチャネルは、チャネルに一定量のデータを格納できるようにすることで、ゴルーチン間のブロッキングを軽減できます。バッファ付きチャネルは、make()
関数の第2引数にバッファサイズを指定して作成します。
package main
import "fmt"
func sendData(channel chan string) {
channel <- "Hello, Go!"
}
func main() {
messageChannel := make(chan string, 1)
go sendData(messageChannel)
message := <-messageChannel
fmt.Println(message)
}
ゴルーチンとチャネルを使った並行処理のパターン
-
パイプライン
: ゴルーチンとチャネルをつなげてデータの処理フローを構築する。 -
チャネルの選択
: selectステートメントを使用して、複数のチャネルからの入力を待機する。
※ ファンイン、ファンアウトについて
ファンイン(Fan-in)とファンアウト(Fan-out)は、並行処理のデザインパターンで、それぞれ複数のゴルーチンから1つのゴルーチンへのデータの収束(マージ)と、1つのゴルーチンから複数のゴルーチンへのデータの分散を指します。Go言語では、これらのパターンをチャネルを使って実現することができます。
-
ファンイン(Fan-in):
ファンインは、複数のゴルーチンからデータを受け取り、それらを1つのゴルーチンで処理するパターンです。例えば、複数のデータソースから情報を取得し、それらをマージして最終的な結果を生成する場合に使用されます。
実装方法は、複数のゴルーチンが同じチャネルにデータを送信し、1つのゴルーチンがそのチャネルからデータを受信して処理を行うことです。 -
ファンアウト(Fan-out):
ファンアウトは、1つのゴルーチンからデータを受け取り、それらを複数のゴルーチンに分散して処理するパターンです。例えば、大量のデータを分割して、複数のゴルーチンで並行して処理を行い、処理時間を短縮する場合に使用されます。
実装方法は、1つのゴルーチンがデータをチャネルに送信し、複数のゴルーチンがそのチャネルからデータを受信してそれぞれの処理を行うことです。
Go言語のチャネルを使ってファンインやファンアウトを実装することで、並行処理による効率的なデータ処理が可能になります。これにより、処理の分散やマージを簡単かつ安全に行うことができます。
ファンインの例
package main
import (
"fmt"
"sync"
)
func sendData(channel chan string, data string, wg *sync.WaitGroup) {
defer wg.Done()
channel <- data
}
func main() {
messageChannel := make(chan string)
var wg sync.WaitGroup
wg.Add(2)
go sendData(messageChannel, "Hello, Go!", &wg)
go sendData(messageChannel, "Concurrency is fun!", &wg)
go func() {
wg.Wait()
close(messageChannel)
}()
for message := range messageChannel {
fmt.Println(message)
}
}
ファンアウトの例
package main
import (
"fmt"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("worker %d started job %d\n", id, j)
time.Sleep(time.Second)
fmt.Printf("worker %d finished job %d\n", id, j)
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)
for a := 1; a <= 9; a++ {
<-results
}
}
パイプラインの例
package main
import "fmt"
func generator(nums ...int) chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func multiplier(in chan int) chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * 2
}
close(out)
}()
return out
}
func main() {
numbers := generator(1, 2, 3, 4, 5)
multiples := multiplier(numbers)
for m := range multiples {
fmt.Println(m)
}
}
チャネルの選択の例
package main
import (
"fmt"
"time"
)
func main() {
channel1 := make(chan string)
channel2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
channel1 <- "Hello from channel 1!"
}()
go func() {
time.Sleep(2 * time.Second)
channel2 <- "Hello from channel 2!"
}()
for i := 0; i < 2; i++ {
select {
case message1 := <-channel1:
fmt.Println(message1)
case message2 := <-channel2:
fmt.Println(message2)
}
}
}
まとめ
Go言語での並行処理と並列処理の基本的な概念、ゴルーチンとチャネルの使い方、そして実際のコード例を通じて、いくつかの一般的な並行処理パターンを紹介しました。これらの知識とテクニックを活用することで、効率的でスケーラブルなGoアプリケーションを開発することが可能です。