LevelDB 中的跳表實(shí)現(xiàn)

何為跳表

跳躍表(skiplist),簡(jiǎn)稱「跳表」备典。是一種在鏈表基礎(chǔ)上進(jìn)行優(yōu)化的數(shù)據(jù)結(jié)構(gòu),最早由 William Pugh 在論文《Skip Lists: A Probabilistic Alternative to Balanced Trees》中提出。

William Pugh 于 1989 在論文中將「跳表」定位為:一種能夠替代平衡樹的數(shù)據(jù)結(jié)構(gòu)膏秫,比起使用強(qiáng)制平衡算法的各種平衡樹,跳表采用一種隨機(jī)平衡的策略做盅。因此跳表擁有更為簡(jiǎn)單的算法實(shí)現(xiàn)缤削,同時(shí)擁有與平衡樹相媲美的時(shí)間復(fù)雜度(logn 級(jí)別)和操作效率。

設(shè)計(jì)思想

普通鏈表是一種順序查找的數(shù)據(jù)結(jié)構(gòu)吹榴。即在查找某個(gè)元素時(shí)需要依次遍歷所有元素進(jìn)行比較亭敢。且在元素有序的情況下也無法像數(shù)組那樣利用二分查找,固元素查詢時(shí)間復(fù)雜度為 O(n)图筹,若需維護(hù)鏈表有序吨拗,元素插入的時(shí)間復(fù)雜度也同樣需要 O(n)

在一些數(shù)據(jù)量極大的場(chǎng)景下婿斥,O(n) 的時(shí)間復(fù)雜度仍然存在優(yōu)化的空間劝篷。

平衡二叉查找樹 AVL 或者紅黑樹是經(jīng)常被用來實(shí)現(xiàn)上述優(yōu)化的數(shù)據(jù)結(jié)構(gòu)。但這些平衡樹需要在整個(gè)過程中保持樹的平衡民宿,這帶來了一定的復(fù)雜度娇妓。

而跳表的核心思想類似于對(duì)有序鏈表中元素建立索引。整個(gè)跳表按照層次進(jìn)行構(gòu)建活鹰,底層為原始的有序鏈表哈恰,然后抽取鏈表中的關(guān)鍵元素到上一層作為索引,從剛構(gòu)建的索引中可以繼續(xù)抽取關(guān)鍵元素到新的一層志群,以此類推着绷。

跳表原理結(jié)構(gòu)如下圖所示:

normal_skip_list.png

以查找元素 2 為例,查找將從第 2 層索引確定 1 ~ 5 范圍锌云,再到第 1 層索引進(jìn)一步確定 1 ~ 3 范圍荠医,最后回到底層原始鏈表查找到元素 2。

性能分析

上文提到了「抽取每一層的關(guān)鍵元素到上一層作為索引」,但是隨著數(shù)據(jù)量的增加每一層的結(jié)點(diǎn)也會(huì)越來越多彬向,該如何選取結(jié)點(diǎn)讓跳表的結(jié)點(diǎn)呈現(xiàn)我們所需的分布兼贡?

跳表采用「投硬幣」的方式?jīng)Q定結(jié)點(diǎn)是否向上提升。

假設(shè)現(xiàn)在底層有序鏈表有 n 個(gè)結(jié)點(diǎn)且投硬幣概率為 50%娃胆,則第一層應(yīng)該[1]有 n/2 個(gè)索引結(jié)點(diǎn)遍希,第二層有 n/4 個(gè)索引結(jié)點(diǎn),第 i 層索引有 n/2i 個(gè)結(jié)點(diǎn)里烦,當(dāng) n/2i 等于 2 時(shí)凿蒜,意味著已經(jīng)到了最高層。此時(shí)由 n/2i = 2胁黑,可推導(dǎo)出 i = log2n废封。

即投硬幣概率為 50% 時(shí),跳表層高為 log2n别厘,且由于每?jī)蓚€(gè)結(jié)點(diǎn)就有一個(gè)結(jié)點(diǎn)上升為一個(gè)索引結(jié)點(diǎn)虱饿。所以當(dāng)從最上層向下搜索的過程中,每一個(gè)最多只會(huì)比較 3 個(gè)結(jié)點(diǎn)(常數(shù)級(jí))触趴,所以整個(gè)搜索過程時(shí)間復(fù)雜度最終為 log2n氮发。

[1] 概率事件,并非一定具有準(zhǔn)確的 n/2 結(jié)點(diǎn)冗懦。

將上述過程進(jìn)一步擴(kuò)展概率為 p爽冕,則時(shí)間復(fù)雜度為 log1/pn。其中每一層比較的次數(shù)不超過 1/p披蕉。

上述是為了方便理解而簡(jiǎn)化的概率推導(dǎo)過程颈畸,結(jié)論也建立在 n 足夠大的前提下。實(shí)際推導(dǎo)過程要復(fù)雜很多没讲,有興趣的讀者可以閱讀論文原文:《Skip Lists: A Probabilistic Alternative to Balanced Trees》

實(shí)現(xiàn)

上文介紹了跳表的基本思想眯娱,其中為了方便理解和講述,我們將索引結(jié)點(diǎn)單獨(dú)繪制成一個(gè)結(jié)點(diǎn)爬凑。如果完全按照上文圖示實(shí)現(xiàn)跳表徙缴,則跳表需要額外 n 個(gè)結(jié)點(diǎn)空間。但在實(shí)際實(shí)現(xiàn)時(shí)嘁信,無需額外結(jié)點(diǎn)只需使用指針指向相應(yīng)結(jié)點(diǎn)即可于样,因此只是多出了 n 個(gè)指針而已。

即跳表實(shí)際實(shí)現(xiàn)的結(jié)構(gòu)如下圖所示:

skip_list_wiki.png

其中黃色格子為數(shù)據(jù)結(jié)點(diǎn)潘靖,白色格子為數(shù)據(jù)結(jié)點(diǎn)內(nèi)的指針穿剖。

LevelDB 中的跳表源碼解析

我們以 LevelDB 中的跳表實(shí)現(xiàn) skiplist.h 為例,分析跳表的具體實(shí)現(xiàn)

設(shè)計(jì)結(jié)點(diǎn)結(jié)構(gòu)如下:

template <typename Key, class Comparator>
struct SkipList<Key, Comparator>::Node {
  explicit Node(const Key& k) : key(k) {}
  // 存儲(chǔ) key
  Key const key;

  // ...... 

  private:
    // 下標(biāo)表示結(jié)點(diǎn)的層次 level
    // 整個(gè)數(shù)組表示該結(jié)點(diǎn)在各層次的存儲(chǔ)情況
    std::atomic<Node*> next_[1];
}

如下圖所示:

data_structure_0.png

進(jìn)一步理解如下圖所示:


data_structure_1.png

其中上圖 head_ 內(nèi)的 next_ 數(shù)組存儲(chǔ)著指向各個(gè)索引層次第一個(gè)元素的指針卦溢。

其它每個(gè)結(jié)點(diǎn)(如圖中的結(jié)點(diǎn) 1)中的 next_ 數(shù)組包含了如下信息:

結(jié)點(diǎn)在各個(gè)索引層中的下一個(gè)結(jié)點(diǎn)的指針

查詢?cè)?/h4>

元素查詢主要邏輯集中在 FindGreaterOrEqual 這個(gè)函數(shù)糊余,就以這個(gè)函數(shù)為例秀又,體現(xiàn)元素查詢過程:

// 搜索大于等于 key 的所有結(jié)點(diǎn)
template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node*
SkipList<Key, Comparator>::FindGreaterOrEqual(const Key& key,
                                              Node** prev) const {
  Node* x = head_;
  // 獲取當(dāng)前結(jié)點(diǎn)的層高
  // 從最上層的索引層開始遍歷
  int level = GetMaxHeight() - 1;
  while (true) {
    // 假設(shè) next_ = [*3, *5, *6]
    // 表示該結(jié)點(diǎn):
    // 在第 2 層的下一個(gè)索引結(jié)點(diǎn)為 6
    // 在第 1 層的下一個(gè)索引結(jié)點(diǎn)為 5
    // 在第 0 層的下一個(gè)結(jié)點(diǎn)為 3
    // 那么就可以直接通過 next_[level] 找到下一個(gè)索引結(jié)點(diǎn)
    Node* next = x->Next(level);
    if (KeyIsAfterNode(key, next)) {  // key 是否在當(dāng)前結(jié)點(diǎn)之后(大小關(guān)系由比較器最終確認(rèn))
      // Keep searching in this list
      // 繼續(xù)遍歷搜索該層的剩余結(jié)點(diǎn)
      x = next;
    } else {  // key 是否在當(dāng)前結(jié)點(diǎn)之后(大小關(guān)系由比較器最終確認(rèn))
      // 記錄結(jié)點(diǎn)到 prev 數(shù)組
      // prev 數(shù)組記錄每個(gè)索引層次要插入 key 的位置
      if (prev != nullptr) prev[level] = x; prev
      if (level == 0) { // 遍歷到 0 層,遍歷結(jié)束
        return next;
      } else {
        // Switch to next list
        // 進(jìn)入下一層遍歷
        level--;
      }
    }
  }
}

刪除元素

LevelDB 業(yè)務(wù)層面無刪除結(jié)點(diǎn)的需求啄刹,見源碼注解如下:

// (1) Allocated nodes are never deleted until the SkipList is
// destroyed.  This is trivially guaranteed by the code since we
// never delete any skip list nodes.

插入元素

template <typename Key, class Comparator>
void SkipList<Key, Comparator>::Insert(const Key& key) {
  // TODO(opt): We can use a barrier-free variant of FindGreaterOrEqual()
  // here since Insert() is externally synchronized.
  Node* prev[kMaxHeight];
  // 獲取所有大于等于(比較器定義) key 的結(jié)點(diǎn)
  // prev 保存各個(gè)索引層要插入的前一個(gè)結(jié)點(diǎn)
  Node* x = FindGreaterOrEqual(key, prev);

  // Our data structure does not allow duplicate insertion
  // 不允許插入重復(fù)的元素
  // 那么為空涮坐,表示沒有 >= key 的結(jié)點(diǎn)凄贩。要么不等于列表中的所有 key誓军,表示沒有重復(fù)元素
  assert(x == nullptr || !Equal(key, x->key));

  // 生成一個(gè)隨機(jī)高度
  int height = RandomHeight();
  // 如果隨機(jī)高度比當(dāng)前最大高度大
  if (height > GetMaxHeight()) {
    // prev 下標(biāo)從原先的最大 height 到最新的最大 height 之間初始化為 head_
    for (int i = GetMaxHeight(); i < height; i++) {
      prev[i] = head_;
    }
    // It is ok to mutate max_height_ without any synchronization
    // with concurrent readers.  A concurrent reader that observes
    // the new value of max_height_ will see either the old value of
    // new level pointers from head_ (nullptr), or a new value set in
    // the loop below.  In the former case the reader will
    // immediately drop to the next level since nullptr sorts after all
    // keys.  In the latter case the reader will use the new node.
    // 原子操作:保存最新的最大高度
    max_height_.store(height, std::memory_order_relaxed);
  }

  // 創(chuàng)建一個(gè)新結(jié)點(diǎn)
  x = NewNode(key, height);
  for (int i = 0; i < height; i++) {
    // NoBarrier_SetNext() suffices since we will add a barrier when
    // we publish a pointer to "x" in prev[i].
    // 
    // 插入新結(jié)點(diǎn),即:
    // new_node->next = pre->next;
    // pre->next = new_node;
    x->NoBarrier_SetNext(i, prev[i]->NoBarrier_Next(i));
    prev[i]->SetNext(i, x);
  }
}

并發(fā)處理

LevelDB 的跳表實(shí)現(xiàn)支持單線程寫疲扎、多線程讀昵时,為了滿足該特點(diǎn),LevelDB 在更新和讀取時(shí)需要注意 C++ memory_order 的設(shè)置椒丧。

在講解 LevelDB 跳表中的 memory_order 之前需要先介紹相關(guān)的基礎(chǔ)知識(shí)壹甥。

原子性

原子寓意著「不可再分的最小單位」,固計(jì)算機(jī)領(lǐng)域提及的原子性操作指的是那些「不可或不該再被切分(或中斷)的操作」壶熏。

而關(guān)于原子性句柠,我們應(yīng)當(dāng)具有一個(gè)基本的認(rèn)知:高級(jí)語言層面,單條語句并不能保證對(duì)應(yīng)的操作具有原子性棒假。

在使用 C溯职、C++、Java 等各種高級(jí)語言編寫代碼時(shí)帽哑,不少人會(huì)下意識(shí)的認(rèn)為一條不可再分的單條語句具有原子性谜酒,例如常見 i++

// 偽碼

int i = 0;

void increase() {
  i++;
}

int main() {
  /* 創(chuàng)建兩個(gè)線程妻枕,每個(gè)線程循環(huán)進(jìn)行 100 次 increase */
  // 線程 1
  Thread thread1 = new Thread(
    run() {
      for (int i = 0; i < 100; i++) increase();
    }
  );
  // 線程 2
  Thread thread2 = new Thread(
    run() {
      for (int i = 0; i < 100; i++) increase();
    }
  );
}

如果 i++ 是原子操作僻族,則上述偽碼中的 i 最終結(jié)果為 200。但實(shí)際上每次運(yùn)行結(jié)果可能都不相同屡谐,且通常小于 200述么。

之所以出現(xiàn)這樣的情況是因?yàn)?i++ 在執(zhí)行時(shí)通常還會(huì)繼續(xù)劃分為多條 CPU 指令。以 Java 為例愕掏,i++ 編譯將形成四條字節(jié)碼指令度秘,如下所示:

// Java 字節(jié)碼指令
0:  getstatic
1:  iconst_1
2:  iadd
3:  putstatic

而上述四條指令的執(zhí)行并不保證原子性,即執(zhí)行過程可被打斷亭珍》蠹兀考慮如下 CPU 執(zhí)行序列:

  1. 線程 1 執(zhí)行 getstatic 指令,獲得 i = 1
  2. CPU 切換到線程 2肄梨,也執(zhí)行了 getstatic 指令阻荒,獲得 i = 1。
  3. CPU 切回線程 1 執(zhí)行剩下指令众羡,此時(shí) i = 2
  4. CPU 切到線程 2侨赡,由于步驟 2 讀到的是 i = 1,固執(zhí)行剩下指令最終只會(huì)得到 i = 2

以上四條指令是 Java 虛擬機(jī)中的字節(jié)碼指令,字節(jié)碼指令是 JVM 執(zhí)行的指令羊壹。實(shí)際每條字節(jié)碼指令還可以繼續(xù)劃分為更底層的機(jī)器指令蓖宦。但字節(jié)碼指令已經(jīng)足夠演示原子性的含義了

如果對(duì)底層 CPU 層面如何實(shí)現(xiàn)機(jī)器指令的原子操作[1]感興趣,可查閱 Spinlock油猫、MESI protocol 等資料稠茂。

[1] 一條 CPU 指令可能需要涉及到緩存、內(nèi)存等多個(gè)單元的交互情妖,而在多核 CPU 的場(chǎng)景下并會(huì)存在與高層次多線程類似的問題睬关。固需要一些機(jī)制和策略才可實(shí)現(xiàn)機(jī)器指令的原子操作。

有序性

上述已經(jīng)提到 CPU 的一條指令執(zhí)行時(shí)毡证,通常會(huì)有多個(gè)步驟电爹,如取指IF 即從主存儲(chǔ)器中取出指令、ID 譯碼即翻譯指令料睛、EX 執(zhí)行指令丐箩、存儲(chǔ)器訪問 MEM 取數(shù)、WB 寫回恤煞。

即指令執(zhí)行將經(jīng)歷:IF屎勘、ID、EX阱州、MEM挑秉、WB 階段。

現(xiàn)在考慮 CPU 在執(zhí)行一條又一條指令時(shí)該如何完成上述步驟苔货?最容易想到并是順序串行犀概,指令 1 依次完成上述五個(gè)步驟,完成之后夜惭,指令 2 再開始依次完成上述步驟姻灶。這種方式簡(jiǎn)單直接,但執(zhí)行效率顯然存在很大的優(yōu)化空間诈茧。

思考一種流水線工作:

指令1   IF ID EX MEM WB
指令2      IF ID EX MEM WB
指令3         IF ID EX MEM WB

采用這種流水線的工作方式产喉,將避免 CPU 、存儲(chǔ)器中各個(gè)器件的空閑敢会,從而充分利用每個(gè)器件曾沈,提升性能。

同時(shí)注意到由于每條指令執(zhí)行的情況有所不同鸥昏,指令執(zhí)行的先后順序?qū)?huì)影響到這條流水線的負(fù)載情況塞俱,而我們的目標(biāo)則是讓整個(gè)流水線滿載緊湊的運(yùn)行。

為此 CPU 又實(shí)現(xiàn)了「指令重排」技術(shù)吏垮,CPU 將有選擇性的對(duì)部分指令進(jìn)行重排來提高 CPU 執(zhí)行的性能和效率障涯。例如:

x = 100;    // #1
y = 200;    // #2
z = x + y;  // #3

雖然上述高級(jí)語言的語句會(huì)編譯成多條機(jī)器指令罐旗,多條機(jī)器指令還會(huì)進(jìn)行「指令重排」,#1 語句與 #2 語句完全有可能被 CPU 重新排序唯蝶,所以程序?qū)嶋H運(yùn)行時(shí)可能會(huì)先執(zhí)行 y = 200; 然后再執(zhí)行 x = 100;

但另一方面九秀,指令重排的前提是不會(huì)影響線程內(nèi)程序的串行語義,CPU 在重排指令時(shí)必須保證線程內(nèi)語義不變粘我,例如:

x = 0; // #1
x = 1; // #2
y = x; // #3

上述的 y 一定會(huì)按照正常的串行邏輯被賦值為 1鼓蜒。

但不幸的是,CPU 只能保證線程內(nèi)的串行語義涂滴。在多線程的視角下友酱,「指令重排」造成的影響需要程序員自己關(guān)注晴音。


// 公共資源
int x = 0;
int y = 0;
int z = 0;

Thread_1:             Thread_2:
x = 100;              while (y != 200);
y = 200;              print x
z = x + y;

如果 CPU 不進(jìn)行「亂序優(yōu)化」執(zhí)行柔纵,那么 y = 200 時(shí),x 已經(jīng)被賦值為 100锤躁,此時(shí)線程 2 輸出 x = 200搁料。

但實(shí)際運(yùn)行時(shí),線程 1 可能先執(zhí)行 y = 200系羞,此時(shí) x 還是初始值 0郭计。線程 2 觀察到 y = 200 后,退出循環(huán)椒振,輸出 x = 0;

C++ 中的 atomic 和 memory_order

C++ 提供了 std::atomic 類模板昭伸,以保證操作原子性。同時(shí)也提供了內(nèi)存順序模型 memory_order指定內(nèi)存訪問澎迎,以便提供有序性和可見性庐杨。

其中 memory_order 共有六種,如下表所示:

memory_order 解釋
memory_order_relaxed 只保證原子操作的原子性夹供,不提供有序性的保證
memory_order_consume 當(dāng)前線程中依賴于當(dāng)前加載的該值的讀或?qū)懖荒鼙恢嘏诺酱思虞d前
memory_order_acquire 在其影響的內(nèi)存位置進(jìn)行獲得操作:當(dāng)前線程中讀或?qū)懖荒鼙恢嘏诺酱思虞d前
memory_order_release 當(dāng)前線程中的讀或?qū)懖荒鼙恢嘏诺酱舜鎯?chǔ)后
memory_order_acq_rel 帶此內(nèi)存順序的讀修改寫操作既是獲得操作又是釋放操作
memory_order_seq_cst 有此內(nèi)存順序的加載操作進(jìn)行獲得操作灵份,存儲(chǔ)操作進(jìn)行釋放操作,而讀修改寫操作進(jìn)行獲得操作和釋放操作哮洽,再加上存在一個(gè)單獨(dú)全序填渠,其中所有線程以同一順序觀測(cè)到所有修改

六種 memory_order 可以組合出四種順序:

  1. Relaxed ordering 寬松順序
Thread1: 
y.load(std::memory_order_relaxed);

Thread2:
y.store(h, std::memory_order_relaxed);

寬松順序只保證原子變量的原子性(變量操作的機(jī)器指令不進(jìn)行重排序),但無其他同步操作鸟辅,不保證多線程的有序性氛什。

  1. Release-Acquire ordering 釋放獲得順序
 
std::atomic<std::string*> ptr;
int data;
 
void producer()
{
    std::string* p  = new std::string("Hello"); // #1
    data = 42;  // #2
    ptr.store(p, std::memory_order_release);
}
 
void consumer()
{
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_acquire)))
        ;
    assert(*p2 == "Hello"); // 絕無問題 #3
    assert(data == 42); // 絕無問題 #4
}
 
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}

如例子所示,store 使用 memory_order_release匪凉,load 使用 memory_order_acquire枪眉,CPU 將保證如下兩點(diǎn):

  • store 之前的語句不允許被重排序到 store 之后(例子中的 #1 和 #2 語句一定在 store 之前執(zhí)行)
  • load 之后的語句不允許被重排序到 load 之前(例子中的 #3 和 #4 一定在 load 之后執(zhí)行)

同時(shí) CPU 將保證 store 之前的語句比 load 之后的語句「先行發(fā)生」,即先執(zhí)行 #1洒缀、#2瑰谜,然后執(zhí)行 #3欺冀、#4。這實(shí)際上就意味著線程 1 中 store 之前的讀寫操作對(duì)線程 2 中 load 執(zhí)行后是可見的萨脑。

注意是所有操作都同步了隐轩,不管 #3 是否依賴了 #1 或 #2

值得關(guān)注的是這種順序模型在一些強(qiáng)順序系統(tǒng)例如 x86、SPARC TSO渤早、IBM 主框架上是自動(dòng)進(jìn)行的职车。但在另外一些系統(tǒng)如 ARM、Power PC 等需要額外指令來保障鹊杖。

  1. Release-Consume ordering 釋放消費(fèi)順序
    理解了釋放獲得順序順序后悴灵,就非常容易理解釋放消費(fèi)順序,因?yàn)閮烧呤诸愃啤?/li>
 
std::atomic<std::string*> ptr;
int data;
 
void producer()
{
    std::string* p  = new std::string("Hello"); // #1
    data = 42; // #2
    ptr.store(p, std::memory_order_release);
}
 
void consumer()
{
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_consume)))
        ;
    assert(*p2 == "Hello"); // #3 絕無出錯(cuò): *p2 從 ptr 攜帶依賴
    assert(data == 42); // #4 可能也可能不會(huì)出錯(cuò): data 不從 ptr 攜帶依賴
}
 
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}

store 使用 memory_order_release骂蓖,load 使用 memory_order_consume雪标。其效果與 Release-Acquire ordering 釋放獲得順序類似,唯一不同的是并不是所有操作都同步(不夠高效)筋岛,而是只對(duì)依賴操作進(jìn)行同步淹冰,保證其有序性上例就是 #3 一定發(fā)生在 #1 之后,因?yàn)檫@兩個(gè)操作依賴于 ptr被芳。但不會(huì)保證 #4 一定發(fā)生在 #2 之后(注意「釋放獲得順序」可以保證這一點(diǎn))缰贝。

  1. Sequential consistency 序列一致順序
    理解上述幾種順序后,Sequential consistency 就很好理解了畔濒。

「釋放獲得順序」是對(duì)某一個(gè)變量進(jìn)行同步剩晴,Sequential consistency 序列一致順序則是對(duì)所有變量的所有操作都進(jìn)行同步。

store 和 load 都使用 memory_order_seq_cst侵状,可以理解對(duì)每個(gè)變量都進(jìn)行 Release-Acquire 操作赞弥。所以這也是最慢的一種順序模型。

LevelDB 跳表的并發(fā)處理

在 LevelDB 的 skiplist.h 中壹将,涉及到了 atomic 和 memory_order嗤攻,我們結(jié)合上文的介紹來理解其中的實(shí)現(xiàn)邏輯。

首先對(duì)跳表的最大高度 max_height_ 設(shè)置了 atomic诽俯,并采用 memory_order_relaxed 進(jìn)行讀寫:

// 確保在所有平臺(tái)下以及內(nèi)存對(duì)齊或非對(duì)齊情況下
// 對(duì) max_height_ 的讀寫都是原子性的
std::atomic<int> max_height_;

// ....

// store 和 load 都采用了 memory_order_relaxed
// 即采用 Relaxed ordering 寬松順序
// 即對(duì)多線程有序性不做保證
max_height_.store(height, std::memory_order_relaxed);

// ...

max_height_.load(std::memory_order_relaxed);

max_height_ 如同實(shí)現(xiàn)一個(gè)計(jì)數(shù)器 i++ 一樣妇菱,如果多線程讀不是原子性的,那么就會(huì)造成類似某個(gè)線程讀到舊數(shù)據(jù)或不完整數(shù)據(jù)的局面暴区。

其次對(duì)跳表結(jié)點(diǎn)的索引結(jié)點(diǎn)也進(jìn)行了 atomic 的處理闯团,如下所示:

std::atomic<Node*> next_[1];

// ...

// 插入結(jié)點(diǎn)時(shí)
next_[n].store(x, std::memory_order_release);

// ...

// 讀取結(jié)點(diǎn)時(shí)
next_[n].load(std::memory_order_acquire);

從中可知,對(duì) next_[n] 使用了 Release-Acquire ordering 釋放獲得順序仙粱,其可以保證某個(gè)線程進(jìn)行 store 后房交,其他所有執(zhí)行 load 的讀線程都將讀到 store 的最新數(shù)據(jù)。因?yàn)獒尫奴@得順序保證了 先 store 后 load 的執(zhí)行順序伐割。

這也正是 LevelDB 的跳表支持多線程讀的原因候味。

值得注意的是其中還實(shí)現(xiàn)了 NoBarrier_SetNextNoBarrier_Next刃唤。這兩個(gè)沒有內(nèi)存屏障的操作實(shí)際就是使用了寬松順序?qū)?next_[n] 進(jìn)行讀寫。這種操作是線程不安全的白群,為什么需要這種操作尚胞?

void SkipList<Key, Comparator>::Insert(const Key& key) {
  // ... 

  // 插入新結(jié)點(diǎn)
  x = NewNode(key, height);
  for (int i = 0; i < height; i++) {
    // 這兩句相當(dāng)于:
    // new_node->next = pre->next;
    // pre->next = new_node;
    x->NoBarrier_SetNext(i, prev[i]->NoBarrier_Next(i));
    prev[i]->SetNext(i, x);
  }
}

在一個(gè)鏈表中插入一個(gè)結(jié)點(diǎn)的步驟實(shí)際就是:

new_node->next = pre->next;
pre->next = new_node;

而 new_node->next = pre->next; 這一步賦值不必立馬對(duì)所有讀線程可見,因?yàn)榇藭r(shí)還未完全插入結(jié)點(diǎn)帜慢,并不影響讀線程的讀取笼裳。如下圖所示:

concurrency.png

為什么要特意使用 NoBarrier_SetNext ?因?yàn)閷捤身樞蛐矢吡涣幔梢钥吹?LevelDB 的跳表實(shí)現(xiàn)為了性能已經(jīng)優(yōu)化到了如此變態(tài)的地步躬柬。

附錄

源碼注解

對(duì) LevelDB 中跳表 skiplist.h 的源碼實(shí)現(xiàn)做了詳細(xì)注解,可見源碼注解抽减。

操作樣例

  1. 創(chuàng)建一個(gè) SkipList允青,數(shù)據(jù)結(jié)構(gòu)初始化如下圖所示:
implement_0.png
  1. 新增一個(gè)結(jié)點(diǎn),且設(shè) key = 10胯甩,隨機(jī) height = 4,則數(shù)據(jù)結(jié)構(gòu)如下圖所示:
implement_1.png
  1. 繼續(xù)新增一個(gè)結(jié)點(diǎn)昧廷,且設(shè) key = 5,隨機(jī) height= 3偎箫,則數(shù)據(jù)結(jié)構(gòu)如下圖所示:
implement_2.png
  1. 繼續(xù)新增一個(gè)結(jié)點(diǎn),且設(shè) key = 4皆串,隨機(jī) height = 5淹办,則數(shù)據(jù)結(jié)構(gòu)如下圖所示:
    implement_3.png

參考資料

跳躍列表 Wikipedia
Skip list Wikipedia
Skip Lists: A Probabilistic Alternative to Balanced Trees
Atomic_semantics Wikipedia
Spinlock Wikipedia
MESI_protocol Wikipedia
CPU的工作過程
std::memory_order
周志明.2011.深入理解 Java 虛擬機(jī)
葛一鳴,郭超.2015.Java高并發(fā)程序設(shè)計(jì)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末恶复,一起剝皮案震驚了整個(gè)濱河市怜森,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌谤牡,老刑警劉巖副硅,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異翅萤,居然都是意外死亡恐疲,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門套么,熙熙樓的掌柜王于貴愁眉苦臉地迎上來培己,“玉大人,你說我怎么就攤上這事胚泌∈∽桑” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵玷室,是天一觀的道長(zhǎng)零蓉。 經(jīng)常有香客問我笤受,道長(zhǎng),這世上最難降的妖魔是什么敌蜂? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任感论,我火速辦了婚禮,結(jié)果婚禮上紊册,老公的妹妹穿的比我還像新娘比肄。我一直安慰自己,他們只是感情好囊陡,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布芳绩。 她就那樣靜靜地躺著,像睡著了一般撞反。 火紅的嫁衣襯著肌膚如雪妥色。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天遏片,我揣著相機(jī)與錄音嘹害,去河邊找鬼。 笑死吮便,一個(gè)胖子當(dāng)著我的面吹牛笔呀,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播髓需,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼许师,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了僚匆?” 一聲冷哼從身側(cè)響起微渠,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎咧擂,沒想到半個(gè)月后逞盆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡松申,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年云芦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片攻臀。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡焕数,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出刨啸,到底是詐尸還是另有隱情堡赔,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布设联,位于F島的核電站善已,受9級(jí)特大地震影響灼捂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜换团,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一悉稠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧艘包,春花似錦的猛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至舌厨,卻和暖如春岂却,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背裙椭。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工躏哩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人揉燃。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓扫尺,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親你雌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子器联,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355