[跟吉姆一起讀LevelDB]3.Memory Barrier與leveldb::DB::Open操作(2)

我以前從來沒有C++多線程的經驗, 借著看源碼的機會, 才有機會了解. 曾今工作時, 我寫Python爬蟲就用thread-safe隊列, 以為原子性全是靠鎖實現的. 所謂的無鎖就是先修改再檢查要不要反悔的樂觀鎖. 我錯了, X86 CPU的賦值(Store)和讀取(Load)操作天然可以做到無鎖.

相關問題: C++的6種memory order

那memory barrier這個名詞是哪裡蹦出來的呢? Load是原子性操作, CPU不會Load流程走到一半, 就切換到另一個線程去了, 也就是Load本身是不會在多線程環境下產生問題的. 真正導致問題的是做這個操作的時機不確定!

1. 編譯器有可能讓指令亂序, 比如, int a=b; long c=b; 編譯器一旦判定a和c沒有依賴性, 就有權力讓這兩個取值操作以任意順序執行. 因為有可能有CPU指令可以一下取4個int, 亂序可以湊個整.

2. CPU會讓指令亂序, 原因同上, 但額外還有個原因是分支預測. AB線程都讀寫一個中間量c, B在處理c, 你預期B好了, A才會取. 但萬一A分支預測成功, B在處理的時候, A已經提前Load c進寄存器, 這就沒得玩了...

所以, 必須要有指令告訴CPU和編譯器, 不要改變這個變數的存取順序. 這就是Memory Barrier了. call MemoryBarrier保證前後一行是嚴格按照代碼順序的.

atomic_pointer.h 126-143行, 注意MemoryBarrier()的擺放,

class AtomicPointer {n private:n void* rep_;n public:n AtomicPointer() { }n explicit AtomicPointer(void* p) : rep_(p) {}n inline void* NoBarrier_Load() const { return rep_; }n inline void NoBarrier_Store(void* v) { rep_ = v; }n inline void* Acquire_Load() const {n void* result = rep_;n MemoryBarrier();n return result;n }n inline void Release_Store(void* v) {n MemoryBarrier();n rep_ = v;n }n}; n

大公司的開源項目真的是一個寶庫! 就算用不到, 各種踩了無數坑的庫, 編碼規則和跨平台代碼都是一般人沒機會完善的.

另外, 有菊苣在問題leveldb中atomic_pointer裡面memory barrier的幾點疑問?提到MemoryBarrier不保證CPU不亂序. 我覺得這個應該不用擔心. 因為MemoryBarrier的counterpart是std::atomic, 肯定嚴格保證語義相同啊. 實在不放心用std::atomic是墜吼的.

------

繼續上次沒讀完的Open部分代碼.

db_impl.cc 139-146行,

has_imm_.Release_Store(NULL); // atomic pointernn // Reserve ten files or so for other uses and give the rest to TableCache.n const int table_cache_size = options_.max_open_files - kNumNonTableCacheFiles;n table_cache_ = new TableCache(dbname_, &options_, table_cache_size);nn versions_ = new VersionSet(dbname_, &options_, table_cache_,n &internal_comparator_);n

  • has_imm_, 用於判斷是否有等待或者正在寫入硬碟的immemtable
  • table_cache_, SSTable查詢緩存
  • versions_, 資料庫MVCC

has_imm_就是我上面描述的atomic pointer, 我推測這裡大概率Google程序員雇了一個臨時工(233), 把可以列表構造的has_imm_放到了函數部分, 因為這裡不存在任何race的可能性. db new完了. 說下一個很重要的原則, 構造函數究竟要做什麼? 阿里和Google共同的觀點: 輕且無副作用(基本就是賦值). 業務有需求的話, 兩步構造或者工廠函數, 二選一.

回到最早的工廠函數, 一個靠譜資料庫的Open操作, 用腳趾頭也能想到要從日誌恢複數據,

Status DB::Open(const Options& options, const std::string& dbname,n DB** dbptr) { // 工廠函數n *dbptr = NULL; // 設置結果默認值, 指針傳值nn DBImpl* impl = new DBImpl(options, dbname);n impl->mutex_.Lock(); // 數據恢復時上鎖, 禁止所有可能的後台任務n VersionEdit edit;n // Recover handles create_if_missing, error_if_existsn bool save_manifest = false;n Status s = impl->Recover(&edit, &save_manifest); // 讀log恢復狀態n if (s.ok() && impl->mem_ == NULL) {n // Create new log and a corresponding memtable. 複位n uint64_t new_log_number = impl->versions_->NewFileNumber();n WritableFile* lfile;n s = options.env->NewWritableFile(LogFileName(dbname, new_log_number),n &lfile);n if (s.ok()) {n edit.SetLogNumber(new_log_number);n impl->logfile_ = lfile;n impl->logfile_number_ = new_log_number;n impl->log_ = new log::Writer(lfile);n impl->mem_ = new MemTable(impl->internal_comparator_);n impl->mem_->Ref();n }n }n if (s.ok() && save_manifest) {n edit.SetPrevLogNumber(0); // No older logs needed after recovery.n edit.SetLogNumber(impl->logfile_number_);n s = impl->versions_->LogAndApply(&edit, &impl->mutex_); // 同步VersionEdit到MANIFEST文件n }n if (s.ok()) {n impl->DeleteObsoleteFiles(); // 清理無用文件n impl->MaybeScheduleCompaction(); // 有寫入就有可能要compactn }n impl->mutex_.Unlock(); // 初始化完畢n if (s.ok()) {n assert(impl->mem_ != NULL);n *dbptr = impl;n } else {n delete impl;n }n return s;n}n

------

就這樣, Open操作的脈絡大概應該是有了, 下篇再見.

推薦閱讀:

阿里雲Elasticsearch的X-Pack:機器學習、安全保障和可視化
[MySQL]獵聘網數據分析職位數據清洗
如何用R訪問MySQL資料庫
[跟吉姆一起讀LevelDB]5.資料庫恢復(2)

TAG:LevelDB | 数据库 | CC |