Upload
kumazaki-hiroki
View
4.004
Download
3
Embed Size (px)
Citation preview
Lockfree List
Lockfree Listって?
• 複数のスレッドから同時に挿入・検索・削除を行う事が可能な並行線形リストをLockfreeにしたもの
• 挿入・削除が立て込んでいてもロックを用いないので検索がロックされない
挿入処理
A B C
D
リストの繋ぎ替え処理が一瞬で片付くように工夫して、検索処理の邪魔をしない
CompareAndSwap(以下 CAS)でポインタを差し替える
挿入処理
A B C
D
リストの繋ぎ替え処理が一瞬で片付くように工夫して、検索処理の邪魔をしない
CompareAndSwap(以下 CAS)でポインタを差し替える
CAS
挿入処理
A B CD
リストの繋ぎ替え処理が一瞬で片付くように工夫して、検索処理の邪魔をしない
CompareAndSwap(以下 CAS)でポインタを差し替える
成功
挿入処理
A B C
D
CASを使う事によって、同一の場所に同時に複数の挿入が発生しても
CAS
ECAS
挿入処理
A B C
D
片方が必ず失敗する
失敗
E成功
挿入処理
A B C
D
失敗したらもう一度接続先を改めてやりなおす
E
失敗したのでやりなおし
挿入処理
A B C
D
失敗したらもう一度接続先を改めてやりなおす
E
CAS
挿入処理
A B CD
これで直列化できる
E
成功
削除処理
A B C
挿入処理と同様に、ポインタを CASで繋ぎ変える
削除
削除処理
A B C
挿入処理と同様に、ポインタを CASで繋ぎ変える
CAS
削除処理
A C
B
こうして追い出した後に Bを delete- CASのおかげで、複数のスレッドが一つのノードを取り合っても複数回 deleteせずに済む
delete
削除処理
A C
こうして追い出した後に Bを delete- CASのおかげで、複数のスレッドが一つのノードを取り合っても複数回 deleteせずに済む
しかし問題が• Bと Cを同時に削除しようとするとデータ構造が破壊される
A B C D
削除 削除
しかし問題が• Bと Cを同時に削除しようとするとデータ構造が破壊される
A B C D
CAS CAS
しかし問題が• Bと Cを同時に削除しようとするとデータ構造が破壊される–削除したはずの Cに接続されてしまう
A
B
C
D
delete
delete
しかし問題が• Bと Cを同時に削除しようとするとデータ構造が破壊される–削除したはずの Cに接続されてしまう–こちらを状況 1と呼ぶことにします
A
D
しかし問題が• 削除されるノードの次に挿入する際も
A B C D
E
削除
挿入
しかし問題が• 削除されるノードの次に挿入する際も
A B C D
E
CAS
CAS
しかし問題が• 削除されるノードの次に挿入する際も
–挿入されたはずの物が孤立してしまう
A
B
C D
E
delete
しかし問題が• 削除されるノードの次に挿入する際も
–挿入されたはずの物が孤立してしまう–こちらを状況 2と呼ぶことにします
A C D
E
そこで削除を2段階操作とする• ポインタに削除マークを取り付け、削除操作をマーキング・削除の 2ステップに分割する
• 削除マークとポインタは一つの CASで同時に扱う事ができるとする–動的に確保したオブジェクトは大体 4byte程度でアラインされているのでポインタの下位bitがそのままフラグとして使える
• リストを辿るスレッドは、マークされたノードを発見したらそれを削除する
そうなると?
A B C D
α:ここまでイタレーションし削除マーキング
そうなると?
A B C D
α:そして CASによる削除を試みるCAS
そうなると?
A B
C
D
α:成功したなら良し
そうなると?• 先ほどの状況 1は
• α ・ βの 2スレッドで同時に削除するとして
A B C D
α:ここまでイタレーションし削除マーキング
β:ここまでイタレーションし削除マーキング
そうなると?• 先ほどの状況 1は
• α ・ βの 2スレッドで同時に削除するとして
A B C D
α: CASを試みる
β: CASを試みる
そうなると?• 先ほどの状況 1は
• α ・ βの 2スレッドで同時に削除するとして
A
B
C D
α: Bのポインタがマーキングされているので CASに失敗する
β: Aのポインタは変わらないので CASに成功する
そうなると?• 先ほどの状況 1は
• α ・ βの 2スレッドで同時に削除するとして
A C D
α:リストの先頭からイタレーションし直す
そうなると?• 先ほどの状況 1は
• α ・ βの 2スレッドで同時に削除するとして
A C D
α:削除マークを発見
そうなると?• 先ほどの状況 1は
• α ・ βの 2スレッドで同時に削除するとして
A C D
α: CASを試みる
そうなると?• 先ほどの状況 1は
• α ・ βの 2スレッドで同時に削除するとして• 直列化できる
A
C
D
α: CAS成功
そうなると?• 先ほどの状況 1は
• α ・ βの 2スレッドで同時に削除するとして• 直列化できる
A D
そうなると?• 先ほどの状況 2は
A B C D
E
削除
挿入
そうなると?• 先ほどの状況 2は
A B C D
E
α:削除マーキング
β: CASを試みる
そうなると?• 先ほどの状況 2は
A B C D
E
α:削除マーキング
β:マークのせいで CAS失敗
そうなると?• 先ほどの状況 2は
A B C D
E
α: CASを試みる
そうなると?• 先ほどの状況 2は
A
B
C D
E
α: CAS成功
β:リストの頭から再びイタレート
そうなると?• 先ほどの状況 2は
A C D
E
β:リストの頭から再びイタレート
そうなると?• 先ほどの状況 2は
A C D
E
β: CASを試みる
そうなると?• 先ほどの状況 2は
–直列化できる
A C DE
β: CAS成功
ポイント• リストをイタレートするときは、削除マークが付いていないことを常に確認する–付いているならその場で削除させる
• これにより削除済みのオブジェクトに対して操作をしてしまう状況を防げる
–付いていない事を確認したはずの物にいつの間にか削除マークが付いていたならイタレートを頭からやり直す
問題点• イテレータがしょっちゅうリストの先頭に戻ってしまうので– コストが高い。なのでイタレートを進める度にロックを繰り返す悲観的ロックリストや、書き換えを行う時だけロックを行い、ロックに失敗したらリストの先頭に戻る楽観的ロックリストなどを状況に応じて使い分ける
• 読み出し頻度が多い程、ロック粒度を細かくするほうが良い
– std::list<hoge>::iterator it;のような形として個々のスレッドにイテレータを持たせるのは無理
– そもそも STLの list::iteratorも並列利用は無保証– std::setのように挿入 検索・削除の利用のみ・
話題• このリストでは ABA問題には未対処
– 挿入・削除が運悪く重なって、望まない状況で CASが成功してしまう場合がある
• 対処方法は2種類ある– 運悪く同じアドレスに CASすることになっても CASが成功しないよう、ポインタに更新スタンプを付ける
• スタンプが運悪く一周してしまうとやはり ABA問題– 参照している最中のオブジェクトは削除しない事にする
• 参照カウンタ? → atomicカウンタ重いです…• ガベージコレクタ? → マルチスレッド対応のGCが必要• ハザードポインタ → 当店お勧め
ABA問題って?
A B C D
α:ここまでイタレーションし削除マーキング
ABA問題って?
A B C D
α:そのまましばらく休眠
ABA問題って?
A B C D
α:そのまましばらく休眠
β:別の用事でイタレーションしてくる
ABA問題って?
A B C D
α:そのまましばらく休眠
β:マークを確認したので削除
ABA問題って?
A B
C
D
α:そのまましばらく休眠
β:マークを確認したので削除
ABA問題って?
A B
C
D
α:そのまましばらく休眠
γ: Bの後に新規ノード Xを挿入する
ABA問題って?
A B X D
α:そのまましばらく休眠
γ:挿入時に運悪く αが参照中のノードを使いまわしてしまう
ABA問題って?
A B X D
α:やっと目覚める
ABA問題って?
A B X D
α: Cの削除を再開する
ABA問題って?
A B X D
α: CASを発行
CAS
Cが保存されていた時と同じポインタを指してしまっ
ている
ABA問題って?
A B
X
D
α:アドレスが一致しているので CAS成功
削除する気の無かった Xが削除されてしまう
ABA問題って?• ここに書いた状況以外にも、アドレスを使いまわす限り、意図しないアドレス一致の危険性は付いて回る
• 更新カウンタを付ければノードが使い回された後でもカウンタの値を見る事で不一致を検出できるため問題を回避できるが、一度に CASしなくてはならないビット数が増えるため、 DCAS命令や STMが必要になる– 更新カウンタに割くビット数をケチるとカウンタが一巡して一致する危険性がある
ABA問題って?• そもそも他のスレッドが参照している最中のものを削除して使いまわすから悪い
• じゃあ削除しなければ良い。でもどうやって?
• 参照カウンタ→カウンタを atomicに操作する必要がある上、ノードごとにカウンタが付くためリストが肥大化
• ガベージコレクタ→ GC中に全スレッドを止めるしか安全な方法が無い
• そこでハザードポインタです(次回へ