借shared_ptr實現copy-on-write (1)
阿新 • • 發佈:2019-01-28
在《Linux多執行緒服務端程式設計使用muduoC++網路庫》2.8節說“借shared_ptr實現copy-on-write”。那麼copy-on-write是怎樣的技術?
*讀取安全(但是不保證快取一致性),寫入安全(代價是加了鎖,而且需要全量複製)
*不建議用於頻繁讀寫場景下,全量複製很容易造成GC停頓.
*適用於物件空間佔用大,修改次數少,而且對資料實效性要求不高的場景。
Copy-On-Write的原理:
Copy-On-Write使用了"引用計數(retainCount)"的機制(在Objective-C和Java中有應用)。
當第一個string物件str1構造時,string的建構函式會根據傳入的引數在堆空間上分配記憶體。
當有其它物件通過str1進行拷貝構造時,str1的引用計數會增加1.
當有物件析構時,這個引用計數會減1。直到最後一個物件析構時,引用計數為0,此時程式才會真正釋放這塊記憶體。
1.如果你是資料的唯一擁有者,那麼你可以直接修改資料。
2.如果你不是資料的唯一擁有者,那麼你拷貝它之後再修改。
2. write端在寫之前,先檢查引用計數是否為1,
2.1 如果引用計數為1,則你是資料的唯一擁有者,直接修改。
2.2 如果引用計數大於1,則你不是資料的唯一擁有者,還有其它擁有者,此時資料正在被其它擁有者read,則不能再原來的資料上併發寫,應該建立一個副本,並在副本上修改,然後用副本替換以前的資料。這就需要用到一些shared_ptr的程式設計技法了:
shared_ptr::unique(),當引用計數為1時返回true,否則false。
假設一個執行緒讀,一個執行緒寫,當寫執行緒進入到if迴圈中時,原物件的引用計數為2,分別為tmpptr和g_ptr,此時reset()函式將原物件的引用計數減1,並且g_ptr已經指向了新的物件(用原物件構造),這樣就完成了資料的拷貝,並且原物件還在,只是引用計數變成了1。
注意,reset()函式僅僅只是將原物件的引用計數減1,並沒有將原物件析構,當原物件的引用計數為0時才會被析構。
在《Linux多執行緒服務端程式設計—使用muduo C++網路庫》的2.8節中,按照上面的方法,解決了2.1.1節的NonRecursiveMutex_test例子,在這就不累贅程式碼了。
錯誤二:試圖縮小臨界區,把copying移出臨界區
線上程A中,g_foos指向的資源即將被析構(因此遞減它所指向資源的引用計數),同時,線上程B中跑post()函式,正在執行"FooListPtr newFoos(new FooList(*g_foos));"這一行程式碼,此時正進行拷貝g_foos指向的資源,恐怕會出現core dump。因此要遞增同一個引用計數。 錯誤三:把臨界區拆分成兩個小的,把copying放到臨界區外
總而言之,一是要把copying放在臨界區內,二是修改g_foos所指的 FooList要在copy之後進行,這樣才安全。上面是自己對三種錯誤寫法的個人觀點,在此拋磚引玉了。 同樣的,用相同的思路解決Linux多執行緒服務端程式設計—使用muduo C++網路庫》的2.1.2節的MutualDeadLock例子。
具體代程式碼看github地址:
https://github.com/chenshuo/recipes/blob/master/thread/test/RequestInventory_test.cc
修改Inventory類的成員變數:
上面的方案仍然沒解決Request物件析構的race conditon,解決方案需要用boost::enable_shared_from_this,Request繼承它,還有Inventory類的add()和remove()這兩個成員函式的引數,由原始指標改成shared_ptr智慧指標。 具體程式碼看github地址 :https://github.com/chenshuo/recipes/blob/master/thread/test/RequestInventory_test2.cc
下面是繼承boost::enable_shared_from_this的Request類的程式碼:
COW(Copy-On-Write)通過淺拷貝(shallow copy)只複製引用而避免複製值;當的確需要進行寫入操作時,首先進行值拷貝,再對拷貝後的值執行寫入操作,這樣減少了無謂的複製耗時。
特點如下:*讀取安全(但是不保證快取一致性),寫入安全(代價是加了鎖,而且需要全量複製)
*不建議用於頻繁讀寫場景下,全量複製很容易造成GC停頓.
*適用於物件空間佔用大,修改次數少,而且對資料實效性要求不高的場景。
這裡的安全指在進行讀取或者寫入的過程中,資料不被修改。
copy-on-write最擅長的是併發讀取場景,即多個執行緒/程序可以通過對一份相同快照,去處理實效性要求不是很高但是仍然要做的業務,如Unix下的fork()系統呼叫、標準C++類std::string等採用了 copy-on-write,在真正需要一個儲存空間時才去分配記憶體,這樣會極大地降低程式執行時的記憶體開銷。
Copy-On-Write使用了"引用計數(retainCount)"的機制(在Objective-C和Java中有應用)。
當第一個string物件str1構造時,string的建構函式會根據傳入的引數在堆空間上分配記憶體。
當有其它物件通過str1進行拷貝構造時,str1的引用計數會增加1.
當有物件析構時,這個引用計數會減1。直到最後一個物件析構時,引用計數為0,此時程式才會真正釋放這塊記憶體。
即引用計數用來解決用來存放字串的記憶體何時釋放的問題。
COW技術的精髓:1.如果你是資料的唯一擁有者,那麼你可以直接修改資料。
2.如果你不是資料的唯一擁有者,那麼你拷貝它之後再修改。
寫時複製(Copy-On-Write)技術,是程式設計界"懶惰行為"-拖延戰術的產物。
shared_ptr是採用引用計數方式的智慧指標,如果當前只有一個觀察者,則其引用計數為1,可以通過shared_ptr::unique()判斷。用shared_ptr來實現COW時,主要考慮兩點:(1)讀資料 (2)寫資料
通過shared_ptr實現copy-on-write的原理如下:
1. read端在讀之前,建立一個新的智慧指標指向原指標,這個時候引用計數加1,讀完將引用計數減1,這樣可以保證在讀期間其引用計數大於1,可以阻止併發寫。
這部分是shared_ptr最基本的用法,還是很好理解的,read()函式呼叫結束,tmpptr作為棧上變數離開作用域,自然析構,原資料物件的引用計數也變為1。//假設g_ptr是一個全域性的shared_ptr<Foo>並且已經初始化。 void read() { shared_ptr<Foo> tmpptr; { lock(); tmpptr=g_ptr;//此時引用計數為2,通過gdb除錯可以看到 } //訪問tmpptr //... }
2. write端在寫之前,先檢查引用計數是否為1,
2.1 如果引用計數為1,則你是資料的唯一擁有者,直接修改。
2.2 如果引用計數大於1,則你不是資料的唯一擁有者,還有其它擁有者,此時資料正在被其它擁有者read,則不能再原來的資料上併發寫,應該建立一個副本,並在副本上修改,然後用副本替換以前的資料。這就需要用到一些shared_ptr的程式設計技法了:
void write()
{
lock()
if(!g_ptr.unique())
{
g_ptr.reset(new Foo(*g_ptr));
}
assert(g_ptr.unique());
//write
//
}
解釋一下程式碼:shared_ptr::unique(),當引用計數為1時返回true,否則false。
假設一個執行緒讀,一個執行緒寫,當寫執行緒進入到if迴圈中時,原物件的引用計數為2,分別為tmpptr和g_ptr,此時reset()函式將原物件的引用計數減1,並且g_ptr已經指向了新的物件(用原物件構造),這樣就完成了資料的拷貝,並且原物件還在,只是引用計數變成了1。
注意,reset()函式僅僅只是將原物件的引用計數減1,並沒有將原物件析構,當原物件的引用計數為0時才會被析構。
在《Linux多執行緒服務端程式設計—使用muduo C++網路庫》的2.8節中,按照上面的方法,解決了2.1.1節的NonRecursiveMutex_test例子,在這就不累贅程式碼了。
接著講講,《Linux多執行緒服務端程式設計—使用muduo C++網路庫》的2.8節中的三種錯誤寫法。
錯誤一:直接修改g_foos所指的 FooList
void post(const Foo& f)
{
MutexLockGuard lock(mutex);
g_foos->push_back(f);
}
如果有別的地方用到g_foos所指的 FooList的某一個迭代器,由於post()函式的push_bak()導致迭代器失效。錯誤二:試圖縮小臨界區,把copying移出臨界區
void post(const Foo& f)
{
FooListPtr newFoos(new FooList(*g_foos));
newFoos->push_back(f);
MutexLockGuard lock(mutex);
g_foos = newFoos;
}
臨界區前的兩行程式碼都是執行緒不安全的。線上程A中,g_foos指向的資源即將被析構(因此遞減它所指向資源的引用計數),同時,線上程B中跑post()函式,正在執行"FooListPtr newFoos(new FooList(*g_foos));"這一行程式碼,此時正進行拷貝g_foos指向的資源,恐怕會出現core dump。因此要遞增同一個引用計數。 錯誤三:把臨界區拆分成兩個小的,把copying放到臨界區外
void post(const Foo& f)
{
FooListPtr oldFoos;
{
MutexLockGuard lock(mutex);
oldFoos = g_foos;
}
FooListPtr newFoos(new FooList(*oldFoos));
newFoos->push_back(f);
MutexLockGuard lock(mutex);
g_foos = newFoos;
}
新建oldFoos指向原指標,防止被別的執行緒析構。但在這一行:FooListPtr newFoos(new FooList(*oldFoos)); ,如果有別的執行緒在修改g_foos所指的 FooList呢,後果可想而知。總而言之,一是要把copying放在臨界區內,二是修改g_foos所指的 FooList要在copy之後進行,這樣才安全。上面是自己對三種錯誤寫法的個人觀點,在此拋磚引玉了。 同樣的,用相同的思路解決Linux多執行緒服務端程式設計—使用muduo C++網路庫》的2.1.2節的MutualDeadLock例子。
具體代程式碼看github地址:
https://github.com/chenshuo/recipes/blob/master/thread/test/RequestInventory_test.cc
修改Inventory類的成員變數:
typedef std::set<RequestPtr> RequestList;
typedef boost::shared_ptr<RequestList> RequestListPtr;
RequestListPtr requests_;
修改Inventory類的add()和remove()這兩個成員函式
void add(Request* req)
{
muduo::MutexLockGuard lock(mutex_);
if (!requests_.unique())
{
requests_.reset(new RequestList(*requests_));
printf("Inventory::add() copy the whole list\n");
}
assert(requests_.unique());
requests_->insert(req);
}
void remove(Request* req) // __attribute__ ((noinline))
{
muduo::MutexLockGuard lock(mutex_);
if (!requests_.unique())
{
requests_.reset(new RequestList(*requests_));
printf("Inventory::remove() copy the whole list\n");
}
assert(requests_.unique());
requests_->erase(req);
}
上面的方案仍然沒解決Request物件析構的race conditon,解決方案需要用boost::enable_shared_from_this,Request繼承它,還有Inventory類的add()和remove()這兩個成員函式的引數,由原始指標改成shared_ptr智慧指標。 具體程式碼看github地址 :https://github.com/chenshuo/recipes/blob/master/thread/test/RequestInventory_test2.cc
下面是繼承boost::enable_shared_from_this的Request類的程式碼:
class Request : public boost::enable_shared_from_this<Request>
{
public:
Request()
: x_(0)
{
}
~Request()
{
x_ = -1;
}
void cancel() __attribute__ ((noinline))
{
muduo::MutexLockGuard lock(mutex_);
x_ = 1;
sleep(1);
printf("cancel()\n");
g_inventory.remove(shared_from_this());
}
void process() // __attribute__ ((noinline))
{
muduo::MutexLockGuard lock(mutex_);
g_inventory.add(shared_from_this());
// ...
}
void print() const __attribute__ ((noinline))
{
muduo::MutexLockGuard lock(mutex_);
// ...
printf("print Request %p x=%d\n", this, x_);
}
private:
mutable muduo::MutexLock mutex_;
int x_;
};