LockfreePriorityQueue
PriorityQueueとは? 順序関係のあるアイテム群に、新規アイテムをO(log n)で挿入でき、最小の物をO(log n)で取り出せるデータ構造 ヒープとも言う 最小の物以外を検索して取り出すコストは高い
Lockfree PriorityQueueとは 複数のスレッドからロック無しで同時に挿入・取り出しを行ってもきちんと動くPriorityQueue マルチコアでダイクストラやA*などを走らせるのに便利かも?
構造 2 分木を用意する 木の節ごとに atomic なカウンタが付いている 木の先端部にアイテムが保持される 0 0 0 0 0 0 0 1 2 3 4 5 6 7 8 アイテムを保持 Atomicに操作 できるカウンタ
挿入 優先順位の番号に対応する先端部分にデータを保存 先端から根に向けて節を辿っていき、節から見て左の枝なら節のカウンタをインクリメント 根に達したら終了 0 0 0 0 0 0 0 1 2 3 4 5 6 7 8
挿入 優先順位の番号に対応する先端部分にデータを保存 先端から根に向けて節を辿っていき、節から見て左の枝なら節のカウンタをインクリメント 根に達したら終了 0 0 0 0 0 1 0 1 2 3 4 5 6 7 8
挿入 優先順位の番号に対応する先端部分にデータを保存 先端から根に向けて節を辿っていき、節から見て左の枝なら節のカウンタをインクリメント 根に達したら終了 0 0 0 0 0 1 0 1 2 3 4 5 6 7 8
挿入 優先順位の番号に対応する先端部分にデータを保存 先端から根に向けて節を辿り、節から見て左の枝なら節のカウンタをインクリメント 根に達したら終了 1 0 0 0 0 1 0 1 2 3 4 5 6 7 8
挿入 優先順位の番号に対応する先端部分にデータを保存 先端から根に向けて節を辿り、節から見て左の枝なら節のカウンタをインクリメント 根に達したら終了 1 0 0 0 0 1 0 1 2 3 4 5 6 7 8 完了!
挿入操作同士の干渉 カウンタは atomic に操作するため共有しても問題ない 同一の先端部分に複数のアイテムを保存する場合だけが問題 具体的には先端部分に並行 FIFO などを取り付ければ解決 1 0 0 0 0 1 0 1 2 3 4 5 6 7 8
取り出し 先ほどの手順でいくつか挿入した状態を例にします オレンジ色はアイテムが挿入された印 つまり、節のカウンタは、その節より左にいくつアイテムがあるか数えている 2 1 1 0 0 1 0 1 2 3 4 5 6 7 8
取り出し 根から順に下に向かって辿る 節点のカウンタが 1 以上ならデクリメントして左へ辿る 節点のカウンタが 0 なら何もせず右へ辿る 以後繰り返し 2 1 1 0 0 1 0 1 2 3 4 5 6 7 8
取り出し 根から順に下に向かって辿る 節点のカウンタが 1 以上ならデクリメントして左へ辿る 節点のカウンタが 0 なら何もせず右へ辿る 以後繰り返し 1 1 1 0 0 1 0 1 2 3 4 5 6 7 8
取り出し 根から順に下に向かって辿る 節点のカウンタが 1 以上ならデクリメントして左へ辿る 節点のカウンタが 0 なら何もせず右へ辿る 以後繰り返し 1 0 1 0 0 1 0 1 2 3 4 5 6 7 8
取り出し 根から順に下に向かって辿る 節点のカウンタが 1 以上ならデクリメントして左へ辿る 節点のカウンタが 0 なら何もせず右へ辿る 以後繰り返し 1 0 1 0 0 1 0 1 2 3 4 5 6 7 8 発見!
取り出し操作同士の干渉 節のカウンタを共有しているため同一のアイテムに辿り着く事は起こりえない 1 つもアイテムが挿入されていない場合にこの図では 8 の葉に辿り着く。ここだけは特別に取り扱う必要があるが今回は省略 1 0 1 0 0 1 0 1 2 3 4 5 6 7 8
取り出しと挿入の衝突 1 に新規アイテム追加と同時に、取り出し処理も行うとする 1 0 1 0 0 1 0 1 2 3 4 5 6 7 8
取り出しと挿入の衝突 1 に新規アイテム追加と同時に、取り出し処理も行うとする 0 0 1 0 0 1 1 1 2 3 4 5 6 7 8 inc dec
取り出しと挿入の衝突 新規に追加した物はインクリメントしながら木を根に向かって辿る 0 1 1 0 0 1 1 1 2 3 4 5 6 7 8 inc
取り出しと挿入の衝突 挿入処理が根に達して居なくても、カウンタがインクリメントされているならば検索に反映される なので取り出し側のスレッドは挿入されたばかりのアイテムに向かってくれる 1 0 1 0 0 1 1 1 2 3 4 5 6 7 8 inc dec
取り出しと挿入の衝突 取り出しは他のスレッドが挿入処理の最中であってもカウンタの値を見てその瞬間での最良の判断をしてくれる 挿入処理が根に達すれば「節のカウンタはそれより左に有るアイテムの数を表す」というルールは満たされる 結論から言うと「上手くすれ違うから大丈夫」 1 0 1 0 0 1 0 1 2 3 4 5 6 7 8 dec
実装 木を配列で表現、データ実体も配列で保持 Priority Queue のように配列の 2 番目を根として木を表現。上へ行くときは 2 で割り、下へ行くときは 2 倍した後で、右なら 1 足す。左ならそのまま。 同じ場所に複数挿入できるよう、 X[n] は FIFO で実装 h[1] h[2] h[3] h[7] h[6] h[5] h[4] X[0] X[1] X[2] X[3] X[4] X[5] X[6] X[7]
実装(挿入処理) void push(const T& item){ int priority = item.GetPriority(); // 0 ~ 8 の値が出る items[priority].enque(item); //  対応するアイテムリストに挿入 int index = priority + MAX; //  木のインデックスを獲得 while(index > 1){   //  根に達するまで木を登る  if((index & 1) == 0) {//  左から登るのなら atomic_inc(&heap[index / 2]); // カウンタをインクリメント   } index /= 2; // 1 段上る } } 案外簡単!
実装(取り出し処理) T getMin(){ int index = 1; while(index < MAX){ //  末端に続くまで続ける int counter = heap[index]; if(counter > 0){ //  カウンタが大きいなら if(CAS(&heap[index], counter, counter-1)){ index = index / 2; //  左下の枝へ }// CAS 失敗したらカウンタの取得からやり直し }else{ index = index / 2 + 1; //  右下の枝へ } } return X[index].deque(); } 案外簡単!
まとめ Lockfree な PriorityQueue について解説 Priority の最大値に制限がある 制限が無いバージョンも有るらしいので調べます 挿入・取り出しがどんなタイミングでオーバーラップしてもデータ構造が壊れません

Lockfree Priority Queue