目的:
智能指针在多线程编程场景下,可以保证对象安全地析构,解引用时对象有效。
本文中涉及到的具体内容可以参考linux多线程服务端编程一书的第一、二章。
小结:
这里汇总下书中的结论:
- 对象析构算是写操作。
- 借助
shared_ptr
来实现线程安全的对象释放,但是shared_ptr
本身不是100%线程安全。所以多个线程中访问同一个shared_ptr
也需要加锁保护。 -
shared_ptr
会延长对象的生命周期,只要有一个指向x对象的shared_ptr
,该对象就不会析构。 -
shared_ptr
是值语义,当心意外延长对象的生命周期,例如bind和容器都可能拷贝shared_ptr
。 -
weak_ptr
是shared_ptr
的好搭档,可以作弱回调、对象池等。
COW:
这里整理下书中使用智能指针实现写时复制的做法。使用shard_ptr
来管理数据,原理如下:
- shared_ptr是引用计数型智能指针,如果当前只有一个观察者,那么引用计数器值为1。
- 对于write端,如果发现计数为1,这时可以安全的修改共享对象,不必担心有人在读它。
- 对于read端,在读之前把引用计数加1,读完后减1,这样保证在读期间,引用计数大于1,保证读取时对象不会被析构,也可以阻止并发写。
- 对于write端,如果发现计数大于1,则需要复制。
全局数据定义:
// 全局数据;
typedef vector<Foo> FooList;
typedef shared_ptr<FooList> FoolListPtr;
MutexLock mutex;
FoolListPtr g_foos; // 所有线程可见,访问时要加互斥锁;
读端:
/*
临界区非常小只读了一次共享变量g_foos
比以往的写法大为缩短,多个线程调用read也不会相互阻塞,提高了并发性;
*/
void read()
{
FoolListPtr foos; // 位于每个线程的栈空间,每个线程有自己的线程栈
{
MutexLockGuard lock(mutex);
foos = g_foos; // 增加引用计数,确保读的时候对象不会被析构掉;
assert(!g_foos.unique());
}
for (auto iter = foos->begin(); iter != foos->end(); ++iter)
{
iter->doit();
}
}
写端:
void write(const Foo& f)
{
MutexLockGuard lock(mutex);
if (!g_foos.unique()) // 别的线程正在读取FooList,不能原地修改,复制一份,在副本基础上修改
{
// 由于不是g_foos.unique(),所以其指向对象始终有效。
g_foos.reset(new FooList(*g_foos));
}
assert(g_foos.unique());
g_foos->push_back(f);
}
习题:
本部分是书中对COW的一些错误做法:
错误一:直接修改g_foos所指的 FooList。
void write(const Foo& f)
{
MutexLockGuard lock(mutex);
g_foos->push_back(f);
}
如果线程A调用write;
线程B调用read,read正在访问FooList的一个迭代器;
由于线程A执行了push_back,导致read时迭代器失效。
错误二:试图缩小临界区,把copying移出临界区。
void write(const Foo& f)
{
FooListPtr newFoos(new FooList(*g_foos)); // 访问全局对象没有加锁;
newFoos->push_back(f);
MutexLockGuard lock(mutex);
g_foos = newFoos;
}
线程A调用g_foos.reset,如果此时引用计数为0,则FooList对象被释放;
线程B调用write,此时g_foos指向的对象已经被释放,拷贝一个已经释放的对象,程序崩溃;
错误三:把临界区拆分成两个小的,把copying放到临界区外。
void write(const Foo& f)
{
FooListPtr oldFoos;
{
MutexLockGuard lock(mutex);
oldFoos = g_foos;
}
// 以上代码保证了FooList不会被析构,从而拷贝的对象不会是一个被释放的对象;不会出现错误二提到的问题
FooListPtr newFoos(new FooList(*oldFoos));
newFoos->push_back(f);
MutexLockGuard lock(mutex);
g_foos = newFoos;
}
线程A调用write执行到
FooListPtr newFoos(new FooList(*oldFoos));
线程B修改了g_foos所指的 FooList;
导致线程A复制的数据是坏数据。
参考文献:
- 智能指针基础总结
- 借助智能指针实现写时复制
- linux多线程服务端编程 2.8章节
- 陈硕 借shared_ptr实现copy-on-write
网友评论