LoginSignup
7
6

【Go並行プログラミング】A Tour of Goの説明が頭に入らない人のためのゴルーチン&チャネル入門🌟

Last updated at Posted at 2024-03-09

はじめに

私はこれまでPython、JavaScript/TypeScript、PHPといった言語を中心にコーディングしてきました。しかし、仕事の都合上、Go言語の学習が必要になり、少しずつ学び始めてから3週間が経ちました。

2週目でポインタにつまづき、なんとか少し理解できるようになりました。そのときの学びログです。

3週目に入り、今度は並行処理でこけました。なんとか少し理解できるようになったので、私と同じく並行処理が全然頭に入ってこない人に向けて、学びログを兼ねた記事を作成しました。

誰に向けた記事?

  • 「A Tour of Go: Concurrency」に挑戦したが、説明が全く入ってこない人
  • 一通りゴルーチンやチャネルについて学んだが、まだ学んだことを整理できていない人

huh.gif

前提

  • Goの基本的な文法(「A Tour of Go: Basics」レベル)はさらっと舐めた

🌟ゴルーチン: Goの並行処理を支える仕組み

Goプログラミング言語の大きな特徴の一つに、並行処理を簡単に実装できる点が挙げられます。その中核となるのが「ゴルーチン」です。本記事では、ゴルーチンとは何か、どのように使うのかを解説します。

【メインルーチン】 通常の順次処理

ゴルーチンを使う場合とそうでない場合で、比較をしたいので、まずは、通常の順次処理が行われるコードとその実行の様子を表した図を確認していきましょう。

package main

import (
	"fmt"
	"time"
)

// 引数の秒数間スリープさせる関数
func wait(t time.Duration) {
	time.Sleep(t * time.Second)
	fmt.Printf("%d秒経過しました\n", t)
}

func main() {
	// 計測開始
	fmt.Printf("計測を開始します\n")
	s := time.Now()

	wait(3)
	wait(5)
	wait(1)

	// 計測時間を出力
	fmt.Printf("経過時間: %s\n", time.Since(s))
}
出力
計測を開始します
3秒経過しました
5秒経過しました
1秒経過しました
経過時間: 9.003455833s

当然のことですが、前から順番に処理が実行され、前の処理が終了し次第、次の処理が実行されます。したがって、実行完了まで約9秒ほどかかっています。

normal (2).jpg

図にmain routineとあるように、Go言語において通常の処理はメインルーチンと呼ばれる部分で順次実行されます。

【ゴルーチン】 サブルーチンによる実行

並行実行したい関数の前にgoをつけて呼び出すと、その関数はメインルーチンとはことなる部分で処理が実行されます。メインルーチン以外のルーチンのことをサブルーチンと呼ぶことにします。

(※メインルーチンとサブルーチンの区別を気にしないときは、単にゴルーチンと呼びます)

go wait(3)
wait(5)
wait(1)
出力
計測を開始します
3秒経過しました
5秒経過しました
1秒経過しました
経過時間: 6.002480834s

下図のように、3秒待つ処理だけ、サブルーチンで実行されるため、合計して約6秒で処理が完了しています。

goroutine1 (2).jpg

複数のサブルーチンを生成することも可能

サブルーチンを同時に複数生成することもできます。

go wait(1)
go wait(3)
wait(5)
出力
計測を開始します
1秒経過しました
3秒経過しました
5秒経過しました
経過時間: 5.001162292s

この例では、2つのサブルーチンでそれぞれ、wait(1)wait(3)を処理させることで、合計して約5秒で処理を完了させることができました。さらに短くなりましたね。

goroutine2 (1).jpg

無名関数にもgoをつけられる

次のように無名関数にgoをつけて、並行処理を行うこともできます!

package main

import (
	"fmt"
	"time"
)

func wait(t time.Duration) {
	time.Sleep(t * time.Second)
	fmt.Printf("%d秒経過しました\n", t)
}

func main() {
	// 計測開始
	fmt.Printf("計測を開始します\n")
	s := time.Now()

	// 💡 無名関数をサブルーチンで実行
	go func() {
		time.Sleep(1 * time.Second)
		fmt.Printf("1秒経過しました\n")
	}()

	wait(3)

	// 計測時間を出力
	fmt.Printf("経過時間: %s\n", time.Since(s))
}
出力
計測を開始します
1秒経過しました
3秒経過しました
経過時間: 3.00103975s

【注意】 サブルーチンで処理中にメインルーチンの処理が終わってしまうと...

サブルーチンで処理中にメインルーチンの処理が終わってしまうと、サブルーチンの処理が途中であっても終了してしまいます。

go wait(5)
wait(1)
wait(3)

このように、サブルーチンで実行されているwait(5)が終了する前に、メインルーチンでの処理が終わってしまうので、サブルーチンの処理は打ち切られます。

出力
計測を開始します
1秒経過しました
3秒経過しました
経過時間: 4.001787625s

goroutine3.jpg

【WaitGroup】 並行処理が終わるまで待ちたい!

上述の通り、メインルーチンが終了してしまうと強制的にサブルーチンの処理は終了してしまいます。では、並行処理しているサブルーチンの完了を待ちたい場合は、どうすればいいでしょうか?

そのような場合は、syncパッケージのウェイトグループ(WaitGroup)というものを使って、サブルーチンの完了を待つことができます。

sync.WaitGroupの使用法

1️⃣: まず、sync.WaitGroupの値を保存する変数を宣言します。

sync.WaitGroupを宣言
var wg sync.WaitGroup

2️⃣: そして、wg.Add()を使って終了を待ちたいサブルーチンの数を設定します。

wg.Add(n)
wg.Add(4)

3️⃣: サブルーチン内のすべてのコードが実行されたら、最後にwg.Done()を実行するようにします。wg.Done()を実行するとwgがカウントしている数字がデクリメントされます。

wg.Done()
go func() {
    defer wg.Done() // 👈 deferとセットで使うとよいです
    // 処理...
}()

4️⃣: サブルーチンの処理を待ちたい箇所でwg.Wait()を実行すると、すべてのタスクが終わる( = wgのカウントが0になる)まで、その場所で待つことができます。

wg.Wait()
wg.Add(1)

go func() {
    defer wg.Done()
    // 処理...
}()

wg.Wait() // ここで待ちます。

sync.WaitGroupのサンプルコード

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	// 計測開始
	fmt.Printf("計測を開始します\n")
	s := time.Now()

	// 1️⃣
	var wg sync.WaitGroup

	// 2️⃣
	wg.Add(1)
	go func() {
		// 3️⃣
		defer wg.Done()
		time.Sleep(1 * time.Second)
		fmt.Printf("1秒経過しました\n")
	}()

	// 4️⃣
	wg.Wait()

	// 計測時間を出力
	fmt.Printf("経過時間: %s\n", time.Since(s))
}
出力
計測を開始します
1秒経過しました
経過時間: 1.001222334s

🌟チャネル: ゴルーチン間の通信を支える仕組み

ゴルーチンでは情報のやり取りにチャネル(channel)というものを使います。チャネルはゴルーチン間で、特定の型の値を受け渡す通信機構です。

チャネルの宣言と使い方

例えば、int型の値を受け渡したいとします。まずは、次のようにmake関数を使ってチャネルを作成します。

ch := make(chan int)

使い方は簡単で、右から左へ受け流すだけです。ムーディー勝山のイメージを持っておいてください。

値を送信したいときは、右から

右から右から何かが来てる
ch <- 1

値を受信したいときは、左から

それを僕は左へ受け流す
v := <-ch

です。

チャネルを使用したサンプルコード

package main

import (
	"fmt"
)

func main() {
	ch := make(chan int)

	go func() {
		// 右から、右から何かが来てる〜
		ch <- 1
	}()

	// 僕はそれを左へ受け流す〜
	v := <-ch

	fmt.Printf("右から来て左へ受け流されたもの 👉 %d\n", v)
}
出力
右から来て左へ受け流されたもの 👉 1

送信専用チャネルと受信専用チャネル

先述の通り、チャネルを利用すれば、異なるゴルーチン間で値を送受信できます。

チャネルに値を送信する
ch <- 1
チャネルから値を受信する
v := <-ch

しかし、これは本来受信しかしてはいけないゴルーチンが誤ってチャネルに値を送信したり、送信しかしてはいけないゴルーチン内で受信をしてしまうなどのバグを生み出すリスクが伴います。

ですから、チャネルを共有するときに、「これは送信専用で使ってね」「これは受信専用で使ってね」と使用を制限して共有できたらHappyです。

happy-cat-happy-happy-cat.gif

それを可能にするのが、受信専用チャネル型と送信専用チャネル型です。

  • chan T: 送受信可能チャネル
  • chan<- T: 送信専用チャネル
  • <-chan T: 受信専用チャネル

チャネルを渡すときに、用途に応じた専用型へキャストすることで、誤ったチャネルの利用を防ぎます。

送信専用チャネル

まずは、送信専用チャネルから

送信専用チャネル
package main

import (
	"fmt"
)

// チャネルを受け取ると送信専用チャネル型にキャストする
func sendOnly(ch chan<- int) {
	ch <- 1
}

func main() {
	ch := make(chan int)

	go sendOnly(ch)

	v := <-ch

	fmt.Printf("Received 👉 %d\n", v)
}
出力
Received 👉 1

送信専用チャネルで値を受信しようとするとエラーになります。

送信専用チャネルで受信
package main

import (
	"fmt"
)

func sendOnly(ch chan<- int) {
	// 送信専用チャネルで受信する
	v := <-ch
	fmt.Printf("Received 👉 %d\n", v)
}

func main() {
	ch := make(chan int)

	go sendOnly(ch)
}
出力
# command-line-arguments
./main.go:9:13: invalid operation: cannot receive from send-only channel ch (variable of type chan<- int)

受信専用チャネル

package main

import (
	"fmt"
	"time"
)

func receivedOnly(ch <-chan int) {
	v := <-ch
	fmt.Printf("Received 👉 %d\n", v)
}

func main() {
	ch := make(chan int)

	go receivedOnly(ch)

	ch <- 1

	// ゴルーチン内の処理が終了する前に、メインルーチンが終了しないように仮置き
	time.Sleep(time.Second)
}
出力
Received 👉 1

送信専用チャネルと同様、受信専用チャネルで値を送信しようとするとエラーになります。

チャネルはいい子だから、送受信の準備が整ってから、受け流してくれるのよ

[再掲]送信専用チャネル
// 略

func sendOnly(ch chan<- int) {
	ch <- 1
}

func main() {
	ch := make(chan int)

	go sendOnly(ch)

	v := <-ch

	fmt.Printf("Received 👉 %d\n", v)
}

これは送信専用チャネルの説明で使用したコードです。上記の処理を見て、「なぜ必ずv := <-chch <- 1の後に実行されるのか?」と疑問に思った方もいるかもしれません。並行処理をしているので、受信(v := <-ch)が先に実行される可能性もあると思いますよね?

チャネルは偉いので、受信するときは、ちゃんと値が送信されるのまで、そのルーチンの処理を止めます。逆に、送信するときも、別ルーチンで受信者が登場するまで、そのルーチンの処理を止めます。具体的に、

  1. v := <-ch」へ先に到達したケース
  2. ch <- 1」へ先に到達したケース

の両ケースで、何が起こっているか確認しましょう。

v := <-chへ先に到達したケース

func sendOnly(ch chan<- int) {
    // 1️⃣ 送信を受け付けたいですが、まだ受信者が現れていないので、ここで待っててください。
    // 3️⃣ 受信者来たようです!送信を受け付けますね!
	ch <- 1
}

func main() {
	ch := make(chan int)

	go sendOnly(ch)

    // 2️⃣ 受信者さん、こんにちは。まだ値が送信されていないので、ここで待っててください。
    // 4️⃣ おっ、値が送信されたようです!お渡ししますね!
	value := <-ch

	fmt.Printf("Received 👉 %d\n", v)
}

ch <- 1へ先に到達したケース

func sendOnly(ch chan<- int) {
    // 2️⃣ 受信者さんがもういらっしゃるので、そのまま送信いたしますね!
	ch <- 1
}

func main() {
	ch := make(chan int)

	go sendOnly(ch)

    // 1️⃣ 受信者さん、こんにちは。まだ値が送信されていないので、ここで待っててください。
    // 3️⃣ おっ、値が送信されたようです!お渡ししますね!
	v := <-ch

	fmt.Printf("Received 👉 %d\n", v)
}

【デッドロック】 このチャネル...ずっと待ってやがる...

送受信を待ってくれるチャネルでしたが、この生真面目な性格が仇となって、永遠に待機状態へ陥るデッドロックが発生する場合があります。

受信者がいないパターン

package main

func main() {
	ch := make(chan int)
    // 受信者が来るまで待とう....(日が暮れる
	ch <- 1
}
出力
fatal error: all goroutines are asleep - deadlock

送信者がいないパターン

package main

import (
	"fmt"
	"sync"
)

func main() {
	ch := make(chan int)

	var wg sync.WaitGroup

	wg.Add(1)
	go func() {
		defer wg.Done()
		// 送信者来るまで待とう....(日が暮れる
		v := <-ch
		fmt.Println(v)
	}()

	wg.Wait()
}
出力
fatal error: all goroutines are asleep - deadlock

ゴルーチン間での相互依存によるデッドロック

この例では、2つのゴルーチンが相互にチャネルから値を受信しようとしていますが、両方のゴルーチンがブロックされ、どちらも送信処理に到達できないため、デッドロックが発生します。

package main

import (
	"sync"
)

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)

	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		// ch1の送信を待とう...(日が暮れる
		v := <-ch1
		ch2 <- v
	}()

	go func() {
		defer wg.Done()
		// ch2の送信を待とう...(日が暮れる
		v := <-ch2
		ch1 <- v
	}()

	wg.Wait()
}
出力
fatal error: all goroutines are asleep - deadlock

【バッファリング】 受信者なんて待てない!送信者なんて待てない!

先ほどまでのチャネルは、ちゃんと受信の準備が整ってから、送信を受け付けていたので、送信→受信の流れが同期的に行われました。そのため、このようなチャネルを「同期チャネル」と呼んだりします。

一方で、チャネルにバッファ(一時的なストレージ)を持たせることで、送信者と受信者のゴルーチンが同期せずに、ある程度の数のデータをチャネルに格納できるようにする(バッファリングと呼びます)ことも可能です。

バッファリングされたチャネルの宣言

バッファリングされたチャネルを作成するには、make関数の第2引数にバッファサイズを指定します。

バッファリングされたチャネルの宣言
ch := make(chan T, 10) // バッファサイズ10のチャネルを作成

バッファの使用例

送信側は、バッファが満杯になるまではブロックされずにデータを送信できます。受信側は、バッファが空になるまではブロックされずにデータを受信できます。

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	ch := make(chan int, 3) // バッファサイズ3のチャネルを作成

	var wg sync.WaitGroup
	wg.Add(2)

	// 送信側のゴルーチン
	go func() {
		defer wg.Done()
		ch <- 1
		ch <- 2
		ch <- 3
		fmt.Println("Sent 3 values")
	}()

	// 受信側のゴルーチン
	go func() {
		defer wg.Done()
		fmt.Println("Received:", <-ch)
		fmt.Println("Received:", <-ch)
		fmt.Println("Received:", <-ch)
	}()

	wg.Wait()
}
出力
Sent 3 values
Received: 1
Received: 2
Received: 3

デッドロックにはご注意を!

バッファリングされたチャネルは、送信側と受信側のゴルーチンが同期しなくても、ある程度のデータをチャネルに格納できるため、パフォーマンスの向上や、ゴルーチン間の負荷分散に役立ちます。ただし、バッファサイズが適切でない場合、デッドロックの問題が発生する可能性があるため、注意が必要です。

バッファサイズ以上の送信をしてしまいデッドロック
package main

import "sync"

func main() {
	ch := make(chan int, 3) // バッファサイズ3のチャネルを作成

	var wg sync.WaitGroup
	wg.Add(1)

	go func() {
		defer wg.Done()
		ch <- 1
		ch <- 2
		ch <- 3
		// バッファが一杯なので、これ以上は受信者が現れるのを待つ!
		ch <- 4
	}()

	wg.Wait()
}
出力
fatal error: all goroutines are asleep - deadlock!
バッファが空になったのに、さらに受信しようとしてデッドロック
package main

import (
	"fmt"
	"sync"
)

func main() {
	ch := make(chan int, 3) // バッファサイズ3のチャネルを作成

	var wg sync.WaitGroup

	wg.Add(1)
	go func() {
		defer wg.Done()
		fmt.Println("Received:", <-ch)
		fmt.Println("Received:", <-ch)
		fmt.Println("Received:", <-ch)
		// もうこれ以上はバッファに値は格納されていないよ...
		fmt.Println("Received:", <-ch)
	}()

	ch <- 1
	ch <- 2
	ch <- 3

	wg.Wait()
}
出力
fatal error: all goroutines are asleep - deadlock!

デフォルトはバッファサイズ0のチャネルが生成される

ところで、バッファサイズを指定せずにチャネルを生成した場合は、同期チャネルと呼びました。実のところ、バッファサイズを指定せずにチャネルを生成した場合、バッファサイズがデフォルトで0になります。つまり、同期チャネルの正体はバッファサイズ0のチャネルだったということです。

ch := make(chan T) // バッファサイズ0

【クローズ】 受信者「いつまで受信し続けたらええんや...」

バッファリングされたチャネルの使用法で例示したコードをもう一度見てみると、プログラマなら誰しもループさせたいなあと思う箇所がありますね。

// 略

func main() {
	ch := make(chan int, 3)

	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		ch <- 1
		ch <- 2
		ch <- 3
		fmt.Println("Sent 3 values")
	}()

	go func() {
		defer wg.Done()
        // ああ、ループさせたい... >👀✨
		fmt.Println("Received:", <-ch)
		fmt.Println("Received:", <-ch)
		fmt.Println("Received:", <-ch)
	}()

	wg.Wait()
}

おめでとうございます!forでぶん回して書けます。

チャネルをforでぶん回す
package main

import (
	"fmt"
	"sync"
)

func main() {
	ch := make(chan int, 3)

    // わかりやすさのため送信はメインルーチンで
	ch <- 1
	ch <- 2
	ch <- 3

	var wg sync.WaitGroup
 
	wg.Add(1)
	go func() {
		defer wg.Done()
		for v := range ch {
			fmt.Println("Received:", v)
		}
	}()

	wg.Wait()
}

これを実行すると

出力
Received: 1
Received: 2
Received: 3
fatal error: all goroutines are asleep - deadlock!

そう、残念なことにデッドロックが発生します。このfor文だと、受信する値がなくなっても止まらずに、ループを回し続けます。そのため、4周名で受信する値はなくなり、結果としてデッドロックが発生してしまうのです。

この問題を解決するために、チャネルにはクローズclose)という機能が備わっています。チャネルのクローズは、送信側が受信側に「自分の仕事はもう全部終わりましたよ!もうこれ以上は何も送信しないからね!」ということを知らせる機能です。

closeの使用例

close機能を使用することでどのように問題が解決されるかを見てみましょう。

package main

import (
	"fmt"
	"sync"
)

func main() {
	ch := make(chan int, 3)


	ch <- 1
	ch <- 2
	ch <- 3
	// 💡チャネルをクローズ
	close(ch)

	var wg sync.WaitGroup

	wg.Add(1)
	go func() {
		defer wg.Done()
		for {
            // ok: チャネルのオープン/クローズ状態を表す真偽値
            // (true→オープン, false→クローズ)
			v, ok := <-ch

            // 4ループ目でokがfalseになるので、受信側はこれでこれ以上値が来ないことを検知する
			if !ok {
				fmt.Printf("チャネルはクローズされました。以降、値が送信されることはあません。\n")
    			fmt.Printf("ちなみにクローズした状態でも値が取得でき、ゼロ値(%d)が取得される。エラーにならない。\n", v)
				return
			}
			fmt.Println("Received:", v)
		}
	}()

	wg.Wait()
}
出力
Received: 1
Received: 1
Received: 2
Received: 3
チャネルはクローズされました。以降、値が送信されることはあません。
ちなみにクローズした状態でも値が取得でき、ゼロ値(0)が取得される。エラーにならない。

このように送信側がチャネルをクローズすることで、受信側に値の送信が完了したことを通知できます。受信側はokの値を確認することで、チャネルがクローズされたことを検出し、適切な処理を行うことができます。

ちなみに、チャネルがクローズされた後も実は受信し続けること自体はできてしまうので要注意です。またこの場合、受信した値には常にゼロ値が割り当てられます。

また、クローズされたチャネルに再度送信を試みるとパニックが発生します。

クローズしたチャネルに送信してパニック
package main

func main() {
	ch := make(chan int, 3)

	ch <- 1
	ch <- 2
	ch <- 3
	close(ch)

	ch <- 4
}
出力
panic: send on closed channel

【select】 チャネルの操作の条件分岐みたいなもの

複数のチャネルを扱う場合、どのチャネルが通信可能な状態になるかわからない状況が頻繁に発生します。そのような場合に活躍するのが、select文です。select文は、複数のチャネル操作の条件分岐のような機能を提供します。

select文の使用法

select文の基本的な構文は以下の通りです。

select {
case <-ch1:
    // ch1から受信した場合の処理
case msg := <-ch2:
    // ch2から受信した場合の処理(msgに受信した値が入る)
case ch3 <- msg:
    // ch3へ送信した場合の処理
default:
    // どの操作も可能でない場合の処理
}

各caseには、チャネルの送信または受信の操作を指定します。受信の場合は、チャネル変数の前に<-を置き、送信の場合は、<-の後にチャネル変数を置きます。

また、defaultケースを指定することで、どのチャネル操作も準備ができていない場合の処理を記述できます。これを使って、チャネル操作がブロックされるのを防ぐことができます。

switch文に似ているが挙動は異なるので注意

先の説明からわかるように、select文の挙動はswitch文とは異なります。switch文と見た目は似ていますが、switch文では各caseを上からチェックしていって最初にtrueになるcaseが選択されます。一方で、select文ではデータの準備ができているcaseのうちからランダムに選択します。

で、具体的にどんなときにselect文を使うんだい?

select文の使い方を説明しましたが、だからといってどういう場面で使うのかイメージしにくいと思うので、いくつかのユースケースを紹介します。

1️⃣ 複数のチャネルからの受信を待機する

こちら

まずは次のサンプルコードを確認してみましょう。

package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan string)
	ch2 := make(chan string)

	go func() {
		time.Sleep(2 * time.Second)
		ch1 <- "ch1からこんにちは"
	}()

	go func() {
		time.Sleep(1 * time.Second)
		ch2 <- "ch2からこんにちは"
	}()

	v1 := <-ch1
	fmt.Println(v1)

	v2 := <-ch2
	fmt.Println(v2)
}
出力
ch1からこんにちは
ch2からこんにちは

このコードでは、ch1ch2からのデータ受信を順番に待っています。しかし、この方法では、ch1からのデータ受信が完了するまでch2からのデータ受信が行われません。つまり、ch1からのデータ受信に時間がかかる場合、ch2からの受信がそれだけ待たされてしまいます。

ここで、select文を利用することで、複数のチャネルからのデータ受信を同時に待ち受け、どちらかのチャネルからデータを受信した時点で処理を進めることができます。

package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan string)
	ch2 := make(chan string)

	go func() {
		time.Sleep(2 * time.Second)
		ch1 <- "ch1からこんにちは"
	}()

	go func() {
		time.Sleep(1 * time.Second)
		ch2 <- "ch2からこんにちは"
	}()

	for i := 0; i < 2; i++ {
		select {
		case v1 := <-ch1:
			fmt.Println(v1)
		case v2 := <-ch2:
			fmt.Println(v2)
		}
	}
}
出力
ch2からこんにちは
ch1からこんにちは

select文を使用することで、ch1ch2の両方からのデータ受信を同時に待ち受けます。どちらかのチャネルからデータを受信すると、対応するcase文が実行され、受信したデータの処理を行うことができます。

このように、select文を使用することで、複数のチャネルからのデータ受信を効率的に行い、受信したデータをすぐに処理することができます。これにより、複数のゴルーチンから送信されるデータを同時に処理でき、全体的な処理のパフォーマンスを向上させることができます。

2️⃣ タイムアウト付きのチャネル操作

こちら

チャネル操作は、送信または受信の相手が準備できるまでブロックされます。しかし、時には、一定時間待ってもチャネル操作が完了しない場合に、別の処理を実行したいことがあります。そのような場合、select文とtime.Afterを組み合わせることで、タイムアウト付きのチャネル操作を実現できます。

以下のコードは、チャネルからの受信操作にタイムアウトを設定する例です。

func main() {
    ch := make(chan string)

    go func() {
        time.Sleep(2 * time.Second)
        ch <- "data from channel"
    }()

    select {
    case msg := <-ch:
        fmt.Println(msg)
    case <-time.After(1 * time.Second):
        fmt.Println("timeout")
    }
}

select文で、

  1. chからの受信
  2. time.After(1 * time.Second)

の2つのケースを待ちます。chからの受信が先に完了した場合、受信した値を表示します。1秒経過した場合、time.Afterのケースが選択され、"timeout"と表示します。このコードを実行すると、"timeout"が表示されます。これは、チャネルへの送信が2秒後に行われるのに対し、select文では1秒のタイムアウトが設定されているためです。タイムアウト時間を2秒以上に変更すれば、チャネルからの受信が完了し、"data from channel"が表示されるようになります。

このように、select文とtime.Afterを組み合わせることで、チャネル操作にタイムアウトを設定し、一定時間経過後に別の処理を実行することができます。これは、ネットワーク通信やリソースの獲得など、時間がかかる可能性のある操作を扱う際に特に有用です。

3️⃣ デフォルトケースを使った非ブロッキング受信

こちら

チャネル操作は、送信または受信の相手が準備できるまでブロックされます。しかし、状況によっては、チャネル操作がブロックされると困る場合があります。そのような場合、select文のデフォルトケースを使って、非ブロッキングなチャネル操作を実現できます。

以下のコードは、チャネルからの受信操作を非ブロッキングで行う例です。

func main() {
    ch := make(chan string)

    select {
    case msg := <-ch:
        fmt.Println(msg)
    default:
        fmt.Println("データを受信できませんでした")
    }
}

このコードを実行すると、「データを受信できませんでした」と表示されます。これは、デフォルトケースが存在するため、受信操作がブロックされずに、すぐにデフォルトケースの処理が実行されるからです。

もし、デフォルトケースが存在しない場合、プログラムはchからの受信操作で永遠にブロックされ、デッドロックが発生します。

このように、select文のデフォルトケースを使うことで、チャネル操作がブロックされる可能性がある場合に、非ブロッキングな処理を実現できます。これは、チャネルの状態を確認したり、複数のチャネルを扱う際に、一部のチャネルがブロックされても他の処理を続行したい場合などに役立ちます。

ただし、デフォルトケースを使いすぎると、チャネル操作の本来の目的である同期や通信が適切に行われなくなる可能性があるので、注意が必要です。

4️⃣ デフォルトケースを使った非ブロッキング送信

こちら

チャネルへの送信操作は、バッファが満杯の場合にブロックされましたよね。そのような場合、select文のデフォルトケースを使って、非ブロッキングな送信操作を実現できます。

以下のコードは、チャネルへの送信操作を非ブロッキングで行う例です。

func main() {
    ch := make(chan string, 1)

    select {
    case ch <- "データ":
        fmt.Println("データの送信に成功しました")
    default:
        fmt.Println("チャネルは満杯です")
    }
}

このコードを実行すると、「データの送信に成功しました」と表示されます。これは、chが空のチャネルであり、送信操作が成功するためです。

ただし、以下のように、事前にチャネルに値を送信しておくと、実行結果が変わります。

func main() {
    ch := make(chan string, 1)
    ch <- "先にデータ送っておくデータ"

    select {
    case ch <- "データ":
        fmt.Println("データの送信に成功しました")
    default:
        fmt.Println("チャネルは満杯です")
    }
}

この場合、チャネルchはすでに満杯であるため、送信操作がブロックされます。ただし、デフォルトケースが存在するため、送信操作がブロックされずに、すぐにデフォルトケースの処理が実行され、「チャネルは満杯です」と表示されます。

このように、チャネルへの送信操作がブロックされる可能性がある場合に、非ブロッキングな処理を実現できます。これは、チャネルの容量を超えるデータを送信しようとしたときに、ブロックを避けたい場合などに役立ちます。

ただし、デフォルトケースを使いすぎると、チャネルの容量を適切に管理できなくなる可能性があるので、注意が必要です。

さいごに

並行処理の世界は、まだまだ奥深い(と思います。私自身学び始めたばかりでまだ奥深さがわかっていない)です。ただ、ここに書かれた基本を身につければ、きっとその深遠な世界も攻略でき(ると信じてい)ます。私のように、並行処理に馴染みのなかった方々の一助となれば幸いです。

7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6