上発行プログラミング(二):言語記憶モデル——プログラム员以下ダウンロード可能電子映像 – ThinkerQAQ – 博客园

0
上発行プログラミング(二):言語記憶モデル——プログラム员以下ダウンロード可能電子映像 – ThinkerQAQ – 博客园


この文章は ThinkerQAQ の個人ブログで、同時に著者自身によって公開されています。

ディレクトリ


0.続行

上の記事では、ハードウェア層が原子性、可視性、秩序性という基本的な機能をどのように提供するかについて説明しています。

この記事では、視点を言語層に切り替えます。さまざまなハードウェア プラットフォームに直面して、プログラミング言語はプログラマに統一された並列言語を提供する必要があります。

  • 原子性: これらの電子機器は以下にダウンロードできる異なる電気;
  • 可視性: あるスレッドの書き込みが別のスレッドに表示されることが保証されるのはいつですか;
  • 指示: 複数の操作間の前述の関係では、他のスレッドがこの順序に従うことができるといつ保証できますか?

その中でも、Java と Go のメモリ モデルは、目に見えて順序を持つコア抽象です。 それは前に起こります


1. 言語に独自の並列言語が必要なのはなぜですか?

ソース コードはコンパイラとランタイムを通過する必要があり、最終的に CPU 上で実行できるようになります。

プログラマがコンパイラ、ランタイム、CPU、クロスプラットフォーム、並列プログラミングの各種類を個別に調査する必要がある場合

したがって、言語ルールの層も必要です。

Java                                  Go                                  Python
 │                                     │                                     │
 ▼                                     ▼                                     ▼
synchronized / volatile / AtomicInteger
                                       sync.Mutex / sync/atomic / channel
                                                                             threading.Lock / queue.Queue
 │                                     │                                     │
 ▼                                     ▼                                     ▼
Java Memory Model                Go Memory Model              CPython Concurrency Semantics
 │                                     │                                     │
 └─────────────────────────────────────┼─────────────────────────────────────┘
                                       │
                         Compiler / Runtime 实现
                         屏蔽不同硬件平台的差异
                                       │
                              ┌────────┴────────┐
                              │                 │
                              ▼                 ▼
                           x86-64             ARM64

言語メモリ モデルは、プログラマが信頼できる動作を定義し、コンパイラとランタイムは、これらの保証を実現するために再び異なるハードウェア プラットフォームをターゲットにします。

したがって、プログラムが最終的に x86-64 または ARM64 で実行されるかどうかに関係なく、プログラマは、どの CPU 命令を最下層で生成する必要があるかを個別に分析する必要はなく、言語で定義された同時実行ルールに従ってプログラムの動作を判断します。


2. 言語記憶モデルは何を答える必要がありますか?

前の 2 つの例を引き続き使用します。

counter++

そして:

counter = 1
ready = true

言語層には答えが必要です。

問題 具体的に言うと
原子性 counter++ ダウンロードは不可分とみなされますか?
可視性 書くには counter = 1 その後、B は以下に同じようにダウンロードできますか?
指示 Bが見ると ready = true 時、これもまたこの設計の counter = 1

言語メモリ モデルでは、プログラマがキャッシュ、ストア バッファ、またはガードを直接分析する必要はありません。これは結果の表示を可能にするソース コード層を定義し、最下層はこれらのルールを満たす責任があります。


2.1 Java メモリ モデル

Java メモリ モデル (JMM) Java 定義

前の 3 つの質問から引き続き説明します。

原子性

先見:

int counter = 0;

// Thread A
counter++;

// Thread B
counter++;

即ち:

Thread A                    Thread B

counter++;                  counter++;

counter++ 同時実行の観点からは、次のように理解できます。

read counter
     ↓
counter + 1
     ↓
write counter

2 つのスレッド間で次のエラーが発生する可能性があります。

Thread A                    Thread B

read counter = 0            read counter = 0

counter + 1                 counter + 1

write counter = 1           write counter = 1

2 つのスレッドが 1 回実行される counter++、最終的には次のように取得できます。

expected: counter = 2
actual:   counter = 1

ここでJMMは明示的には言っていないcounter++ 「不是電影的」ですが、それでも次のことを知る必要があります。counter++ これは複数のアクションで構成される複雑な更新であるため、操作全体を 1 つとして直接見ることはできません。


可視性

卡看:

int counter = 0;

// Thread A
counter = 1;

// Thread B
System.out.println(counter);

即ち:

Thread A                    Thread B

counter = 1                 read counter

ここで私たちが本当に気にかけていることは次のとおりです。

スレッドAの書き込み counter = 1 その後、タブ B のダウンロードは作成者によって行われます 1

JLS §17.4.5 の発生前に非常に効果的な設定:

「あるアクションが別のアクションの前に発生すると、最初のアクションが表示され、2 番目のアクションの前に順序付けされます。」

つまり、次のことが確立できれば、

Thread A                    Thread B

counter = 1                 read counter
     │                           ▲
     │                           │
     └──── happens-before ───────┘

この場合、スレッド A の書き込みはスレッド B の読み取り可能性を保証します。

そして、このコードでは次のようになります。

Thread A                    Thread B

counter = 1                 read counter
     │                           ▲
     │                           │
     └── no happens-before ──────┘

歌詞の安全主作は、関係の前に起こります。

したがって:

Thread B 可能看到 1

しかし、この手順は信頼できません。

Thread B 一定看到 1

VisibilityのJMMが提供する判定方法です。


指示

2 番目の例に戻ります。

// Thread A
counter = 1;
ready = true;

// Thread B
if (ready) {
    System.out.println(counter);
}

即ち:

Thread A                    Thread B

counter = 1
ready = true                read ready == true
                                  │
                                  ▼
                             read counter

ここでの懸念は次のとおりです。

スレッド B が表示されました ready == true,下方も確実に前が見えるという意味です counter = 1

電影電影の定義前に発生する可能性手机了交交分生:

visible to
+
ordered before

つまり:

Visibility
    ↓
前一个操作的结果是否被保证对后一个操作可见

Ordering
    ↓
两个操作之间是否存在程序可以依赖的先后关系

ただし、時間のかかるコードでは、タブ A とスレッド B ですが、携帯端末には最新のダウンロードが保証されていません:

Thread A                    Thread B

counter = 1
ready = true                read ready == true
     │                            │
     │                            ▼
     │                       read counter
     │
     └──── no cross-thread happens-before

したがって、次のことのみが観察されます。

ready == true

プログラムをさらに以下に依存させることはできません:

counter 一定等于 1

ハンドマシンの安全メインがどのようにダウンロードするかについては、メディアを置き換える作業の際に再検討する必要があります。


2.2 Go Memory モデル

同じ方法で独自のメモリ モデルを定義し、それを使用して Goroutine のメモリ操作を定義します。

まったく同じ 3 つの質問を引き続き使用します。

原子性

先見:

var counter int

// Goroutine A
counter++

// Goroutine B
counter++

同時実行の観点からは、同じことは次のように理解できます。

read counter
     ↓
counter + 1
     ↓
write counter

2 つのゴルーチンでこの種のクロストークが発生する可能性があります。

Goroutine A                 Goroutine B

read counter = 0            read counter = 0

counter + 1                 counter + 1

write counter = 1           write counter = 1

したがって:

expected: counter = 2
actual:   counter = 1

Go メモリ モデルの非公式な概要 データ レースには非常に優れた定義があります。

「データ ストリームは、同じ場所への別の読み取りまたは書き込みと同時に発生する、メモリ ロケーションへの書き込みとして定義されます。」

つまり、1 つのゴルーチンが特定のメモリ位置に書き込み、別のゴルーチンが同じメモリ位置に読み取りまたは書き込みを実行し、これらのアクセスが失敗した場合です。 sync/atomic 提供されたアトム操作が完了すると、データ競合が発生します。

この例への回帰:

Goroutine A                 Goroutine B

read counter                read counter
     │                           │
write counter               write counter

2 つのゴルーチンは同じものを共有します counter,これには写電影が含まれており、これらのアクセスは同期されていないため、ここでデータ競合が発生します。

それで:

counter++

不可分な同時更新とはみなされません。


可視性

続きを読む:

var counter int

// Goroutine A
counter = 1

// Goroutine B
fmt.Println(counter)

拆成两列:

Goroutine A                 Goroutine B

counter = 1                 read counter

ゴーメモリモデル

1 つのゴルーチンが読み取られ、そのコンテンツ内で電子ダウンロードを行うための設定が行われました

公式ルールでは、一般の読者は特定の書き込みを確実に観察でき、この書き込みは読むために必要です。条件の 1 つは次のとおりです。

「w は r の前に発生します。」

確立できる場合は、この例をもう一度示します。

Goroutine A                 Goroutine B

counter = 1                 read counter
     │                           ▲
     │                           │
     └──── happens-before ───────┘

そうすれば、この書き込みは後で確実に観察できます。

そして現在のコードでは次のようになります。

Goroutine A                 Goroutine B

counter = 1                 read counter
     │                           ▲
     │                           │
     └── no happens-before ──────┘

2 つのゴルーチンの間にそのような関係はなく、データ競合が存在します。

したがって、2 番目のゴルーチンに依存することはできません。

counter == 1

指示

卡看:

// Goroutine A
counter = 1
ready = true

// Goroutine B
if ready {
    fmt.Println(counter)
}

拆成双列:

Goroutine A                 Goroutine B

counter = 1
ready = true                read ready == true
                                  │
                                  ▼
                             read counter

この例と Go メモリ モデルはここにあります 不正な同期 与えられた例では、基本的に同じです: 1 つのゴルーチンは先円写データ、再書内行; 別のゴルーチンは後でロゴを観察し、前のデータを再度読み取ります。

公式声明に移動します。

「たとえこれが起こったとしても、r の後に発生した読み取りが w より前に発生した書き込みを認識するという意味ではありません。」

私たちの例を振り返る:

Goroutine A                 Goroutine B

counter = 1
ready = true                read ready == true
                                  │
                                  ▼
                             read counter

        没有 happens-before 保证

それで:

看到 ready == true

例 机が使えない:

一定看到 counter == 1

これは Java の例に非常に近いです。

Java と Go は以前に来電影電視官が使用していましたが、食立電視の作成にはまだ電影モデルが含まれています


2.3 CPython の投稿

Python と Java には重要な違いがあります、Go:

Python には、JMM や Go Memory Model など、すべてのインタープリターに適したセットがありません。

そこでここでは、最も頻繁に使用される実装について説明します。CPython

同じ 2 つの例を引き続き使用します。

counter += 1

そして:

counter = 1
ready = True

原子性、可視性、順序を参照してください。

2.3.1 GILモード

議論を続けます counter 前に、正しく理解しましょう。

結局ギルは何を守ったのか?

GIL は、まず、Python オブジェクトとインタープリターの内部状態を保護するために CPython ランタイムによって使用されるメカニズムです。

GIL の Python 公式 C API ドキュメントの要件は次のとおりです。

「GIL を所有するスレッドのみが、Python オブジェクトを操作したり、C Python API を呼び出したりできます。」

つまり、GIL に対してデフォルトで有効になっている CPython、電視通信先先サポートGIL、手机Python对谱炡戨がGILを持っている必要があります

公式文書が添付されています 参考文献の数 このロックが必要な理由の説明: 2 つのスレッドが同じオブジェクトの参照カウントを同時に増加させた場合、最終的には 2 回ではなく 1 回だけ増加します。

現在 Python オブジェクトがあると仮定します。

refcount = 10

2 つのスレッドがこの参照カウントを同時に変更できる場合:

Thread A                    Thread B

read refcount = 10          read refcount = 10

refcount + 1                refcount + 1

write refcount = 11         write refcount = 11

2 つのスレッドで一度引用が追加されましたが、最終的には次のようになります。

expected: refcount = 12
actual:   refcount = 11

これにより、CPython による Python オブジェクトのライフサイクル管理が破壊されます。

したがって、GIL はまず以下を保護します。

             GIL
              │
              ▼
      CPython Runtime
              │
      ┌───────┴────────┐
      ▼                ▼
Python Objects     Runtime State
      │
      ├── Reference Count
      └── Object Internals

それは解決します:

CPython 自己如何安全地操作 Python 对象

の代わりに:

应用程序中的所有共享状态
自动获得线程安全

原子性

ここで次の話に戻ります。

counter += 1

アプリケーションの観点から見ると、これは依然として 1 回の読み取り、変更、書き込みです。

read counter
     ↓
counter + 1
     ↓
write counter

つまりコンセプト:

Thread A                    Thread B

read counter                read counter

counter + 1                 counter + 1

write counter               write counter

Python公式FAQ

i = i + 1

免费列在非基机手机一級:

「これらは次のようなものではありません: i = i+1

したがって、GILがあっても「読み込み→変更→書き回」という複合更新は直接的には全体のアトミックな操作に依存できるアプリケーションとみなすことができる。

ここで区別する必要があります。

GIL
 │
 └── CPython Runtime / Python Object 的保护

counter += 1
 │
 └── 应用程序定义的一次复合状态更新

可視性

同じ例を引き続き使用します。

counter = 0

# Thread A
counter = 1

# Thread B
print(counter)

拆成双列:

Thread A                    Thread B

counter = 1                 read counter

この時点で、CPython と Java/Go には違いがあります。

Java と Go は続行できます。

write
 │
 │ happens-before ?
 ▼
read

なぜなら、それらには正式な定義のメモリモデルがあるからです。

しかし、Python には、すべての Python 実装に適用できる対応するものがなく、ルールの前に発生します。

したがって、上記の CPython コードは作成できません。

Thread A                    Thread B

counter = 1                 read counter
     │                           ▲
     │                           │
     └──── Python happens-before ┘

次に、Python 言語メモリ モデルを参照して結論を​​導き出します。この方法ではこのモデルは存在せず、元のモデルと互換性がないからです。


指示

最後に読んだ:

# Thread A
counter = 1
ready = True

# Thread B
if ready:
    print(counter)

以下と同じ:

Thread A                    Thread B

counter = 1
ready = True                read ready == True
                                  │
                                  ▼
                             read counter

在 Java / Go 中,我们可以下下下下下メモリ モデル

電気、最高の歌の歌は、中国のルールに先立ち、安全なPython電気を使用、設計しています。

したがって、プログラムが明確なクロススレッド保証を必要とする場合は、明確な同期ツールを使用する必要があります。

この金属加工のすべては、原子性、可視性、順序付け、次の文章の追加です。


2.3.2 糸のほつれのあるパターン

Python 3.13 以降、CPython は GIL を無効にできるフリースレッド ビルドを提供します。

GIL に対して CPython が有効になっています 中:

Thread A ──┐
           │
           ├── GIL ───> Python Code
           │
Thread B ──┘

フリースレッド CPython の場合:

Thread A ─────────────> CPU Core 0

Thread B ─────────────> CPU Core 1

実際には、複数のスレッドで Python コードを並行して実行できます。

ただし、GIL を削除しても、CPython がそのオブジェクトと実行時の状態を保護する必要がなくなるわけではありません。

Python の公式フリースレッド ドキュメントで明確に説明されています。dictlistset 等の処理は内部ロックを使用して保護および変更します。

「組み込みタイプ」 dictlistそして set 内部ロックを使用してください」

これらの内部ロックは、フリースレッド CPython の実装メカニズムに属しており、Python 语言山可以了可帀庀一モデル

したがって、変更は次のようになります。

GIL-enabled

一把全局的 GIL
      │
      ▼
保护 CPython Runtime

なる:

Free-threaded

更细粒度的内部同步
      │
      ▼
保护 CPython Runtime

このアプリケーションに関しては、前述の 3 つの問題が依然として存在します。

Atomicity

counter += 1
是否能被当成不可分割的共享状态更新?
Visibility

一个线程的写入,
什么时候能被另一个线程可靠观察?
Ordering

两个线程之间,
哪些操作顺序是程序可以依赖的?

フリースレッドでは、CPython ランタイムがスレッド セーフを実現する方法が変更され、これらの同時発生する問題は解消されません。


2.3.3 Python プログラムは何に依存する必要がありますか?

读上、Java、Go、CPython はこの層に存在しますが、重要な違いがあります。

Java
  │
  ▼
Java Memory Model
  │
  └── happens-before

Go
  │
  ▼
Go Memory Model
  │
  └── happens-before

Python
  │
  ▼
不同 Interpreter 的并发语义
  │
  └── CPython
        ├── GIL-enabled
        └── Free-threaded

要約は次のような文です。Python には、すべてのインタプリタに適用できる 1 つのセットがありません。JMM または Go メモリ モデル同瀚纉站瀚纟モデル。したがって、Python の安全性について議論するには、まずどのような種類のインタープリターが使用されるかを明確に定義する必要があります。これは、特定の同期ツールと、それによって提供される同時実行性の保証に依存します。


3. 下一篇:二步集

この記事はまだ、言語層からの 3 つの質問に答えているだけです。

Atomicity
Visibility
Ordering

最初の特定の同期ツール: Mutex。

まったく同じ 3 つの方向に沿って進みます。

Atomicity
    ↓
一把锁为什么能够让临界区表现为不可分割?

Visibility
    ↓
为什么前一个临界区的写入,
能够被后一个进入临界区的线程看到?

Ordering
    ↓
锁如何约束同步边界两侧的操作顺序?

次に、これらの保証を提供する方法について、Java、Go、CPython のロックを個別に見ていきます。

ランタイムから CPU まで下に進みます。


参照

  1. Java 言語仕様 §17.4 — メモリ モデル
  2. Java 言語仕様 §17.4.5 — 注文前に発生
  3. Go メモリー モデル
  4. Python C API — スレッド状態とグローバルインタープリターのブロック
  5. Python FAQ — どのようなタイプのグローバル値の変更がスレッドセーフですか?
  6. 無料のスレッド化のための Python サポート
  7. PEP 703 — CPython でのオプションのグローバル インタープリタ ロック

Leave a Reply

Your email address will not be published. Required fields are marked *