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;
}
临界区前的两行代码都是线程不安全的。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之后进行,这样才安全。上面是自己对三种错误写法的个人观点,在此抛砖引玉了。
具体代代码看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);
}
下面是继承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_;
};