美文网首页
智能指针与多线程

智能指针与多线程

作者: _张鹏鹏_ | 来源:发表于2022-02-10 15:53 被阅读0次

    目的:

    智能指针在多线程编程场景下,可以保证对象安全地析构,解引用时对象有效。

    本文中涉及到的具体内容可以参考linux多线程服务端编程一书的第一、二章。

    小结:

    这里汇总下书中的结论:

    • 对象析构算是写操作。
    • 借助shared_ptr来实现线程安全的对象释放,但是shared_ptr本身不是100%线程安全。所以多个线程中访问同一个shared_ptr也需要加锁保护。
    • shared_ptr会延长对象的生命周期,只要有一个指向x对象的shared_ptr,该对象就不会析构。
    • shared_ptr是值语义,当心意外延长对象的生命周期,例如bind容器都可能拷贝shared_ptr
    • weak_ptrshared_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复制的数据是坏数据。

    参考文献:

    1. 智能指针基础总结
    2. 借助智能指针实现写时复制
    3. linux多线程服务端编程 2.8章节
    4. 陈硕 借shared_ptr实现copy-on-write

    相关文章

      网友评论

          本文标题:智能指针与多线程

          本文链接:https://www.haomeiwen.com/subject/yiknkrtx.html