はじめに
この記事は、勉強がてら覗いているPostgreSQLのgitリポジトリの
src/backend/storage/lmgr/README.barrier
を翻訳したものです。
低レイヤのマルチプロセスは闇が深い。
OutOfOrder実行(OOO: Out of Order execution)とは?
最近のCPUでは並列実行による高速化の恩恵を受けるため、結果一致が保証される範囲でプログラムの命令実行の順序性を無視して並列実行を行う場合がある。
これを、Out Of Order execution(OOO)と呼ぶ。
OOOが行われることによって生まれる潜在バグ
しかし、OOOによって順序性が崩れることで、プログラムから共有メモリにアクセスする際にバグが生まれる場合がある。
例:
47 q->items[q->num_items] = new_item;
48 ++q->num_items;
上記のようなコードが実行されている、まさにその瞬間に
以下のコードが実行された場合。
52 num_items = q->num_items;
53 for (i = 0; i < num_items; ++i)
54 /* do something with q->items[i] */
47行目と48行目は順番に実施される保証はないので、
52行目ではインクリメントされたアイテム数が読まれるが、54行目以降でメモリ上にそのデータが格納されていない場合がある。
この場合、設計者の意図に反したふるまいを起こし、NullPointer Exceptionが発生する場合がある。
解決方法
解決方法1: LightWeightLockを獲得する。
最もシンプルな方法で、確実。
→データのやり取りが頻繁の場合、ネックになる恐れ
※PostgreSQLのLightWeightLockについては別途
解決方法2: Memory Barierr を使う
PostgreSQLでは、pg_memory_barrier()マクロによってこれを防ぐことができる。
これは、
- コンパイラのOOO最適化による、コードの順序変更を防ぎ
- CPUが順不同にメモリアクセスすることをを防ぐようにバイトコード(インラインアセンブリ)を生成する。
以下、コード例:
86 q->items[q->num_items] = new_item;
87 pg_memory_barrier();
88 ++q->num_items;
89
90 And by having the reader do this:
91
92 num_items = q->num_items;
93 pg_memory_barrier();
94 for (i = 0; i < num_items; ++i)
95 /* do something with q->items[i] */
しかし、x86システムなどにおいて、メモリアクセスの順序性確保の強制によって、CPUは読み込み同士、書き込み同士の最適化もしなくなります。
これを避けるため、メモリバリアをreadとwriteに分割することもできます。
以下例:
118 q->items[q->num_items] = new_item;
119 pg_write_barrier();
120 ++q->num_items;
121
122 And the reader can do this:
123
124 num_items = q->num_items;
125 pg_read_barrier();
126 for (i = 0; i < num_items; ++i)
127 /* do something with q->items[i] */
メモリバリアでもだめな例
1)
x->foo++;
というような書き込み処理が複数のプロセスから行われる。
→ 読み出し 加算処理 元のアドレスへの書き込み という3つの命令の間にほかの読み出し 加算処理 元のアドレスへの書き込み が発生する可能性あり
Atomic fetch and add 命令を使うという方法もあるが、すべての環境で使えるわけではないのでPostgreSQLにおいてはこれをサポートしない
-
8バイトデータの読み書き
-
複数のメモリバリアを使用する場合
→単純にLOCKをとった方が効率的である場合もあるこということ
上記のような場合は、LightWeightLockの方法を使う。