借shared_ptr實現copy-on-write
情境:
一個多線程的C++程式,24h x 5.5d運行。有幾個背景工作執行緒ThreadW{0,1,2,3},處理客戶發過來的交易請求,另外有一個背景線程ThreadB,不定期更新程式內部的參考資料。這些線程都跟一個hash表打交道,背景工作執行緒唯讀,背景線程讀寫,必然要用到一些同步機制,防止資料損毀。這裡的範例程式碼用std::map代替hash表,意思是一樣的:
typedef map<string, vector<pair<string, int> > > Map;
map 的 key 是使用者名稱,value 是一個vector,裡邊存的是不同stock的最小交易間隔,vector已經排好序,可以用二分尋找。
我們的系統要求背景工作執行緒的延遲儘可能小,可以容忍背景線程的延遲略大。一天之內,背景線程對資料更新的次數屈指可數,最多一小時一次,更新的資料來自於網路,所以對更新的及時性不敏感。Map的資料量也不大,大約一千多條資料。
最簡單的同步辦法,用讀寫鎖,背景工作執行緒加讀鎖,背景線程加寫鎖。但是讀寫鎖的開銷比普通mutex要大,如果背景工作執行緒能用最普通的非重入Mutex實現同步,就不必用讀寫鎖,效能較高。我們藉助shared_ptr實現了這一點:
class Mutex;
class MutexLockGuard;
class CustomerData
{
public:
CustomerData() : data_(new Map)
{ }
~CustomerData();
int query(const string& customer, const string& stock) const
{
MapPtr data = getData();
// data 一旦拿到,就不再需要鎖了。取資料的時候只有getData()內部有鎖,多線程並發讀的效能很好。
// 假設使用者肯定存在
const EntryList& entries = (*data)[customer];
return findEntry(entries, stock);
}
void update(const string& customer, const EntryList& entries );
private:
typedef vector<string, int> EntryList;
typedef map<string, EntryList> Map;
typedef tr1::shared_ptr<Map> MapPtr;
static int findEntry(const EntryList& entries, const string& key) const
{ /* 用 lower_bound 在 entries 裡找 key */ }
MapPtr getData() const
{
MutexLockGuard lock(dataMutex_);
return data_;
}
MapPtr data_;
mutable Mutex dataMutex_;
};
關鍵看CustomerData::update()怎麼寫。既然要更新資料,那肯定得加鎖,如果這時候其他線程正在讀,那麼不能在原來的資料上修改,得建立一個副本,在副本上修改,修改完了再替換。如果沒有使用者在讀,那麼就能直接修改,節約一次拷貝。
void CustomerData::update(const string& customer, const EntryList& entries )
{
MutexLockGuard lock(dataMutex_);
if (!data_.unique())
{
MapPtr newData(new Map(*data_));
data_.swap(newData);
}
assert(data_.unique());
(*data_)[customer] = entries;
}
注意其中用了shared_ptr::unique()來判斷是不是有人在讀,如果有人在讀,那麼我們不能直接修改,因為query()並沒有全程加鎖,只在getData()內部有鎖。shared_ptr::swap()把data_替換為新副本,而且我們還在鎖裡,不會有別的線程來讀,可以放心地更新。
據我們測試,大多數情況下更新都是在原來資料上進行的,拷貝的比例還不到1%,很高效。更準確的說,這不是copy-on-write,而是copy-on-other-reading。
我們將來可能會採用無鎖資料結構,不過目前這個實現已經非常好,滿足我們的要求。