借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并且已经初始化。
void read()
{
    shared_ptr 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 RequestList;
typedef boost::shared_ptr 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
{
 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_;
};






你可能感兴趣的:(C++)