Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 18930] ファイルの概要

このコミットでは、Go言語の公式ドキュメントである doc/effective_go.htmldoc/go_mem.html の2つのファイルが変更されています。

  • doc/effective_go.html: Go言語を効果的に記述するためのプラクティスとイディオムを解説するドキュメントです。このコミットでは、バッファ付きチャネルをセマフォとして使用する際の誤ったイディオムが修正され、より正確な使用方法が示されています。具体的には、チャネルの初期化(プリフィル)が不要であること、およびセマフォの取得と解放の操作が修正されています。
  • doc/go_mem.html: Go言語のメモリモデルについて解説するドキュメントです。このコミットでは、バッファ付きチャネルにおけるセンドとレシーブの「happens before」関係に関する新しいルールが追加され、バッファ付きチャネルがセマフォとして機能する根拠が明確化されています。

コミット

commit 132e816734de8cb7d5c52ca3a5a707135fc81075
Author: Russ Cox <rsc@golang.org>
Date:   Mon Mar 24 19:11:21 2014 -0400

    doc: allow buffered channel as semaphore without initialization
    
    This rule not existing has been the source of many discussions
    on golang-dev and on issues. We have stated publicly that it is
    true, but we have never written it down. Write it down.
    
    Fixes #6242.
    
    LGTM=r, dan.kortschak, iant, dvyukov
    R=golang-codereviews, r, dominik.honnef, dvyukov, dan.kortschak, iant, 0xjnml
    CC=golang-codereviews
    https://golang.org/cl/75130045

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/132e816734de8cb7d5c52ca3a5a707135fc81075

元コミット内容

doc: allow buffered channel as semaphore without initialization

This rule not existing has been the source of many discussions
on golang-dev and on issues. We have stated publicly that it is
true, but we have never written it down. Write it down.

Fixes #6242.

LGTM=r, dan.kortschak, iant, dvyukov
R=golang-codereviews, r, dominik.honnef, dvyukov, dan.kortschak, iant, 0xjnml
CC=golang-codereviews
https://golang.org/cl/75130045

変更の背景

このコミットの背景には、Go言語のコミュニティ、特に golang-dev メーリングリストやGitHubのissueトラッカーで、バッファ付きチャネルをセマフォとして使用する際の「初期化(プリフィル)の必要性」に関する多くの議論があったことが挙げられます。

以前は、バッファ付きチャネルをセマフォとして使う場合、その容量分だけ初期値をチャネルに送信しておく(プリフィルする)のが一般的なイディオムとされていました。しかし、実際にはGoのチャネルの動作原理上、この初期化は不要であり、むしろ誤解を招く可能性がありました。Goチームは、この「初期化なしでバッファ付きチャネルをセマフォとして使用できる」というルールが事実であることを公に表明していましたが、公式ドキュメントには明記されていませんでした。

このコミットは、この重要な事実を公式ドキュメントに明文化し、混乱を解消することを目的としています。特に、effective_go.html のセマフォの例を修正し、go_mem.html に新しいメモリモデルのルールを追加することで、このイディオムの正当性を技術的に裏付けています。これにより、開発者がより正確かつ効率的にGoの並行処理パターンを理解し、利用できるようになります。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念と並行処理の基礎知識が必要です。

  1. Goのチャネル (Channels):

    • Goにおけるゴルーチン間の通信と同期のための主要なプリミティブです。
    • チャネルは型付けされており、特定の型の値を送受信できます。
    • make(chan Type) で作成され、<-chan で受信、chan<- で送信します。
    • アンバッファードチャネル (Unbuffered Channels): 容量が0のチャネルです。送信操作は受信操作が完了するまでブロックし、受信操作は送信操作が完了するまでブロックします。これにより、送信側と受信側が同時に準備ができたときにのみ通信が行われる「同期」の役割を果たします。
    • バッファードチャネル (Buffered Channels): make(chan Type, capacity) のように容量を指定して作成します。チャネルのバッファに指定された容量までの値を格納できます。
      • バッファが満杯でない限り、送信操作はブロックしません。
      • バッファが空でない限り、受信操作はブロックしません。
      • バッファが満杯の場合、送信操作はブロックします。
      • バッファが空の場合、受信操作はブロックします。
  2. セマフォ (Semaphore):

    • 並行プログラミングにおける同期メカニズムの一つです。
    • 複数のプロセスやスレッドが共有リソースにアクセスする数を制限するために使用されます。
    • 通常、acquire (P操作、wait) と release (V操作、signal) の2つの操作を持ちます。
    • acquire 操作は、利用可能なリソースがある場合にのみ続行し、リソースを消費します。利用可能なリソースがない場合はブロックします。
    • release 操作は、リソースを解放し、待機しているプロセスやスレッドがあればそれを再開させます。
    • カウンティングセマフォは、同時にアクセスできるリソースの数を数えるセマフォです。
  3. Goメモリモデル (Go Memory Model) と Happens Before:

    • Goメモリモデルは、Goプログラムにおけるメモリ操作の順序付けを定義するものです。これにより、複数のゴルーチンが共有メモリにアクセスする際の挙動が予測可能になります。
    • 「Happens Before」関係は、Goメモリモデルの核心となる概念です。これは、あるイベントが別のイベントの前に発生することを保証する順序付けのルールです。
    • もしイベント A がイベント B の前に発生するならば、A の効果は B から観測可能です。
    • チャネル操作は、この「Happens Before」関係を確立する主要な手段の一つです。
      • アンバッファードチャネルの場合、送信操作が完了する前に、対応する受信操作が開始されます。
      • バッファードチャネルの場合、k 番目の送信が完了する前に、k 番目の受信が開始されます。

これらの概念を理解することで、バッファ付きチャネルがどのようにしてセマフォとして機能し、なぜ初期化が不要であるのか、そしてメモリモデルの新しいルールがその正当性をどのように保証するのかが明確になります。

技術的詳細

このコミットの技術的な核心は、バッファ付きチャネルがその容量を上限とするカウンティングセマフォとして機能するというGo言語の特性を明確にし、そのための誤解を招く初期化が不要であることを示す点にあります。

従来のセマフォのイディオムでは、MaxOutstanding の容量を持つチャネル sem を作成した後、init() 関数などで for i := 0; i < MaxOutstanding; i++ { sem <- 1 } のようにチャネルをプリフィル(初期化)していました。そして、セマフォの取得には <-sem を、解放には sem <- 1 を使用していました。これは、チャネルに「トークン」を事前に置いておき、それを取り出すことでリソースを取得し、戻すことでリソースを解放するという考え方に基づいています。

しかし、Goのバッファ付きチャネルの動作を深く理解すると、このプリフィルは不要であることがわかります。

  • セマフォの取得 (Acquire):

    • 新しいイディオムでは、セマフォの取得に sem <- 1 を使用します。
    • チャネル sem のバッファが満杯(つまり、MaxOutstanding 個のゴルーチンが既に process 関数を実行中)の場合、この送信操作はブロックします。
    • これにより、同時に実行される process 関数の数が MaxOutstanding に制限されます。
    • この動作は、セマフォの acquire 操作と全く同じです。リソースが利用可能になるまで待機し、利用可能になったらそれを消費します。
  • セマフォの解放 (Release):

    • 新しいイディオムでは、セマフォの解放に <-sem を使用します。
    • process 関数が完了した後、チャネルから値を受信することで、バッファから「トークン」が一つ取り除かれ、チャネルに空きができます。
    • これにより、ブロックしていた他の sem <- 1 操作が再開できるようになり、次のゴルーチンが process 関数を実行できるようになります。
    • この動作は、セマフォの release 操作と全く同じです。リソースを解放し、待機している他の操作を再開させます。

この変更の正当性を裏付けるために、doc/go_mem.html に新しいメモリモデルのルールが追加されました。

The kth send on a channel with capacity C happens before the k+Cth receive from that channel completes.

このルールは、バッファ付きチャネルにおけるセンドとレシーブの順序関係をより厳密に定義しています。具体的には、容量 C のチャネルにおいて、k 番目の送信が完了する前に、k+C 番目の受信が完了するというものです。これは、チャネルのバッファが満杯になったときに送信がブロックし、受信によってバッファに空きができることで送信が再開されるという動作を形式的に保証します。このルールがあることで、バッファ付きチャネルをセマフォとして使用する際の「happens before」関係が明確になり、競合状態(race condition)の発生を防ぎ、プログラムの正しさを保証します。

要するに、このコミットは、Goのチャネルの設計が元々持っていたセマフォとしての能力を再認識し、その最もシンプルで効率的な使用方法を公式ドキュメントに反映させたものです。これにより、開発者は不要な初期化コードを記述することなく、より直感的にバッファ付きチャネルを並行処理の制御に活用できるようになります。

コアとなるコードの変更箇所

このコミットにおけるコアとなるコードの変更箇所は、主に doc/effective_go.htmldoc/go_mem.html の2つのファイルにわたります。

doc/effective_go.html の変更

このファイルでは、バッファ付きチャネルをセマフォとして使用する例が修正されています。

  1. セマフォの初期化部分の削除: 以前は init() 関数内でチャネルをプリフィルするコードがありました。

    func init() {
        for i := 0; i < MaxOutstanding; i++ {
            sem <- 1
        }
    }
    

    この部分が完全に削除されました。

  2. handle 関数のセマフォ操作の修正: セマフォの取得と解放の順序が逆転しています。 変更前:

    func handle(r *Request) {
        <-sem          // Wait for active queue to drain. (取得)
        process(r)     // May take a long time.
        sem <- 1       // Done; enable next request to run. (解放)
    }
    

    変更後:

    func handle(r *Request) {
        sem <- 1    // Wait for active queue to drain. (取得)
        process(r)  // May take a long time.
        <-sem       // Done; enable next request to run. (解放)
    }
    

    これにより、sem <- 1 がチャネルが満杯であればブロックする「取得」操作となり、<-sem がチャネルから値を取り出して空きを作る「解放」操作となります。

  3. Serve 関数のセマフォ操作の修正: Serve 関数内のゴルーチン起動部分でも同様の修正が行われています。 変更前:

    // ...
    for req := range queue {
        <-sem // 取得
        go func() {
            process(req)
            sem <- 1 // 解放
        }()
    }
    // ...
    

    変更後:

    // ...
    for req := range queue {
        sem <- 1 // 取得
        go func() {
            process(req)
            <-sem // 解放
        }()
    }
    // ...
    

    これは、Serve 関数内の他の2つの例(クロージャの引数渡し、シャドーイングによる req のコピー)でも同様に適用されています。

doc/go_mem.html の変更

このファイルでは、Goメモリモデルに新しいルールが追加されています。

  1. 新しいルールの追加: バッファ付きチャネルにおける「happens before」関係を定義する以下のルールが追加されました。

    <p class="rule">
    The <i>k</i>th send on a channel with capacity <i>C</i> happens before the <i>k</i>+<i>C</i>th receive from that channel completes.
    </p>
    

    このルールは、容量 C のチャネルにおいて、k 番目の送信が完了する前に、k+C 番目の受信が完了することを保証します。

  2. ルールの説明とセマフォへの適用: 追加されたルールの直後に、このルールがどのようにバッファ付きチャネルをカウンティングセマフォとしてモデル化できるかを説明する段落が追加されています。

    <p>
    This rule generalizes the previous rule to buffered channels.
    It allows a counting semaphore to be modeled by a buffered channel:
    the number of items in the channel corresponds to the semaphore count,
    the capacity of the channel corresponds to the semaphore maximum,
    sending an item acquires the semaphore, and receiving an item releases
    the semaphore.
    This is a common idiom for rate-limiting work.
    </p>
    
  3. セマフォの例の追加: 新しいルールを具体的に示すために、limit というバッファ付きチャネル(容量3)を使った並行処理のレート制限の例が追加されています。

    var limit = make(chan int, 3)
    
    func main() {
        for _, w := range work {
            go func() {
                limit <- 1 // Acquire
                w()
                <-limit    // Release
            }()
        }
        select{}
    }
    

これらの変更は、Goの並行処理におけるバッファ付きチャネルのセマフォとしての役割をより正確に、かつ効率的に記述するための重要なドキュメントの更新です。

コアとなるコードの解説

このコミットのコアとなるコードの変更は、Goの並行処理におけるバッファ付きチャネルのセマフォとしての利用方法に関する、長年の誤解を解消し、より正確で効率的なイディオムを確立するものです。

doc/effective_go.html の変更の解説

以前の effective_go.html のセマフォの例では、MaxOutstanding の容量を持つチャネル sem を作成した後、init() 関数で MaxOutstanding 個のダミー値をチャネルに送信して「プリフィル」していました。そして、セマフォの取得には <-sem (チャネルからの受信) を、解放には sem <- 1 (チャネルへの送信) を使用していました。

このアプローチは、チャネルに「利用可能なスロット」を表すトークンを事前に置いておき、それを取り出すことでスロットを消費し、処理が終わったらスロットを戻す、という直感的な考え方に基づいています。しかし、これはGoのバッファ付きチャネルの本来の動作を完全に活用しているわけではありませんでした。

新しいイディオムでは、init() によるプリフィルが完全に削除されました。そして、セマフォの取得と解放の操作が逆転しました。

  • 取得操作: sem <- 1

    • バッファ付きチャネルへの送信操作は、チャネルに空きがある限りブロックしません。
    • しかし、チャネルのバッファが満杯(つまり、既に MaxOutstanding 個の要素がチャネル内に存在し、それらがまだ受信されていない状態)の場合、この送信操作はブロックします。
    • この「満杯であればブロックする」という性質が、まさにセマフォの acquire (P) 操作に相当します。同時に実行できるゴルーチンの数をチャネルの容量に制限する役割を果たします。
  • 解放操作: <-sem

    • バッファ付きチャネルからの受信操作は、チャネルが空でない限りブロックしません。
    • この受信操作により、チャネルから要素が一つ取り除かれ、バッファに空きができます。
    • これにより、sem <- 1 でブロックしていた他のゴルーチンが再開できるようになります。
    • この「空きを作る」という性質が、セマフォの release (V) 操作に相当します。

この変更により、コードはより簡潔になり、チャネルの容量が直接的に並行処理の制限数として機能するという、Goのチャネルの設計思想に合致した形になりました。

doc/go_mem.html の変更の解説

go_mem.html に追加された新しいメモリモデルのルールは、この新しいセマフォのイディオムの正当性を形式的に保証するものです。

The kth send on a channel with capacity C happens before the k+Cth receive from that channel completes.

このルールは、バッファ付きチャネルにおける送信と受信の間の「happens before」関係を明確にします。具体的には、チャネルの容量 C を考慮に入れた上で、k 番目の送信が完了した後に、k+C 番目の受信が完了するという順序が保証されます。

このルールが重要なのは、並行処理におけるデータ競合(data race)を防ぎ、プログラムの予測可能性を保証するためです。例えば、セマフォとして使用する場合、sem <- 1 でリソースを取得し、process(r) を実行し、<-sem でリソースを解放するという一連の操作が、他のゴルーチンとの間で正しく同期されることを、このメモリモデルのルールが保証します。

新しい例として追加された limit チャネルを使ったレート制限のコードは、この新しいイディオムとメモリモデルのルールを具体的に示しています。limit <- 1 でゴルーチンが実行スロットを取得し、w() で実際の処理を行い、<-limit でスロットを解放するという流れは、まさにバッファ付きチャネルがカウンティングセマフォとして機能する典型的なパターンです。

総じて、このコミットは、Goのチャネルが持つ強力な同期プリミティブとしての能力を再確認し、その最も自然で効率的な利用方法を公式ドキュメントに反映させることで、Go開発者がより堅牢で理解しやすい並行プログラムを記述できるよう支援するものです。

関連リンク

参考にした情報源リンク

  • doc/effective_go.html (Go言語の公式ドキュメント: Effective Go)
  • doc/go_mem.html (Go言語の公式ドキュメント: The Go Memory Model)
  • Go言語のチャネルに関する公式ドキュメントやチュートリアル
  • 並行処理におけるセマフォの概念に関する一般的な情報源
  • Go言語のメモリモデルと「happens before」関係に関する一般的な情報源
  • golang-dev メーリングリストの議論 (具体的なリンクはコミットメッセージにはないが、背景情報として言及されている)
  • Go言語のGitHub Issues (特に #6242)