SlideShare a Scribd company logo
DSIRNLP
@kumagi
辻Lock-free
Lock-freeと発言した人に文脈を無視して
いきなり@を飛ばす行為
冬のLock free祭り safe
僕と一緒に
Lock-free!
CPUの系譜のおさらい

無限に続くかに思われたCPU加速戦争
周波数が勝手に上がるので「システムを高速
化して欲しいという案件は半年遊んでからマ
シン買い換えさせればいいや」とまで言われ
た
周波数加速はPentium終盤から雲行きが怪しくなる
AthlonX2やCore世代の台頭
AMD「周波数あげるの無理だからコア数増やそう
ぜ」
CPUの系譜のおさらい
CPUの製造コストのためチップ面積を節
約したい
尐ない面積で高い性能を出す方法はないか?
ポラックの法則
「2倍のシングルスレッド性能を得るためには
4倍のリソースが必要になる」というintelの中
の人の経験則
逆手に取れば「半分の性能で良ければ4個のコ
アを積める」
ポラックの法則
半分の性能のコアなら1/4の面積
¼の面積なら同じ面積に4個積める!
½の性能 × 4コア = 2倍の性能!!



                1/2 1/2
                1/2 1/2
それ活かしてパワフルなの作れば?



1/3の性能なら1/9の面積だから9個積める。
それなら合計で3倍のパワーが出る




既にあります
Intel Many Core
驚きの48コア搭載
最大消費電力125W
同規模のCPUの約3倍のエネルギー効率
今年の秋ごろから世界中の研究機関に試作品
が送られて使い方を研究してる
知ってる範囲だと東大と京大には納入されてる
なんで普及しないの?
We are the
     Bottleneck!
情報工学の発展が追い付いていない!

ヽ('ω')ノ三ヽ('ω')ノもうしわけねぇもうしわけ
ねぇ
マルチコアを使い倒して
スーパープログラマへ!!
そして
タダ飯の時代よ今再び!
マルチスレッドは簡単じゃない
同時に実行するということは、発生する実行の
組み合わせが指数的に爆発する
通常はロックを用いて調停を行う
そしてBlockingする
What is Blocking?

ビーチフラッグを例に



        CPU
What is Blocking?

排他すると     ロッ
          ク

                      OK!




               クリティカルセク
                 ション
What is Blocking?

クリティカルセクションは危険がいっぱ
い




              クリティカルセク
                ション
What is Blocking?

クリティカルセクションは危険がいっぱ
い




              クリティカルセク
                ション
What is Blocking?

 コアが増えるほどに問題が顕著に!
         早くしろよ…    早くしろよ…
早くしろよ…
                      早くしろよ…

早くしろよ…                   早くしろよ…

早くしろよ…
                      早くしろよ…
  早くしろよ…
           クリティカルセク
             ション
そこで Lock-free
    クリティカルセクションを
      作らないので
   他のスレッドを足止めしない!
Lockを用いないとどうなるか

CPU内部では処理は複数のステップで行われ
る

           1.xを読み出す
 ++x;      2.読んだ値に +1
           3.xを保存する
Lockを用いないとどうなるか

複数スレッドが同時に行うと
              x==1
 スレッド                      スレッド
   A                         B
1.xを読み出す(1)            1.xを読み出す(2)

2.読んだ値に +1             2.読んだ値に +1

3.xを保存する(1)            3.xを保存する(3)
                     OK!
              x==3
Lockを用いないとどうなるか

複数スレッドが同時に行うと破綻する場合が
        x==1
ある
 スレッド                 スレッド
   A                    B
1.xを読み出す(1)          1.xを読み出す(1)

2.読んだ値に +1           2.読んだ値に +1

3.xを保存する(2)          3.xを保存する(2)
                     数が合わない
              x==2
道具の紹介

「targetが期待通りの値だったら置換」を一
気に行う命令
以後ではCASと略します
 bool compare_and_swap(int* target, int expected, int newval){
   if(*target == expected){
       *target = newval;
       return true;
   }
   return false;
 }
CASを使ってみよう
    Lock-free共有カウンタの例
int x;
void add1(){
    int old,new;
    do{
          old = x;
          new = old+1;
                                  失敗してたらやり直す
    }while(!cas(&x, old, new));
}
CASを使ってみよう
CASのお陰で衝突しても破綻しない
                x==1
 スレッド                    スレッド
   A                       B
1. xを読み出す(1)            1. xを読み出す(1)
2. 読んだ値に +1
3. 値が1なら2へCAS           2. 読んだ値に +1
4. 失敗したので再挑戦
5. xを読み出す(2)            3. 値が1なら2へCAS
6. 読んだ値に +1
7. 値が2なら3へCAS          数が合う!
                x==3
冬のLock-free祭り

マルチスレッドReadyでありながらロックを用
いないデータ構造
話す予定のおしながき
 Lock-free Stack
 Lock-free Queue
 Lock-free List
 Lock-free Hashmap
 Lock-free SkipList
 Lock-free Btree
 Dynamic STM(Obstruction-free STM)
 ???
Lock-free Stack
Compare And Swapの正しい使い方
Lock-free Stack

                        ↓ポインタ
             A         Head

                 CAS

「Headが指している物を指したノードを作って
 CAS」
Lock-free Stack

           CAS
           CAS
            CAS Head
       A


 B    C       D



            失敗した!
Lock-free Stack


                 A   CAS Head
                        CAS
           B    C      D




また失敗した!
Lock-free Stack


       A             Head
           CAS
      C


 B               D
Lock-free Stackからpop


         A         Head
             CAS
         C

         D

         B
ね。簡単でしょ!
Lock-free怖くない!
いわゆるABA問題は今日は扱いません




どしどし行きます!
Lock-free QUEUE
  不変条件に手を加える
Lock-free Queue の Deque


      Tail        Head
Lock-free Queue の Deque


Deque操作は簡単
Lock-free Stackと同じ挙動なので
Lock-free Queue の Deque


      Tail           Head


             CAS
               CAS
Lock-free Queue の Enque
Enque操作
1. Enqueしたい要素eを用意する
2. 末尾のノードが指すポインタがeを指すようCAS
3. Tailポインタがeを指すようCAS


                      2ステップ必
                      要
Lock-free Queue の Enque


        Tail      Head
 CAS
  CAS
Lock-free Queue の Enque


             Tail           Head
       CAS
               構造が破綻
         CAS
CAS

  CPU1               CPU2
   1. 要素eを用意する        1. 要素eを用意する
   2. 末尾をCAS          2. 末尾をCAS
   3. TailポインタをCAS    3. TailポインタをCAS
不変条件を考える
アルゴリズムは必ず何かしらの前提が必要
マルチスレッドプログラムでもそれは同じ
常にその条件を満たし続けるのは無理→ロッ
ク

   条件守ってる!       条件守ってる!

   時間軸→
          条件守ってない!
不変条件を守る
  他のCPUが思いもよらない変更を加えてくる

               危険な瞬間

CPU1

CPU2

CPU3

CPU4
       時間軸→
不変条件を守る
  ロックを用いて排他する



CPU1

CPU2

CPU3

CPU4
       時間軸→
不変条件を守る
  ロックのおかげで危険な瞬間がなくなる



CPU1

CPU2

CPU3

CPU4
       時間軸→
ロックの目的と効果
普通にプログラムを書くとどうしても不変条
件を破壊する瞬間が生まれてしまう
そこはロックで守るのが普通
    void queue::enque(const T& v){          ↓時間軸
      node* const new_node = new node(v);
      node* const tail = que_tail_;
      tail->next = new_node;
      que_tail = new_node;
    }


                                            危ない
ロックの目的と効果
  Lock-free Stackは不変条件を満たさない瞬間
  を外部から観測されないように最小化して
  CASで片づけられた

CPU1               CAS         CAS


CPU2                     CAS               CAS


CPU3         CAS



CPU4   CAS                           CAS
ロックの目的と効果
  Lock-free Queueは2度CASが要るのでタイミ
  ングによって危険
                危険な瞬間

CPU1               CAS     CAS   CAS   CAS


CPU2                     CAS     CAS          CAS    CAS


CPU3         CAS   CAS



CPU4   CAS   CAS                        CAS    CAS
Lock-free Queueはどうするのか
  不変条件
  Headがキューの先頭を指す
  Tailがキューの末尾を指す
  Tail以外のノードは必ず有効なノードを指す



                   条件を緩め
  不変条件
                   る
  Headがキューの先頭を指す
  Tailがキューの末尾を指さないかもしれない
  Tail以外のノードは必ず有効なノードを指す
Tailが末尾を指さないとは?

末尾を指してる
      Tail   Head




末尾を指してない
      Tail   Head
解決
「Tailが遅れてたらみんな手伝ってあげてね」
「Tailの更新に失敗してもみんな気にしないで
ね」
             Tail                  Head

       CAS
          破綻しない!
        CAS


 CPU1                  CPU2
  1. 要素eを用意する
                           1.   要素eを用意する
  2. Tailが遅れてたら進める
                           2.   Tailが遅れてたら進める
  3. 末尾をCAS
                           3.   末尾をCAS
  4. TailポインタをCAS(失
                           4.   TailポインタをCAS
      敗)
Lock-free Queueかっこいい!

   EnqueとDequeの共同作業!
                                                    T deque(){
                                                      while(1){
                                                        const Node* const first = mHead;
void enque(const T& v){                                 const Node* const last = mTail;
  const Node* const node = new Node(v);                 const Node* const next = first->mNext;
  while(1){                                             if(first != mHead){ continue;}
     const Node* const last = mTail;                    if(first == last){
     const Node* const next = last->mNext;                  if(next == NULL){
     if(last != mTail){ continue; }                             while(mHead->mNext ==
     if(next == NULL){                              NULL){ usleep(1); }
         if(compare_and_set(&last-                              continue;
>mNext, next, node)){                                       }
             compare_and_set(&mTail, last, node);           compare_and_set(&mTail,last,next);
             return;                                    }else{
         }                                                  T result = next->mValue;
     }else{                                                 if(compare_and_set(&mHead, first, next)){
         compare_and_set(&mTail, last, next);                   delete first;
     }                                                          return result;
  }                                                         }
}                                                       }
                                                      }
Lock-free Queueかっこいい!

   EnqueとDequeの共同作業!
                                                    T deque(){
                                                      while(1){
                                                        const Node* const first = mHead;
void enque(const T& v){                                 const Node* const last = mTail;
  const Node* const node = new Node(v);                 const Node* const next = first->mNext;
  while(1){                                             if(first != mHead){ continue;}
     const Node* const last = mTail;                    if(first == last){
     const Node* const next = last->mNext;                  if(next == NULL){
     if(last != mTail){ continue; }                             while(mHead->mNext ==
     if(next == NULL){                              NULL){ usleep(1); }
         if(compare_and_set(&last->mNext, next,                 continue;
node)){                                                     }
             compare_and_set(&mTail, last, node);           compare_and_set(&mTail,last,next);
             return;                                    }else{
         }                                                  T result = next->mValue;
     }else{                                                 if(compare_and_set(&mHead, first, next)){
         compare_and_set(&mTail, last, next);                   delete first;
     }                                                          return result;
  }                                                         }
}                                                       }
                                                      }
閑話休題
Lock-freeよくある質問と答え
そんな複雑な事しなくても
Message-Passingなら完璧だよ
MessagePassing

「メッセージを渡す」という哲学に基づいて、
操作の実行順序をメッセージ順にしてしまう
こと
ですよね?

 Deq Enq Deq Deq Enq   Queue
Lock-free

例えるならこんな感じ!

          Deq

          Enq

          Deq      Queue
          Deq

          Enq
どういうこと?

スケールする!
 FASTER




          Single-lock


                        Lock-free
あなたも一緒に




Lock-free!
発展話題
今のアルゴリズムで実装されてるのが
boost.lockfreeのqueue
リソース管理がもう尐し複雑
IntelのTBBやらFastFlowとかいうライブラリ
でもこのlock-free queueだったような…
このアルゴリズムは発明者の名前を取って
MS-Queueと呼ぶ
Lock-free List
ポインタに情報を詰め込め
ConcurrentList
複数のスレッドから同時に読み書きできるList


いろんな粒度のロックがあるので順番に紹介
粗粒度ロック

全体を排他するだけ
もちろんスケールしない


 Head
悲観的ロック

ノードごとにロックを用意
 ロック・アンロックしながら「歩く」


  Head




• かなり遅くなる
• 他のスレッドのイテレーションを追い越せない
楽観的ロック

必要になるまでロックを取らない



 Head




        これでいいんじゃない?
楽観的ロック

だめなんです
                 ロックを2つ取っ
    私の安定性…低すぎ…     おりゃ!
                     て
                 これ消すぞー!

 Head      Segmentation Fault




           これ消すぞー
            消したぞー
楽観的ロック
ロックした後に、リストの先頭からきちんと
到達可能であることを確かめてから消す必要
がある
つまり安全のためリストを2回舐めることになる
 Head
つまり

単一ロックだとスケールしない


細粒度ロックだと
悲観的にロックすると遅い
楽観的にロックすると二度見のコストがつく
 Lazyな消去を導入する事で改善可能だけど省略
さぁLock-freeだ
挿入処理

• リストの繋ぎ替え処理にCASを使用して衝突を回避
  し、検索の邪魔をしない
• 道路封鎖せず道路工事してる感じ



   CAS
挿入処理
CASを使う事によって、同一の場所に同時に
複数の挿入が発生しても


      大丈夫!
CAS
CAS

CAS     失敗した!
削除処理
挿入処理と同様に、ポインタをCASで繋ぎ変
える
     CAS



追い出しに成功したスレッドがdelete
このアルゴリズムにはミスがある
問題が
連続したノードを同時に削除しようとすると
データ構造が破壊される

     CAS   CAS


     Segmentation Fault
問題が

削除と挿入がぶつかっても破壊される

         CAS


           CAS
      Memory Leak
解決策

ノードの削除を「マーキング」「ポインタ繋
ぎかえ」の2段階に分ける
マーキングされてたら誰でも削除を手伝う
ポインタ繋ぎかえに失敗しても気にしない


マーキング前と後の2状態に対して操作を記
述
マーキングはどこに?

ポインタの1bit目をマーキング対象に
 動的確保したメモリは4の倍数ぐらいにはAlignされてる
 つまり1bit目は通常0
 マーキング処理もCASを用いる      ココ!
 順序づけ大事

なぜそんなところに埋め込むの?
1回のCASで1word分しか操作できない
何とかして削除マークとポインタをAtomicにした
い
正しい削除手順

2回CASを使う



           CAS   CAS
さっきのはどうなるか



 CAS    CAS CAS
CAS    CAS


        マーキングにより
         CAS失敗する
さっきのはどうなるか



      CAS


       CAS
            マーキングにより
CAS          CAS失敗する
素晴らしい!!
Lock-free Hashmap
バケットがアイテムの間を動く
Hashmap便利ですよね

いわゆる連想配列
O(1)でアクセスできるみんなの人気者
細粒度Lock Hashmap

        0
        1
        2
        3
        4
        5
        6
        7



全部のバケットにロックを取り付けてやる方
法
実はあんまり効率よくない
Striped Lock Hashmap

            0
            1
            2
            3
            4
            5
            6
            7


固定数のLockをmoduloでバケットに割り当てる
スレッドの数が膨大にならない限りロックそのものが
増えても恩恵がないため
バケットごとに対応するロックが縞模様になるので
Striped
java.util.concurrent.ConcurrentHashMap

                  0
                  1
                  2
                  3
                  4
                  5
                  6
                  7
               volatile   final
基本的にStriped Lockだけど、成功する検索は
wait-free
失敗する検索はロックを取ってもう一回行う
削除はチェインを部分的にまるっとRCU
バケットの拡張は再帰的にロックを全て獲得しながら
Lock free?
Lockの無くし方を考える

           0
           1
           2
           3
           4
           5
           6
           7

線形リスト部分はLock-free Listを使えばいい
バケット部分の拡張が鬼門
どうするバケット拡張
あらかじめ充分巨大なバケットにして
しまう
                      0
富豪的過ぎて実用的でない          1
                      2
CASでバケット列を複製      0   3
                  1   4
複製作業中に挿入・削除が走る    2   5
                  3   6
 一貫性無理☆           4   7
                  5
                  6
                  7
                  8
                  9
                 10
                 11
バケットの拡張が
   困難
そうだ


バケットの中身の移動が必要ない設計になれ
ばいいんだ!
全体図


内部を昇順のLock-free List一本で集約
番兵ノード          00000010        00000001        00000011
データノード             ↓               ↓               ↓
               01000000        10000000        11000000

     00   02   40   48    6d   7f   80   8a   c0   74


 0
 1                  Hashmapのインデックス値を逆順にしてリ
 2                  ストに投入
 3
どういうこと?
普通はバケットにデータを吊るしてる
 0         02
 1                        48         6d        7f
 2         8a
 3                    74




こっちはデータの間にバケットの目印を吊る
す
     00        02          1本のList
 0        40         48         6d        7f
 1
 2             80         8a
 3
                c0         74
データの挿入
1. 対象となるデータのハッシュ値を算出→2
2. ハッシュ値に対応するテーブルにアクセス
3. テーブルに番兵ノードへのポインタが書いてあるた
   め対応するノードへジャンプ
4. 番兵ノードの指すポインタをたどっていけばHash値
   の昇順にデータが並んでいるため、対応する場所に
   挿入
5. リストが昇順に並んでいるという前提は崩れない!
      00 02 40 48 6d 7f 80 8a c0 74


     0
     1
69   2
     3
テーブルの拡張
    番兵ノードの間に挟まるアイテムの数が一定数を超
    えた場合にテーブルを拡張する
    左のテーブルは工夫してあり、基本的に初期化以降
    immutable
    詳細な部分は論文で

     00   02   40   48   6d   7f   80   8a   c0   74


0
1
2
3
4
5
6
7
テーブルの拡張
         1.    拡張操作はテーブルを大きくするだけでおしまい(CASでも可
               能)
         2.    テーブル拡張後は新規探索は新しいテーブル上で行う
         3.    テーブルが未初期化ならその一個左の番兵ノードから探索
         4.    番兵ノードが挿入されているべき個所を見つけ次第、番兵
               ノードを挿入する
         5.    目的の場所を見つけたら挿入
         6.    リストが昇順に並んでいるという前提は崩れない
          00    02   40   48   60   6d   69   7f   80   8a   c0   74

     0
     1
     2
     3
     4
     5
62   6
     7
ロックなしで並行動作する!


何があっても

リストが昇順に並んでいるという前提は
       崩れない
   挿入と拡張が同時に走っても
   削除と挿入が同時に走っても
       大丈夫!
FAST   驚異的なスケーラビリティ
Lock-free



        java.util.concurrent.ConcurrentHashMap
FAST




       性能負けてる!!!




       スレッド数
Lock-free Hashmapかわいい!
Lock-free SkipList




線形化ポイントはどこ?
SkipList
ご存じ・・・ですよね?
SkipList

順序関係のあるデータをO(log n)で検索・挿
入・削除ができるデータ構造
 2分木とモロ被り

乱数に依存するため木と違いリバランスする
必要が無い
 平衡2分木を平衡に扱うにはリバランス専用のスレッドを
 別に立てる方法が現時点で一番高いスループットの出る
 方法だったような…

LevelDBの中で使われていてちょっと話題に
SkipList

昇順に並べた普通のリスト
に高レベルなリストを加えたもの
レベル1上がる毎に1/2のノードが間引かれる
-∞                    33         ∞
-∞           12       33         ∞
-∞           12       33    64   ∞
-∞   3       12       33 51 64   ∞
-∞   3   8   12 16    33 51 64   ∞
-∞   3   8   12 16 29 33 51 64   ∞
SkipListでの検索

高いレベルから順に辿っていくだけ
 新幹線・特急・快速・鈍行と乗り換えるイメージ
29どこー?
-∞                       33         ∞
-∞           12          33         ∞
-∞           12          33    64   ∞
-∞   3       12          33 51 64
                     あったー!          ∞
-∞   3   8   12   16     33 51 64   ∞
-∞   3   8   12   16 29 33 51 64    ∞
これをどうLock-freeにするの?



まずはLock-basedなSkip Listの実装を見ま
しょう
Lock-based Skip List

ノードごとにロックを用意
Lock-based Skip List
1.   ここに挿入したいとする
2.   そこ以前のリンクの状態を記憶
3.   昇順にロックを獲得
4.   2の時に記憶した内容と現在の内容が一致していることを確認
5.   全部一致したら下から順にリンクを繋ぎ変えていく
Lock-based Skip List
できました!
実際は線形化ポイントを確定するため
FullyLinkedビットを用いて線形化します
 詳しくはgithub.com/kumagi/sl
さあ



Lock-freeだ
具体的な戦略
Lock-free Listと同様に、ポインタに削除bitを導入
一番下のリストへの操作をSkipListへの操作と見なす
それ以上のリストは高速化のための「飾り」
Lock-free SkipListへの挿入

1. 最下位リストへの挿入を行う
2. 下のレベルから順に上位リストのリンクを繋ぎ変える
3. 繋ぎ変える途中で他人に書き換えられてたら前後の情報を
   再確認して繋ぎ変えを続行
Lock-free SkipListへの挿入

1. 最下位リストへの挿入を行う
2. 下のレベルから順に上位リストのリンクを繋ぎ変える
3. 繋ぎ変える途中で他人に書き換えられてたら前後の情報を
   再確認して繋ぎ変えを続行
Lock-free SkipListでの削除

1. 対象物の前後の関係を洗い出す
2. 上位ノードから順番に削除マークを振って
   いく
3. 上位ノードから順に追い出していく
そんなので平気なの?




詳しく追ってみましょう
挿入・挿入の衝突



          やりなおすだけ
           割り込んだ




最下位レベルで割り込まれた場合は初めからやり直す
通常のLock-free Listと同様の作法
挿入・挿入の衝突



      割り込んだ




最下位以外で割りこまれたらそこから再開
最下位以外は厳密に一貫している必要はないので
OK
挿入・削除の衝突


     削除開始




削除はLock-free Listと同じくマーキングによってCASが失
敗する仕組みになっている
そしてやり直すだけ
ポイント
削除・挿入ともに「一番下のリストへの操作
の成功・失敗」を全体の成功・失敗と見なす
挿入は下のリストから。削除は上のリストか
ら。
リストのイテレートが上のレベルからなので衝突
した際に複雑な手続きにならないようにした配慮
Java.util.concurrentのファミリーに居る
クレイジーな人はコードを読んでみよう☆
DougLea先生…
Lock-free Btree
恐るるに足らず!
みんな大好きBtree
                          18



           16                       28




   5   7        13   16        22   25   33




HashMapと並ぶデータベースの基礎技術
O(log n)のアクセス速度で大量のデータに向
く
普通のBtreeの挿入操作
                               26
                          18



           16                        25
                                     28   28


                                     足りない!
   5   7        13   16   22    22   25 26     33


                                       Split

ノードに余裕がなくなった時にSplit操作を行う
余裕のないノードしかSplit操作は行われない
削除はノードの中身が半分を割った時にmerge操
作
Lock-free!
Lock-free化
         Root    CAS
                複製
                       複製

                            複製


                ここに挿入したい
必要なデータをすべて複製してしまえばいい
Lock-free化
            Root




                     Split




必要なデータをすべて複製してしまえばいい
SplitやMergeも全く同様。部分木を丸ごと作り直す
Lock-free化
            Root    割り込まれたので
割り込んだ別スレッ          CAS失敗→初めから
    ド                 やりなおし




必要なデータをすべて複製してしまえばいい
SplitやMergeも全く同様。部分木を丸ごと作り直す
衝突したらそのスレッドは初めからやり直し
すべての操作はRootに対するCASで直列化
超☆遅い
まともなアルゴリズムが他にあるはずなのでまた調べてきま
す…。
話題
BtreeがLockFreeになって喜ぶ人は案外尐ない
Btreeの部分ロックをDBの論理的競合解決に使って
る
ロックがなくなったら上のレイヤーで競合解決し
ないといけない
 たいてい性能が出ないとかなんとか…
Dynamic Software Transactional
          Memory
          何千箇所でもCASだけで同時に!
Lock-freeはすごいけど・・・

CASだけで操作するのは疲れたよ・・・


複数個所を一気に更新できたら簡単なの
に・・・
Lock-freeには限界がある
Lock-freeなデータ構造は単体で扱う分に
は頑健で強固でスケーラブル!



  でも、あなたが欲しかったのは
    本当にそれでしょうか?
あなたが本当に欲しかった物

トイレのカギを考えましょう



      この姿じゃ男子トイレ
       から出れない…!
あなたが本当に欲しかった物
今をときめく男の娘に必要なのは
      「堅牢なだけのトイレの鍵ではない
…!」


いくらデータ構造が強固でスケーラブルに
なっても実地で使えなければ意味がない…!
「Composabilityがない」とも言う
コードを使いまわせないのはソフトウェア工学の
         世の中のすべてのロックを
敗北       生まれる前に消し去りたい
         過去と未来のすべてのロックをこの手で
そこでSoftwareTransactionalMemory

複数の操作をSerializableな分離レベルで実行
1ワードのCASのみを使う
単一ロックよりもスケールする




   タダ飯の時代の再来!?
SoftwareTransactionalMemory

外部からこのように見える場合を想定



 A



 B
SoftwareTransactionalMemory

内部ではこのように扱う



 A      Old   New   Owner   Commit




 B      Old   New   Owner    Abort
SoftwareTransactionalMemory

どういう意味か



 A      Old   New   Owner   Commit
                               スレッド
                               スレッドの
                               のステー
                               ステータス
                                タス


 B      Old   New   Owner    Abort
SoftwareTransactionalMemory

ステータスは
  Commit    (既に作業を終えた)
  Abort    (一時的に作業を止めた)
  Active (現在作業中である)         Commit
 のどれかの状態をとる                       スレッド
                                  スレッドの
                                  のステー
Commit状態ならNew                     ステータス
                                   タス
  Abort状態ならOld
               )を読みだす!
                                Abort

          Active状態なら競合解決(後述)へ
SoftwareTransactionalMemory

つまり
                        これらが最新の値

 A      Old   New   Owner   Commit




 B      Old   New   Owner    Abort
SoftwareTransactionalMemory
A,Bの2つを一気に更新する事例



 A                  Commit




 B                  Abort
SoftwareTransactionalMemory
自分のステータスを保存         Active



 A                  Commit




 B                  Abort
SoftwareTransactionalMemory
                   Active

CAS

A                  Commit


CAS
B                  Abort
SoftwareTransactionalMemory
                       CAS
                   Commit
                   Active



A
                    最新の値



B
邪魔が入る場合

              Active



A
                   B欲しい…

               Active
B
邪魔が入る場合

                 Active
                 Abort



A                         CAS
             最新の値

                  Active
B

    Bを獲得!
どう凄いのか
A
        複数個所の最新情報
B       がCAS1回で変わる

C        Commit
         Active
          Abort

D

E
/人◕ ‿‿ ◕人\

「その壮大過ぎる祈りを叶えた対価に、
STMが背負うことになる呪いの量が分かる
かい?」
The Art of
Multiprocessor
Programming
読みましょう!
略称TAoMP本
Transaction on Key Value Store
       僕の修士論文(予定)
研究背景
Webサービスを支えるためにスケールアウト
可能なデータストアであるキーバリュースト
アが広く利用されている
memcached, flare, cassandra
しかしキーバリューストアは一度に一つの
キーバリューペアにしかアクセスできない

複数のクライアントから並行してアクセスす
る際に一貫性を保てない
キーバリューストアでのCAS

すべてのキーバリューペアがバリュー値と
共にuniq値を持っていて、書き換え時に更
新される
1. Getsコマンドでuniq値を獲得
2. CASコマンドでuniq値を指定しながら保存
3. uniq値が一致した場合に限り保存が成功
   成功なら1~2間で値が更新されていないこと
アトミックな操作ができる
   が保証される
キーバリューストア上でのロック

並行制御を実現するための一番素直な実装
ロックを保持するキーバリューペアを用意
キーバリューストアのCASコマンドで可能

     キー    バリュー
     K      V
ロックを実装すると

上手く動きそうに見える


キー    バ       lock(a);
 a   リュー      lock(b);
              set(a, 1);
              set(b, 2);
 b            unlock(a);
              unlock(b);
ロックを実装すると

クライアントが離脱すると破綻する
タイムアウトでアンロックしてもデータが一貫
しない      永久にロッ lock(a);
キー    バ    ク
 a   リュー されたまま lock(b);
                set(a, 1);
           離脱   set(b, 2);
 b              unlock(a);
                unlock(b);
提案
キーバリューストア上にトランザクションを実
装
キーバリューストアには手を加えず、クライアント
ライブラリとして実現
 基本的性能や障害耐性はキーバリューストアの実装に依存
 サーバが落ちても大丈夫な分散KVSなら信頼性も安心
クライアントが離脱しても大丈夫
トランザクションを用いて一貫性を保つ

        トランザクション
        キーバリュースト
           ア
基本方針

Dynamic STMを応用
メモリの間接参照の代わりに、キーバリュース
トアに間接化を導入
トランザクショナルキーバリューペアという
データ構造を構築
キーバリューストアでの間接化
キーは文字列なのでバリューの中に挿入でき
る    キー     バリュー
        A       B
        B       C


    A       B       C
複数のキーを格納

複数のキーを格納する事もできる
   キー     バ
          リュー
   A     ab|cd|ef

   ab    value1
   cd    value2
   ef    value3
複数のキーを保持

以後はこのように図示


   A        ab cd ef
   value1
        value2
              value3
提案手法

ソフトウェアトランザクショナルメモリの
一つであるDynamic STMを応用
メモリの間接参照の代わりに、キーバリュース
トアに間接化を導入
トランザクショナルキーバリューペアという
データ構造を構築
トランザクションナルキーバリューペア
        (TKVP)
   通常のキーバリューペア(KVP)をトランザ
    クションのために拡張したもの

          key               value
                                         トランザクション
           古い値            新しい値             ステータス
                          oMel1d
                Sde93A             8Yu4naplE2
    key         old new status
                u40F...
                          iLdsa.
                            ..
                                   saFASFD...


                                                activ
          value value’
                                                  e
トランザクショナルキーバリューペ
       ア
statusの値によって読み出し方を分岐
 commited:Newの値を読み出す
 abort:Oldの値を読み出す
 active:競合を解決する(後述)



 key      old new status
                     activ
       value value’ commited
                     abort
                       e
トランザクショナルキーバリューペア
       (TKVP)
キーバリューストアにはこのように保存さ
れる
                 バ
   キー
                 リュー
                Sde93Au40F...
   key           oMel1diLdsa...
                8Yu4naplE2saFAS
                      FD…
   Sde93A
   u40F...       value old
    oMel1d
    iLdsa.
      ..
                value’new
   8Yu4naplE2    activ status
   saFASFD...
                   e
トランザクショナルキーバリューペ
       ア

 key      old new status

       value value’ commited


                 commited
            省略して表記
トランザクションの実例

トランザクションを用いて

 Transaction{
    set(a, 1);   // a=1
    set(b, 2);   // b=2
    set(c, 3);   // c=3
 }
を実現する
トランザクションの概観
ステータスを初期化    初期化


必要なTKVPを更新
             更新


ステータスをコミッ
             コミット
トへ                   no
             成功?
               yes
             終了
初期化
自身のステータスをActiveとしてキーバ
リューストアに新たに保存する
この際のキーはランダムな文字列を生成す
る   Transaction{
       set(a, 1); // a=1
       set(b, 2); // b=2
       set(c, 3); // c=3
    }
     set(hoge,active)
                activ
    hoge
トランザクションの概観
ステータスを初期化    初期化


必要なTKVPを更新
             更新


ステータスをコミッ
             コミット
トへ                   no
             成功?
               yes
             終了
TKVPの最新の値(おさらい)
 Statusの値によって最新の値が変わる


   a              commited
       4    8
           commitedならa→8
   a              abort
       4    8
最新の値
           abortならa→4
更新操作
    TKVPの所有権を奪って                 Transaction{
      oldが「これまでの最新の値」               set(a, 1); // a=1
      newが「これからの最新の値」               set(b, 2); // b=2
     をそれぞれ指すよう書き換える                 set(c, 3); // c=3
                                 }
   commitedなら            CAS     commited
        a         old new aaaa
                          hoge   set(fEe09d, 1);
                                 cas(a,
              4      8     1     [old,fEe09d,hoge])
                                 active
                                          初期化時に保存
   abortなら               CAS    abort    したステータス
        a         old new aaaa
                          hoge   set(fEe09d, 1);
                                 cas(a,
              4      8     1     [old,fEe09d,hoge])
                                 active
複数のTKVP更新
同一のトランザクションステータスを指す
よう複数のTKVPを書き換える Transaction{
                           set(a, 1); // a=1
                           set(b, 2); // b=2
                1          set(c, 3); // c=3
    a                  }
                hoge
        CAS     2
    b                      active
               hoge
        CAS     3
    c
               hoge
トランザクションの概観
   ステータスを初期     初期化
    化


   必要なTKVPを更新   更新


                 コミット
   ステータスをコ
    ミットへ                 no
                 成功?
                   yes
                 終了
コミット

ステータスがactiveであることを確認して
commitedへ書き換える
   Transaction{
      set(a, 1); // a=1
      set(b, 2); // b=2
      set(c, 3); // c=3
   }
    cas(hoge, commited);
                    Activ
    hoge            commited
              CAS
                      e
コミット
  書き換えるステータスは必ずキーバリュー
  ペア一つ
  一斉にnewの指している物が最新になる
              1
       a
              hoge
                           CAS
個数制限          2       Activ
                     commited
 なし    b                e
              hoge
              3
       c             最新の値
              hoge
トランザクションの概観

ステータスを初   初期化
期化

          更新
必要なKVPを
オープンしなが
ら更新
          コミット
                  no
ステータスをコ   成功?
ミット状態へ      yes
          終了
アボート
コミット時にステータスがabortに書き換えられ
ていたらコミット失敗。トランザクションを始
めからやり直す
abortに書き変わっているというのはどういう状
況か


    hoge      abort
              commited
トランザクショナルキーバリューペ
       ア
ステータスの値によって読み出し方を分岐
 commited:Newの値を読み出す
 abort:Oldの値を読み出す
 active:競合を解決する



 key     old new status
                       activ
       value value’
                         e
競合する場合を考える


 トランザクション
       hoge
Transaction{
   set(a, 1);
   set(b, 2);     トランザクション
                     fuga
                Transaction{
                   set(b, 999);
TKVPの所有者がactiveだった場合

所有者のstatusをabortへ書き換える
                                トランザクション
TKVPを奪う                            fuga
                              Transaction{
       a         hoge            set(b, 999);
             8   1
 aはロール                      CAS
                                cas(hoge,abort)
 バック b               fuga
                     hoge       active リトライ
                                abort
       CAS                          b欲し
                                    bを獲得
                                     い
             7   2      999
最新の値                              active 続行
クライアントが離脱した場合

トランザクション途中で故障などによって
離脱したらいずれ誰かからabortされる。


             CAS
                   離脱
             active
              abort
性能測定中…
問題点
ゴミが増え続ける
もともとDynamicSTMはGC前提    ゴミ
                      active
                      commited
     a                active
                      commited

                      active
                      commited
    b
                      active
    c
解決策
双方向化により参照カウンタGC
ここ実装途中
github.com/kumagi/kvtx2   active
                          commited
        a                 active
                          commited

                          active
                          commited
       b
                          active
       c
Future Work
強い一貫性を実現できるのでKVS上にイン
デックス用にBtreeを構成しても大丈夫
トランザクションと範囲検索ができればSQLだっ
て捌けるかも
CassandraなどのEventual Consistentな環境
でもクライアント側の論理クロックを付与す
ればいけそう


まだ絵に描いた餅ですが…
まとめ
Lock-free Stack, Queue, List, Hashmap,
SkipList, BtreeとDynamicSTMのアルゴリズム
を解説
 腑に落ちない所は懇親会で
 実装の具体例は僕のGithubや懇親会で
 ブログ記事で読みたいネタがあったら懇親会で
分散環境に応用しようと取り組んでます

More Related Content

冬のLock free祭り safe