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

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

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

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

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

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

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

我们知道,多线程并发读写同一个对象,需要对读写操作进行同步以避免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 ( == 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 ;
}

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指针传递到其他线程中的对象

== 完 ==

最新文章

  1. uva 12169
  2. atitit &#160;opencv apiattilax总结&#160;约500个函数 .xlsx
  3. ReactNative之坑爹的在线安装
  4. xhEditor用法
  5. PHP Date ( I need to use)
  6. dedecms代码研究六
  7. Javascript基础系列之(四)数据类型 (数组 array)
  8. 20. 求阶乘序列前N项和
  9. reactjs入门到实战(六)---- ReactJS组件API详解
  10. UVA 565 565 Pizza Anyone? (深搜 +位运算)
  11. Django过滤器
  12. 深入理解 Java 虚拟机
  13. ELK之elasticsearch集群搭建
  14. C++使用默认参数的构造函数
  15. Lintcode: Unique Paths
  16. Spark(Hive) SQL中UDF的使用(Python)【转】
  17. 开源项目-SlideMenu和actionbarsherlock的配置
  18. Android之 解析XML文件(1)—— Pull解析
  19. 【c++】重载操作符
  20. ErrorUnable to tunnel through proxy. Proxy returns HTTP1.1 400 Bad Reques

热门文章

  1. 蒙特卡洛马尔科夫链(MCMC)
  2. Linux ftp 使用
  3. linux下使用多线程编写的聊天室
  4. python GUI输入窗口
  5. NSScanner知悉 (转)
  6. AWS EC2首次使用VPS
  7. Html5浏览器缓存 sessionStorage 与 localStorage
  8. jquery获得option的值和对option进行操作
  9. x-csrf-token
  10. PHPCMS V9 非超级管理员批量移动权限