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

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

このコミットは、Go言語のメモリモデルに関する公式ドキュメント doc/go_mem.html の記述を修正し、データ競合(data race)に関する誤解を招く可能性のある表現を排除することを目的としています。特に、データ競合が発生した場合のプログラムの挙動について、以前のドキュメントが示唆していた「クラッシュしない」といった保証が誤りであることを明確にしています。

コミット

doc: Don't imply incorrect guarantees about data races.

A race between
        a = "hello, world"
and
        print(a)
is not guaranteed to print either "hello, world" or "".
Its behaviour is undefined.

Fixes #4039.

R=rsc
CC=dvyukov, gobot, golang-dev, r
https://golang.org/cl/6489075

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

https://github.com/golang/go/commit/aecf5033dfc972f9989b6681ab0f00e346c60e60

元コミット内容

commit aecf5033dfc972f9989b6681ab0f00e346c60e60
Author: David Symonds <dsymonds@golang.org>
Date:   Tue Sep 11 08:47:30 2012 +1000

    doc: Don't imply incorrect guarantees about data races.
    
    A race between
            a = "hello, world"
    and
            print(a)
    is not guaranteed to print either "hello, world" or "".
    Its behaviour is undefined.
    
    Fixes #4039.
    
    R=rsc
    CC=dvyukov, gobot, golang-dev, r
    https://golang.org/cl/6489075
---\n doc/go_mem.html | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)\n

変更の背景

Go言語の並行処理モデルは、ゴルーチン(goroutine)とチャネル(channel)を核としており、これらを適切に使用することで安全な並行プログラムを記述できます。しかし、並行処理においては「データ競合(data race)」という問題が発生する可能性があります。データ競合とは、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって順序付けされていない場合に発生する状況を指します。

このコミット以前のGoのメモリモデルに関するドキュメント doc/go_mem.html には、データ競合が発生した場合のプログラムの挙動について、誤解を招く可能性のある記述がありました。具体的には、バッファなしチャネルの例でデータ競合が発生した場合に「クラッシュすることはない」というような保証を示唆する表現が含まれていました。

しかし、Go言語のメモリモデルの厳密な定義によれば、データ競合が発生した場合のプログラムの挙動は「未定義(undefined behavior)」です。未定義の挙動とは、コンパイラや実行環境がどのような動作をしても許される状態を意味し、クラッシュする可能性もあれば、予期せぬ結果を出力する可能性も、あるいはたまたま正しく動作するように見える可能性もあります。特定の挙動が保証されるわけではありません。

このコミットは、このドキュメントの記述がGoのメモリモデルの厳密な定義と矛盾している点を修正し、データ競合が未定義の挙動を引き起こすことを明確に伝えるために行われました。これにより、開発者がデータ競合の危険性を正しく理解し、適切な同期メカニズムを用いて競合を回避することの重要性が強調されます。

前提知識の解説

データ競合 (Data Race)

データ競合は、並行プログラミングにおける最も一般的なバグの一つであり、プログラムの予測不可能な挙動やクラッシュの原因となります。Go言語の文脈では、以下の3つの条件がすべて満たされた場合にデータ競合が発生します。

  1. 複数のゴルーチンが同じメモリ位置にアクセスする: 2つ以上のゴルーチンが、同じ変数やデータ構造を読み書きしようとします。
  2. 少なくとも1つのアクセスが書き込みである: アクセスのうち少なくとも1つが、メモリの内容を変更する操作(書き込み)です。もし両方とも読み込みであれば、データ競合は発生しません(ただし、読み込みが古いデータを見る可能性はあります)。
  3. アクセスが同期メカニズムによって順序付けされていない: ゴルーチン間のアクセス順序が、ミューテックス(sync.Mutex)、チャネル(chan)、sync.WaitGroupなどのGoの同期プリミティブによって保証されていない場合です。

データ競合が発生すると、プログラムの挙動は未定義となります。これは、コンパイラ最適化、CPUのキャッシュ、メモリの再順序付けなど、様々な要因によって、アクセス順序が予測不能になるためです。

Goのメモリモデル (Go Memory Model)

Go言語には、並行プログラムにおけるメモリ操作の可視性(visibility)と順序付け(ordering)を定義する厳格なメモリモデルがあります。このモデルは、プログラマが並行処理の挙動を予測し、データ競合を回避するためのルールを提供します。

Goのメモリモデルの基本的な原則は、「happens-before」関係に基づいています。これは、あるイベントが別のイベントの前に発生することが保証される関係を定義します。もし2つのメモリ操作がhappens-before関係にない場合、それらの順序は保証されず、データ競合が発生する可能性があります。

Goのメモリモデルが保証するhappens-before関係の例:

  • ゴルーチンの起動: go ステートメントは、新しいゴルーチンが実行を開始する前に完了します。
  • チャネル通信:
    • チャネルへの送信は、そのチャネルからの受信が完了する前にhappens-beforeします。
    • バッファ付きチャネルの場合、cap(ch) 個の要素がチャネルに送信されるまでは、送信は受信と同期しません。
  • Mutexのロック/アンロック: sync.Mutex のアンロックは、同じミューテックスの次のロックの前にhappens-beforeします。
  • sync.Once: Do メソッド内の関数呼び出しは、Do が返る前にhappens-beforeします。
  • sync.WaitGroup: Add の呼び出しは、Wait が返る前にhappens-beforeします。

これらの同期プリミティブを適切に使用することで、プログラマはメモリ操作の順序を制御し、データ競合を回避することができます。

未定義の挙動 (Undefined Behavior)

プログラミング言語において「未定義の挙動」とは、特定の操作や状況が発生した場合に、言語仕様がそのプログラムの挙動を規定しないことを意味します。これは、コンパイラや実行環境がその状況に対してどのような動作をしても許されるということを意味します。

未定義の挙動の例:

  • C/C++におけるヌルポインタのデリファレンス
  • 配列の範囲外アクセス
  • データ競合

未定義の挙動が発生すると、以下のような予測不能な結果が生じる可能性があります。

  • プログラムのクラッシュ
  • 誤った結果の出力
  • セキュリティ脆弱性
  • 異なる環境(OS、CPUアーキテクチャ、コンパイラのバージョンなど)で異なる挙動を示す
  • デバッグが非常に困難になる

Go言語では、データ競合が未定義の挙動を引き起こすことが明確に規定されています。これは、Goが安全な並行処理を奨励しているにもかかわらず、データ競合を完全に防ぐことはプログラマの責任であることを示しています。

技術的詳細

このコミットが修正しているのは、Goのメモリモデルに関するドキュメント doc/go_mem.html の特定の記述です。問題となっていたのは、チャネルの例でデータ競合が発生した場合の挙動に関する説明でした。

元のドキュメントでは、以下のようなコードスニペットを例に挙げていました(コミットメッセージの例とは異なりますが、文脈は同じです)。

var a string
func f() {
    a = "hello, world"
}
func main() {
    go f()
    print(a)
}

このコードでは、f() ゴルーチンが変数 a に書き込み、main ゴルーチンが a を読み込もうとしています。これら2つの操作の間には同期メカニズムがないため、データ競合が発生します。

元のドキュメントでは、バッファ付きチャネルの例(c = make(chan int, 1))において、データ競合が発生した場合に「"hello, world" を出力することは保証されない。(空文字列を出力するかもしれないが、"goodbye, universe" を出力したり、クラッシュしたりすることはない)」というような記述がありました。

この記述は、データ競合が未定義の挙動を引き起こすというGoのメモリモデルの厳密な定義と矛盾していました。データ競合が発生した場合、プログラムはクラッシュする可能性もあれば、全く関係のない値を出力する可能性も、あるいはセキュリティ上の脆弱性を引き起こす可能性さえあります。特定の挙動(例えば「クラッシュしない」)が保証されることはありません。

コミットメッセージで示されている例: a = "hello, world"print(a) の間の競合は、"hello, world" または "" のどちらかを出力することを保証しません。その挙動は未定義です。

このコミットは、この誤解を招く記述を修正し、データ競合が未定義の挙動を引き起こすことを明確にすることで、Goのメモリモデルの正確な理解を促進します。これにより、開発者はデータ競合を単なる「予測不能な出力」ではなく、「深刻なバグ」として認識し、積極的に回避するよう促されます。

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

--- a/doc/go_mem.html
+++ b/doc/go_mem.html
@@ -270,8 +270,8 @@ before the <code>print</code>.
 <p>
 If the channel were buffered (e.g., <code>c = make(chan int, 1)</code>)
 then the program would not be guaranteed to print
-<code>"hello, world"</code>.  (It might print the empty string;\
-it cannot print <code>"goodbye, universe"</code>, nor can it crash.)
+<code>"hello, world"</code>.  (It might print the empty string,
+crash, or do something else.)
 </p>
 
 <h3>Locks</h3>

コアとなるコードの解説

変更は doc/go_mem.html ファイルの270行目付近にあります。

元のコード:

If the channel were buffered (e.g., <code>c = make(chan int, 1)</code>)
then the program would not be guaranteed to print
<code>"hello, world"</code>.  (It might print the empty string;
it cannot print <code>"goodbye, universe"</code>, nor can it crash.)

修正後のコード:

If the channel were buffered (e.g., <code>c = make(chan int, 1)</code>)
then the program would not be guaranteed to print
<code>"hello, world"</code>.  (It might print the empty string,
crash, or do something else.)

この変更のポイントは以下の通りです。

  1. 削除された部分: it cannot print <code>"goodbye, universe"</code>, nor can it crash.)

    • この部分が、データ競合が発生した場合に「クラッシュしない」という誤った保証を示唆していました。また、「"goodbye, universe" を出力できない」という記述も、未定義の挙動の範囲を不必要に限定していました。未定義の挙動は、理論上どのような結果も生み出す可能性があります。
  2. 追加された部分: crash, or do something else.)

    • この追加により、データ競合が発生した場合にプログラムが「クラッシュする」可能性が明確に示されました。
    • or do something else」(あるいは何か他のことをする)という表現は、未定義の挙動の広範な性質を正確に反映しています。これにより、特定の挙動に限定されず、予測不能なあらゆる結果が生じうることを示唆しています。

この修正により、Goのメモリモデルに関するドキュメントは、データ競合が未定義の挙動を引き起こすというGo言語の厳密な仕様に合致するようになりました。これは、開発者が並行プログラムを記述する際に、データ競合を真剣に回避する必要があることをより強く認識させるための重要な変更です。

関連リンク

参考にした情報源リンク