Menu
Coddy logo textTech

バッファあり Channel とバッファなし Channel

CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 61/107。

これまで使用してきたチャネルはバッファなしです。これらは値を保持する容量を持ちません。送信操作は、別のゴルーチンが受信するまでブロックされ、その逆も同様です。バッファ付きチャネルは内部ストレージを追加し、即座の受信者がいなくても送信を完了できるようにします。

make にキャパシティを渡すことで、バッファ付きチャネルを作成します:

// バッファなし - 容量 0
unbuffered := make(chan int)

// バッファあり - 容量 3
buffered := make(chan int, 3)

バッファ付きチャネルでは、バッファが満杯のときのみ送信がブロックされ、バッファが空のときのみ受信がブロックされます:

ch := make(chan string, 2)

ch <- "first"   // ブロックされない - バッファに空きがある
ch <- "second"  // ブロックされない - バッファに空きがある
// ch <- "third" はブロックされる - バッファが満杯

fmt.Println(<-ch)  // "first"
fmt.Println(<-ch)  // "second"

len(ch) を使用して現在バッファ内にあるアイテム数を確認し、cap(ch) を使用して合計容量を確認します。

それぞれの使い分け:バッファなしチャネルは厳密な同期を提供します。送信側は受信側が値を受け取ったことを知ることができます。バッファありチャネルは送信側と受信側のタイミングを切り離すため、プロデューサーとコンシューマーが異なる速度で動作する場合に便利です。ただし、バッファありチャネルは同期のバグを隠してしまう可能性があるため、バッファリングが必要な特定の理由がない限り、バッファなしチャネルを優先してください。

challenge icon

チャレンジ

簡単

バッファ付きチャネルとバッファなしチャネルの違いを実証するバッチプロセッサを構築しましょう。プロデューサーがプロセッサにアイテムを送信するシステムを作成します。バッファ付きチャネルを使用してタイミングを切り離し、プロデューサーが先行して作業できるようにします。

コードは2つのファイルに分けて構成します:

  • processor.go: チャネルを使用したバッチ処理ロジックを定義します。

    ID (int) と Value (string) フィールドを持つ Item 構造体を作成します。

    2つの関数を実装します:

    • Producer(items []Item, out chan Item) - 各アイテムを出力チャネルに送信します。各アイテムを送信した後、Produced item [ID] と出力します。すべてのアイテムが送信された後、len(out) を使用してバッファ内で待機している現在のアイテム数を Buffer has [count] items という形式で出力します。その後、チャネルを閉じます。
    • Consumer(in chan Item) []string - 入力チャネルからアイテムを受信し、フォーマットされた文字列のスライスに収集します。受信した各アイテムについて、文字列は Consumed: [ID] - [Value] とします。チャネルが閉じられたら、そのスライスを返します。
  • main.go: バッファ付きチャネルをセットアップし、プロデューサーとコンシューマーを調整します。

    バッファ容量、次にアイテム数、続いて各アイテムのIDと値を読み取ります。指定された容量でバッファ付きチャネルを作成します。まずプロデューサーを実行して(ゴルーチンとしてではなく)バッファを満たし、次にコンシューマーを実行してすべてのアイテムを処理します。消費された各結果を個別の行に出力します。

以下の入力が提供されます:

  • 1行目:バッファ容量(整数)
  • 2行目:アイテム数(整数)
  • 続く行:各アイテムについて2行 - アイテムID(整数)、その値(文字列)

例えば、以下のように与えられた場合:

3
3
1
apple
2
banana
3
cherry

出力は以下のようになります:

Produced item 1
Produced item 2
Produced item 3
Buffer has 3 items
Consumed: 1 - apple
Consumed: 2 - banana
Consumed: 3 - cherry

バッファ容量が3の場合、プロデューサーはブロックすることなく3つのアイテムすべてを送信でき、コンシューマーが開始する前にバッファに3つのアイテムが待機していることが示されている点に注目してください。もしバッファがアイテム数よりも小さければ、プロデューサーは空きを待ってブロックすることになります。

チートシート

Goのチャネルは、バッファなし(容量0)またはバッファあり(内部ストレージあり)にすることができます。

makeを使用してチャネルを作成します:

// バッファなし - 容量 0
unbuffered := make(chan int)

// バッファあり - 容量 3
buffered := make(chan int, 3)

バッファありチャネルの動作:

  • 送信はバッファがいっぱいの場合にのみブロックされます
  • 受信はバッファが空の場合にのみブロックされます
ch := make(chan string, 2)

ch <- "first"   // ブロックされない - バッファに空きがある
ch <- "second"  // ブロックされない - バッファに空きがある
// ch <- "third" はブロックされる - バッファがいっぱい

fmt.Println(<-ch)  // "first"
fmt.Println(<-ch)  // "second"

バッファの状態を確認する:

  • len(ch) - 現在バッファ内にあるアイテムの数
  • cap(ch) - バッファの総容量

いつ使用するか:

  • バッファなし: 厳密な同期 — 送信者は受信者が値を受け取ったことを認識します
  • バッファあり: 送信者と受信者が異なる速度で動作する場合に、タイミングを切り離します(デカップリング)

自分で試してみよう

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)

	// バッファの容量を読み込む
	line, _ := reader.ReadString('\n')
	bufferCapacity, _ := strconv.Atoi(strings.TrimSpace(line))

	// アイテムの数を読み込む
	line, _ = reader.ReadString('\n')
	numItems, _ := strconv.Atoi(strings.TrimSpace(line))

	// 各アイテムのIDと値を読み込む
	items := make([]Item, numItems)
	for i := 0; i < numItems; i++ {
		line, _ = reader.ReadString('\n')
		id, _ := strconv.Atoi(strings.TrimSpace(line))
		line, _ = reader.ReadString('\n')
		value := strings.TrimSpace(line)
		items[i] = Item{ID: id, Value: value}
	}

	// TODO: 指定された容量でバッファ付きチャネルを作成する

	// TODO: プロデューサーを(ゴルーチンとしてではなく)実行してバッファを満たす

	// TODO: コンシューマーを実行してすべてのアイテムを処理する

	// TODO: 消費された各結果を個別の行に出力する
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン