1. 程式人生 > >借shared_ptr實現copy-on-write (1)

借shared_ptr實現copy-on-write (1)

    在《Linux多執行緒服務端程式設計使用muduoC++網路庫》2.8節說“借shared_ptr實現copy-on-write”。那麼copy-on-write是怎樣的技術?

    COW(Copy-On-Write)通過淺拷貝(shallow copy)只複製引用而避免複製值;當的確需要進行寫入操作時,首先進行值拷貝,再對拷貝後的值執行寫入操作,這樣減少了無謂的複製耗時。

    特點如下:
         *讀取安全(但是不保證快取一致性),寫入安全(代價是加了鎖,而且需要全量複製)
         *不建議用於頻繁讀寫場景下,全量複製很容易造成GC停頓.
         *適用於物件空間佔用大,修改次數少,而且對資料實效性要求不高的場景。

      這裡的安全指在進行讀取或者寫入的過程中,資料不被修改。

   copy-on-write最擅長的是併發讀取場景,即多個執行緒/程序可以通過對一份相同快照,去處理實效性要求不是很高但是仍然要做的業務,如Unix下的fork()系統呼叫、標準C++類std::string等採用了 copy-on-write,在真正需要一個儲存空間時才去分配記憶體,這樣會極大地降低程式執行時的記憶體開銷。

   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,可以阻止併發寫。

//假設g_ptr是一個全域性的shared_ptr<Foo>並且已經初始化。
void read()
{
    shared_ptr<Foo> tmpptr;
    {
        lock();
        tmpptr=g_ptr;//此時引用計數為2,通過gdb除錯可以看到
    }
    //訪問tmpptr
    //...
}
這部分是shared_ptr最基本的用法,還是很好理解的,read()函式呼叫結束,tmpptr作為棧上變數離開作用域,自然析構,原資料物件的引用計數也變為1。
        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_;
};