C言語で苦しむロックフリー入門(仮

0

No comments posted yet

Comments

Slide 21

Ff

Slide 22

Ff

Slide 23

Ff

Slide 24

Ff

Slide 1

C言語で苦しむロックフリー入門(仮) 熊崎宏樹 @kumagi

Slide 2

C言語 CPUの息遣いを感じられる良い言語 ロックフリーなプログラムを書くには避けては通れないsafe mamory reclamation問題に一番ダイレクトに衝突する言語 スペースの都合上、スライド上のコードはグローバル変数モリモリだから真似しちゃダメ メモリ確保も絶対成功する前提で書いてるけど真似しちゃダメ ほんとはキャストが必要な部分もスペースの都合で省略

Slide 3

Stackについて 最初に入れた物が最後に出てくるデータ構造 積み重ねるようなデータの持ち方をするからStackと呼ばれる 今回話すstackがサポートするメソッドはpush()とpop()のみとする

Slide 4

Stackについて void push(int x): x を上に積む。関数は何も返さない物とする。 int pop(): 最後に積んだ値を取ってくる。 push(1); push(2); push(3); int x = pop(); // => x=3 int y = pop(); // => y=2 int z = pop(); // => z=1

Slide 5

C言語での実装 typedef struct node{ int data; node* next; } node_t; node_t *head = NULL; 構造体定義 線形リストでスタック構造を表現 普通は配列で作るが敢えて線形リスト

Slide 6

C言語での実装 void push(int x) { // 初期化して node_t *new_node = (node_t*)malloc(sizeof(node_t)); new_node->data = x; new_node->next = head; //挿入 head = new_node; }

Slide 7

C言語での実装 int pop() { // 獲得して node_t *got_node = head; node_t *next_head = got_node->next; int value = got_node->data; free(got_node); // 解放して return value; // 返却 }

Slide 8

並行処理実装 近年CPUコアは(中略)マルチスレッド(後略) void* work(void*) { for (int i = 0; i < 100; ++i) { push(i); } } int main(void) { pthread_t t1, t2; pthread_create(&t1, NULL, work, NULL); pthread_create(&t2, NULL, work, NULL); pthread_join(&t1); pthread_join(&t2); }

Slide 9

C言語での並行push実装 void push(int x) { node_t *new_node = (node_t*)malloc(sizeof(node_t)); new_node->data = x; new_node->next = head; pthread_mutex_lock(&stack_lock); head = new_node; pthread_mutex_unlock(&stack_lock); }

Slide 10

C言語での並行pop実装 int pop() { pthread_mutex_lock(&stack_lock); node_t *got_node = head; head = got_node->next; pthread_mutex_unlock(&stack_lock); int value = got_node->data; free(got_node); // 解放して return value; // 返却 }

Slide 11

Mutexでだいたい良い ぶっちゃけStackでなら一番パフォーマンスが出る並行処理実装 「mutexを使え」が実用上もっとも正しくて賢くてメンテナンスしやすい

Slide 12

Mutexなしでできるのでは? Compare And Swap命令を使えばできる!

Slide 13

Compare And Swap 指定したアドレスxが指定した値yだったら新しい値zで書き換えるまでを不可分に行えるCPU命令 以下は疑似コード int CAS(void** x, void* y, void* z) { if (*x == y) { **x = *z; return 1; } else { return 0; } }

Slide 14

CASスピン CASを使って成功するまで無限ループするコードを書けばロックが要らない!

Slide 15

スレッドB Mutexを用いないとどうなるか 複数スレッドが同時に行うと xを読み出す(1) 読んだ値に +1 xを保存する(1) x==1 xを読み出す(2) 読んだ値に +1 xを保存する(3) x==3 スレッドA OK!

Slide 16

スレッドB スレッドA Mutexを用いないとどうなるか 複数スレッドが同時に行うと破綻する場合がある xを読み出す(1) 読んだ値に +1 xを保存する(2) x==1 xを読み出す(1) 読んだ値に +1 xを保存する(2) x==2 数が合わない

Slide 17

CASの使い方例 int x = 0; void add_unsafe() { ++x; } int x = 0; void add_cas() { for (;;) { // spin int old_x = x; if (CAS(&x, old_x, x+1)) { break; } } }

Slide 18

スレッドB スレッドA CASを使ってみよう CASのお陰で衝突しても破綻しない xを読み出す(1) 読んだ値に +1 値が1なら2へCAS 失敗したので再挑戦 xを読み出す(2) 読んだ値に +1 値が2なら3へCAS x==1 xを読み出す(1) 読んだ値に +1 値が1なら2へCAS x==3 数が合う!

Slide 19

Lock-free Stack push void lock_free_push(int x) { node_t *new_node = (node_t*)malloc(sizeof(node_t)); new_node->data = x; do { node_t *old_head = head; new_node->next = head; } while (!CAS(&head, old_head, new_node)); }

Slide 20

Lock-free Stack Push CASによってリトライができるので衝突もセーフ

Slide 21

Lock-free Stack 「Headが指している物を指したノードを作ってCAS」 Head A ↓ポインタ CAS

Slide 22

Lock-free Stack Head A B C D CAS CAS CAS 失敗した! 失敗した!

Slide 23

Lock-free Stack Head A CAS CAS また失敗した!

Slide 24

Lock-free Stack Head A B D CAS

Slide 25

Lock-free Stack pop int lock_free_pop() { node_t *old_head; for (;;) { old_head = head; node_t *next_head = old_head->next; if (CAS(&head, old_head, new_head)) { int data = old_head->data; free(old_head); return data; } } }

Slide 26

Lock-free Stackからpop Head A C D B CAS CAS

Slide 27

Lock-free Stack ABA problem CASは「値が一致した場合に成功する」事までしか確認しない。運悪く一致してしまった場合に事故る。

Slide 28

Lock-free StackのABA Head D C B A Aをpop()しよっと HeadをAからBに書き換えるぞー! うおおおおおー HeadがAを指してたからBに書き換えれるぞー!やったー! push(x)しよっと メモリはAでいいや もう1回pop()しよっと

Slide 29

SEGV

Slide 30

よく言われる解決策 Tagを付ければ解決するよ[1] LL/SCを使ってもいいね[1] LL/SCはx86系CPUでは使えない Double WordのCASを使って、2word目をカウンタに使うとカウンタに充分なビット数が割けるので安心 そもそも2wordのatomicなreadが無いじゃん。 でもpushとpopの両方で増やしたら大丈夫になったわ[2] [1]2004 Maged M. Michaelら ABA Prevention Using Single-Word Instructions1 [2]The difficulty of lock-free programming: a bug in lockfree stack

Slide 31

Lock-free StackのABA Head1 D C B A Aをpop()しよっと HeadをAからBに書き換えるぞー! うおおおおおー HeadがAを指してたけどTag値が1じゃなくて4だからやり直し push(x)しよっと メモリはAでいいや もう1回pop()しよっと Head2 Head3 Head4 大丈夫ぽい!?

Slide 32

SEGV だが

Slide 33

Lock-free Stack pop int lock_free_pop() { node_t *old_head; for (;;) { old_head = head; node_t *next_head = old_head->next; if (CAS(&head, old_head, new_head)) { int data = old_head->data; free(old_head); return data; } } } int lock_free_pop() { node_t *old_head; for (;;) { old_head = head; node_t *next_head = old_head->next; if (CAS(&head, old_head, new_head)) { int data = old_head->data; free(old_head); return data; } } } head D C B A OSへ返却 返却したメモリ->next; を読む! TagによるABA避けをした実装

Slide 34

SEGV

Slide 35

そもそも別の解決策しかない メモリを適当なタイミングでfree()するのは事故のもと そもそもpop()だけをlockで守る解決策もある この問題はガベージコレクションのある言語では発生しない 全てのスレッドが参照しなくなってからfree()されるから よし!同一の状況をCでも再現しよう 参照カウンタ? 参照時のカウンタ更新コストで死ぬ

Slide 36

解決策: Hazard Pointer どこのポインタが捨てられたら困るかを共有する 固定長のグローバルな配列を用意する 1スレッドが1要素使う free前に他のスレッドがそれを使ったら待機する volatile node_t *h_ptr[THREADS]; // global int lock_free_pop() { for (;;) { h_ptr[tid] = head; memory_fence(); if (head != h_ptr[tid]) continue; node_t *next_head = h_ptr[tid]->next; if (CAS(&head, h_ptr[tid], new_head)){ int data = h_ptr[tid]->data; for (int i=0; i<THREADS;) if (tid == i) {++i; continue} else if (h_ptr[i] != h_ptr[tid]) ++i; else sched_yield(); free(h_ptr[tid]); return data; } } }

Slide 37

volatile node_t *h_ptr[THREADS]; // global int lock_free_pop() { for (;;) { h_ptr[tid] = head; memory_fence(); if (head != h_ptr[tid]) continue; node_t *next_head = h_ptr[tid]->next; if (CAS(&head, h_ptr[tid], new_head)){ int data = h_ptr[tid]->data; for (int i=0; i<THREADS;) if (tid == i) {++i; continue} else if (h_ptr[i] != h_ptr[tid]) ++i; else sched_yield(); free(h_ptr[tid]); return data; } } } Hazard pointer Lock-free Stack pop head D C B A 他の全てのスレッドが抜けるのを待つ volatile node_t *h_ptr[THREADS]; // global int lock_free_pop() { for (;;) { h_ptr[tid] = head; memory_fence(); if (head != h_ptr[tid]) continue; node_t *next_head = h_ptr[tid]->next; if (CAS(&head, h_ptr[tid], new_head)){ int data = h_ptr[tid]->data; for (int i=0; i<THREADS;) if (tid == i) {++i; continue} else if (h_ptr[i] != h_ptr[tid]) ++i; else sched_yield(); free(h_ptr[tid]); return data; } } } 解放されないので安心

Slide 38

patented US 20040107227 2010年に放棄されたとwikipediaにはあるが…

Slide 39

解決策:Pass the buck 和訳するなら「なすりつけ法」 hazard pointerのようにglobalなhazard_ptr配列を最初に定義するとエントリ数の需要の動的な増減に耐えられない 利用する時だけhazard_ptr配列のどこを自分のスレッドが使ってよいかをCASで取り合う技法 patented: US7194495 B2 status: 認定 と書いてあるので確実に危険

Slide 40

解決策:RCU Read-Copy-Updateの略でRCU カーネル空間内で、参照頻度の割に更新頻度が極端に低いデータをロックなしで保護する為に使っているアルゴリズム 書き換え側のコストがすごい事になったりするが実用上の問題はない

Slide 41

RCU Lock-free Stack push rcu_read_lockによってrcuクリティカルセクションを記述する そのセクション内のスレッドはプリエンプションされない 余計な共有メモリを必要としないしread側の負荷はかなり小さい int lock_free_pop() { node_t *old_head; for (;;) { rcu_read_lock(); old_head = head; node_t *next_head = old_head->next; if (CAS(&head, old_head, new_head)) { int data = old_head->data; rcu_read_unlock(); synchronize_rcu(); free(old_head); return data; } } }

Slide 42

RCU Lock-free Stack pop int lock_free_pop() { node_t *old_head; for (;;) { rcu_read_lock(); old_head = head; node_t *next_head = old_head->next; if (CAS(&head, old_head, new_head)) { int data = old_head->data; rcu_read_unlock(); synchronize_rcu(); free(old_head); return data; } } } head D C B A 他の全てのスレッドが抜けるのを待つ 解放されないので安心! RCUでメモリ解放を遅延 int lock_free_pop() { node_t *old_head; for (;;) { rcu_read_lock(); old_head = head; node_t *next_head = old_head->next; if (CAS(&head, old_head, new_head)) { int data = old_head->data; rcu_read_unlock(); synchronize_rcu(); free(old_head); return data; } } }

Slide 43

RCU: Grace Period rcuクリティカルセクション内ではプリエンプションしなくなる 実を言うとプリエンプションしても良い版の実装も存在するが詳細はまだ追ってない synchronize_rcuで他のスレッドが最低1回ずつプリエンプションするのを待つ 古いheadを観測して走ってるスレッドを邪魔しない

Slide 44

RCU: Grace Period プリエンプションを禁じるような操作をユーザ空間で気軽に使われると危険が危ない そもそもユーザに使わせるべきではない つまりカーネル空間ならではの解決法であり、ユーザ空間では使えない

Slide 45

解決策: Quiescent-State-Based-Reclamation グローバルなカウントを増やして、それをみんなが観測した後ならfreeしてOK read側はglobal_countを読むだけ 更新が無ければキャッシュラインがSステートに落ちるので最速 write時は更新したglobal_counterが他の全スレッドに読まれるまで待機する uint64_t global_count = 0; uint64_t local_count[THREADS] = {0}; int lock_free_pop() { node_t *old_head; for (;;) { local_count[TID] = global_count; old_head = head; node_t *next_head = old_head->next; if (CAS(&head, old_head, new_head)) { int data = old_head->data; local_count[TID] = add_and_fetch(global_count); for (int i=0; i < THREADS;) if (local_count[TID] <= local_count[i]) ++i; // 読まれた場合だけiが進む free(old_head); return data; } } }

Slide 46

速い! Making Lockless Synchronization Fast: Performance Implications of Memory Reclamation 参照するメモリ数nに対してO(1)のメモリフェンスで済む

Slide 47

解決策: Quiescent-State-Based-Reclamatfion 利点: 特許は取られてなさげ read側は高速 欠点: すべてのスレッドが定期的にglobal_countを読む前提がある 状況によっては結構大規模な改修になる read側のクリティカルセクションがネストした場合、外側のセクションは保護対象外になってしまう ネスト版のEpoch Based Reclamationもあるけど今回は時間の都合で話せない

Slide 48

まとめ とても簡単なLock-free StackひとつとってもC言語上でスレッドセーフにするのは非常に大変 デバッグの難しさ 特許の罠 可変スレッド数対応 メモリバリア厳しい Mutex使え

Summary: Lock-free stackにおける、メモリ解放タイミング問題に対する解決策のサーベイ

URL: