• shared_ptr和多线程


    前一篇文章写得实在太挫,重新来一篇。

    多线程环境下生命周期的管理

    多线程环境下,跨线程对象的生命周期管理会有什么挑战?我们拿生产者消费者模型来讨论这个问题。

    实现一个简单的用于生产者消费者模型的队列

    生产者消费者模型的基本结构如下图所示:

    如果我们要实现这个队列该怎么写?首先我们先简单挖掘下这个队列的一些基本需求。

    显而易见,这个队列需要支持多线程并发读写。

    我们知道,多线程并发读写同一个对象,需要对读写操作进行同步以避免data race[1]。在C++11里,我们可以借助mutex。

    另外当队列为空时,消费者来读取数据时,期望的结果应该是消费者线程被挂起,而不是不停地进行重试看队列是否非空。当生产者插入数据后,唤醒消费者,数据已经生成了。这个唤醒的机制可以通过条件变量来实现,condition_variable。

    在分析基本的需求和了解了相关的技术支持后,我们可以着手设计这个队列的基本接口了。它应该至少包含下面三个对外接口:

    • push
    • pop
    • size

    我们也可以考虑基于模板的方式来实现这个类。因此,程序看起来会是这样:

    template <typename T, typename CONTAINER_TYPE = std::queue<T>>
    class blocking_queue
    {
    public:
        blocking_queue();
        ~blocking_queue();
    
        void push(const T&);
        T pop();
        size_t size() const;
    
    private:
        std::mutex mtx_;
        std::condition_variable cv_;
        CONTAINER_TYPE queue_;
    
        blocking_queue(const blocking_queue&) = delete;
        blocking_queue& operator =(const blocking_queue&) = delete;
    };

    这里我特意屏蔽了拷贝构造和赋值操作。咱的这个队列从语义上不应该支持copy这件事。我们接下来看如何实现其中最主要的push和pop函数。

    push操作相对简单些,使用mtx_进行操作同步,然后插入数据。数据插入后进行通知。

    void push(const T& element)
    {
        {
            std::lock_guard<std::mutex> lock(mtx_);
            queue_.push(element);
        }
        cv_.notify_once();
    }

    pop函数会稍微复杂点。

    T pop()
    {
        std::unique_lock lock(mtx_);
        while (0 == queue_.size())
        {
            cv_.wait(lock);
        }
    
        T ret = queue_.front();
        queue_.pop();
    
        return ret;
    }

    另外,condition_variable::wait有两个重载函数,这里的while循环还可以写成:

    cv_.wait(lock, [this]() -> bool { return queue_.size() > 0; }

    这里我们岔开下话题稍微多说一下pop函数。主要是pop函数中的那个while。

    while loop associated with the condition variable

    条件变量的应用中,这个while语句已经是一个标配了。有人说条件变量的使用是最不容易出错的,因为正确的使用方式就这么一种,必须得配while。

    那为什么一定要用while呢?

    所有的官方解释(POSIXMSDNWiki)都集中到了一个名词:spurious wakeup。但是具体是什么导致的spurious wakeup,都没有挑明。我第一次看到这个while的时候,当时分析的结果是,这个过程存在竞态。

    我们假设有两个消费者(C1、C2)一个生产者(P)。并且此时队列已空。接下来:

    1. C1执行pop,因为队列为空,所以线程在cv_.wait处挂起
    2. P开始执行push,进入临近区并且还未退出
    3. C2执行pop。因为P还没有退出临近区,所以C2在进入临界区处挂起
    4. P插入数据后,退出临界区并通知cv_
    5. C2先被唤醒,进入临近区(可能性很大,因为push操作先退出临近区,再通知cv_)
    6. 此时C1无法从cv_.wait中退出,因为无法成功锁住mtx_
    7. C2消耗了P插入的数据,并从临界区中退出
    8. C1从cv_.wait中返回
    9. 此时,队列中已无数据

    从这个角度分析同样需要条件判断为循环形式。当然,也不止我一个人这么认为

    多线程共享对象生命周期管理的挑战

    我们假定生产者对应的实现类叫做producer,消费者类叫consumer。那么producer和consumer类都应该有一个指向blocking_queue的指针(或者引用),知道该往哪读写数据。

    接下来就有几个问题需要我们考虑了:

    1. producer、consumer和blocking_queue之间是什么关系?
    2. producer、consumer中的blocking_queue指针是raw指针么?

    我们先来思考第一问题。可以确定的一点是,blocking_queue不会同时被producer和consumer管理整个生命周期,这样没法管。同时producer和consumer并不需要知道对方的存在。所以势必有一方和blocking_queue之间是关联关系。我们就假定producer和blocking_queue之间是关联关系。

    再来思考第二个问题。简单起见,先假定producer保存的是指向blocking_queue的指针,类型为blocking_queue *。

    现在我们回到多线程环境里来思考producer对象的处境。

    多个producer线程写一个共享的blocking_queue对象。producer通过blocking_queue *指针如何知道这个blocking_queue对象是有效的?这个问题产生的本源就是这两者之间是关联关系,相互之间的耦合并不十分强烈。blocking_queue对象的创建和销毁对于producer来说都是透明的。这个问题也可以简单归结为通过一个指针,如何判断指向的内存是否有效?

    很不幸,这个问题在C/C++里是无解的(这里夸大了,事实上应该是可以使用二级指针来解决这个问题的)。这种有效性无法通过if语句判断。指针非空并不意味着指向的内存块保存的是有效的对象。既然如此,我们就需要使用新的解决方案。

    既然指针不行,那我们是不是可以实现一个对象管理类,专门用于管理blocking_queue对象,并且提供一个queue_is_valid()成员函数来判断blocking_queue对象的合法性。要实现这个方案,必须保该这个对象的生命周期比blocking_queue长。我们暂且把这个类称为manager。通过manager来管理这个blocking_queue对象指针的生命周期。

    那么,producer就需要有一份manager对象的拷贝(why? 如果是指针,问题是不是又回来了?)。既然如此,那么有多少个producer对象,就有多少个manager对象的拷贝。所以就引入了新的问题,这些manager拷贝如何共享同一个blocking_queue指针的相关信息?当其中一个manager对象释放了这个blocking_queue,其他manager对象如何知道呢?

    如何做好信息的同步是解决这个问题的手段。从这个角度出发,我们希望看到的情况应该是,当有人在用它,那么它就应该是活的;如果已经没有人用它了,那么它就没有必要存在了。类似于GC。所有人都不使用的东西,肯定是垃圾了。那么比较自然的解决方案就是引用计数。

    这就是C++11中引入的shared_ptr。

    我们用shared_ptr管理blocking_queue对象,并且将该shared_ptr对象保存到每一个producer对象中。多线程共享对象的生命周期问题完美解决。producer类看起来可能是这样的:

    class producer
    {
    public:
        // constructor & destructor
    // other public interfaces
    private:
        std::shared_ptr<blocking_queue> product_queue_;
        // other stuff
        …
    };

    等等,这里应该还有个问题。之前我们明明说好了producer不参与blocking_queue对象的生命周期管理。但是现在来看,似乎producer会对blocking_queue对象的生命周期产生非常大的影响。即便某一时刻我们认为blocking_queue对象需要被终结,但是因为producer对象的存在,这个blocking_queue始终无法被销毁。

    shared_ptr带来的新问题

    通过刚才的分析我们已经知道shared_ptr如何帮助我们解决线程共享对象的生命周期管理问题。但是问题解决的同时也引入了副作用,刻意延长了对象的生命周期。按照之前我的设计想法,显然在这里出现了一些出入。这里,我们更期望的结果是,如果这个队列对象还活着,那么producer可以向队列插入数据,如果队列已经死亡,那么producer啥事都不做。简单地说,就是shared_ptr提供了除检测对象有效性的功能外,还提供了生命周期的管理功能(生命周期的管理使得有效性的判断变得比较隐含)。但我们仅需要有效性的判断即可。

    这需要借助weak_ptr。

    使用weak_ptr检测对象的有效性

    weak_ptr如何检查对象的有效性?

    作为和shared_ptr一起被引入的智能指针,weak_ptr和shared_ptr可以说是一对搭档。shared_ptr专职提供生命周期管理,weak_ptr专职提供对象有效性判断。

    weak_ptr的接口等基本信息和用法可以参考这里

    从weak_ptr的构造函数可以知道,weak_ptr需要借力shared_ptr。它需要和一个shared_ptr对象关联,检测这个shared_ptr管理的对象是否还存活。

    对象有效性的检测可以通过weak_ptr::expired或者weak_ptr::lock的返回值来看。一般来说,使用lock的情况更普遍,因为对象有效,我们常常需要更进一步的操作。lock可以直接返回给我们一个shared_ptr对象。通过判断这个shared_ptr对象我们可以知道被管理的内存对象是否还存在。

    那么shared_ptr和weak_ptr该如何配合使用,这其中的基本原则是怎样的呢?

    一般来说,父对象持有子对象的shared_ptr,子对象持有父对象的weak_ptr(Wiki)。

    this指针的跨线程传递

    我们吧问题再说得广一点。前面说到的都是普通的指针,在C++里还有一个特殊的指针this。如果我们要将this跨线程传递怎么办?根据前面的分析,我们已经知道raw指针的跨线程传递是非常危险的。除此以外,this指针的跨线程传递还有跟多要考虑的东西。

    构造函数中,能否将this指针传递出去?

    不可以!因为对象还没有创建完成!你无法预知其他线程中的对象会在什么样的情况下使用这个this指针。

    既然不能传递this指针,那么我们就需要将this指针shared_ptr化。但是直接shared_ptr(this)又是不对的。举个例子:

    class example;
    int main()
    {
        example *e = new example;
        std::shared_ptr<example> sp1(e);
        std::shared_ptr<example> sp2(e);
    
        return 0;
    }

    sp1和sp2虽然都指向e,但是他们相互之间并不知道对方。如果要让shared_ptr相互了解对方,那么除了第一个shared_ptr对象是从raw指针创建除来的之外,其他shared_ptr都必须是从和这个shared_ptr对象相关的shared_ptr或者weak_ptr创建出来的。这其中的本质原因就是他们使用的不是同一份引用计数对象。

    shared_ptr(this),遇到的问题是一样的。

    如果确定要将this指针能够跨线程传递,那么必须(以example为例):

    1. example对象必须是一个在堆上的对象
    2. example对象被shared_ptr管理
    3. example类必须继承std::enable_shared_from_this
    4. 使用enable_shard_from_this::shared_from_this将this指针传递到其他线程中的对象

    == 完 ==

  • 相关阅读:
    FocusBI:MDX检索多维模型
    FocusBI:地产分析&雪花模型
    FocusBI:租房分析&星型模型
    FocusBI:《DW/BI项目管理》之SSIS执行情况
    FocusBI:租房分析可视化(PowerBI网址体验)
    Eclipse创建自定义HTML5,JSP模板
    小测试解析
    vue---组件通讯
    前期准备-Git篇
    npm install 关于 sass 屡次失败问题
  • 原文地址:https://www.cnblogs.com/wpcockroach/p/3611747.html
Copyright © 2020-2023  润新知