前言:
做過GUI開發的同學, 都知曉雙快取機制. 其過程為先把所有的場景和實體物件畫到一個備份canvas, 然後再把備份canvas的內容整個填充真正的畫板canvas中. 如果不採用雙快取機制, 你的畫面有可能會出現閃爍和抖動.
究其原因是整個繪製過程, 包含清屏, 繪製場景和各個實體. 其耗時遠遠大於單個canvas的複製. 進而導致CPU寫canvas的速率小於LCD讀取canvas的速率. 這樣就出現閃爍的現象了.
在後臺服務中, 也會遇到類似的情形: 當資料/資源需要更新時, 採用直接增量更新的方式代價大(耗時長, 阻塞服務可用/實時響應), 由此引入back buffer,做0/1切換.
本文以"配置檔案熱載更新"為例, 著重介紹0/1切換的思路和優化技巧.
熱載更新:
以往更新配置時, 往往需要重啟服務程式. 為了提高服務的可用性, 更方便運維.
採取的改進方式是:
1) 引入配置中心服務(ConfigServer)
把模組的配置檔案擱置在ConfigServer中, 具體模組從ConfigServer中獲取(拉起/通知).
2) 監控本地配置檔案變更
程式模組通過定期輪詢/事件觸發的方式, 感知配置檔案是否發生變化, 若發生變化, 則重新載入.
但無論採用何種方式, 勢必存在切換過程.
切換特點:
把切換的雙方定義為前端和後端, 前端資源往往被N個執行緒訪問(靜態只讀), 後端資源往往是一個執行緒更新寫. 於是就形成了一個N讀1寫的格局.
具體在c/c++實現時, 切換過程往往就是一個指標的重新賦值, 十分簡單.
但問題也就隱藏在這了, 在切換後的舊資源銷燬過程中, 存在多執行緒的競態衝突風險.
有人可能會提議, 如果對資源的訪問和資源的切換加相同的鎖保護, 就沒有這個問題. 但在低頻率切換的場景下, 加鎖帶來的效能損失, 有些得不償失.
無鎖0/1切換:
是否存在無鎖的切換方式呢?
1). 延遲銷燬
工作執行緒持有並訪問舊資源控制程式碼時間不長, 可以設定一個時間視窗, 該時間視窗內屬於保護期, 禁止對舊資源進行銷燬.
注: 在絕大多數場景下, 該方案滿足條件. 只是理論上, 不排除低概率事件.
2). 帶引用計數的智慧指標切換
我們藉助boost的shared_ptr來構建切換的小例子
#include <boost/shared_ptr.hpp>
#include <stdint.h>
#include <stdio.h>
class Config {
};
class DataCenter {
public:
DataCenter() {
}
void init() {
active_idx = 0;
switchover[active_idx].reset(new Config());
}
// *) 切換函式, 由更新執行緒呼叫
void swith() {
uint32_t unactive_idx = (active_idx == 0) ? 1 : 0;
uint32_t old_active_idx = active_idx;
// *) 新資源ready
switchover[unactive_idx].reset(new Config());
// *) 正式切換
active_idx = unactive_idx;
// *) 舊資源reset, 引入計數減一
switchover[old_active_idx].reset();
}
// *) 訪問資源, 由前端執行緒呼叫
boost::shared_ptr<Config> getConfig() {
return switchover[active_idx];
}
private:
volatile uint32_t active_idx;
// *) 切換陣列
boost::shared_ptr<Config> switchover[2];
};
巧用boost::shared_ptr內部有個原子計數器和代理指標, 藉助RAII的思想完美的實現了無引用時的自動清理工作. 也避免了上述的競態衝突.
總結:
在服務模組中的0/1切換有很多, 這邊簡述了下解決方案, 沒有細緻展開, 權當個人的學習筆記.
寫在最後:
如果你覺得這篇文章對你有幫助, 請小小打賞下. 其實我想試試, 看看寫部落格能否給自己帶來一點小小的收益. 無論多少, 都是對樓主一種由衷的肯定.