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

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

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

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

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

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

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

我们知道,多线程并发读写同一个对象,需要对读写操作进行同步以避免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指针传递到其他线程中的对象

== 完 ==

shared_ptr和多线程的更多相关文章

  1. C++之shared_ptr总结

    转自 http://blog.csdn.net/u013696062/article/details/39665247 Share_ptr也是一种智能指针.类比于auto_ptr学习.所以推荐先学习a ...

  2. 智能指针shared_ptr, auto_ptr, scoped_ptr, weak_ptr总结

    看这里: http://blog.csdn.net/lollipop_jin/article/details/8499530 shared_ptr可以多线程同时读,但是涉及到写,需要加锁. share ...

  3. [并发并行]_[C/C++]_[C++标准库里的线程安全问题]

    场景 1.写普通的程序时, 经常会使用cout来做输出, 每个进程只有一个控制台, 如果多线程调用cout时会出状况吗? 2.之所以研究cout会不会在并发下调用有问题, 是因为曾经有一个bug的崩溃 ...

  4. 一些关于VC++开发的笔记

    通常程序卡住了,主要有双方面的可能: (1)死循环了 (2)死锁了 要确定是否是死循环.能够通过调试器(经常使用Windbg)查看线程执行时间,假设隔了一段会儿两次查看的执行时间有非常大区别,那么非常 ...

  5. muduo总结

    总结说的有的过大,算是对自己学习的一个总结.兴许会不断补充. 模型总结 muduo是基于非堵塞的IO和事件驱动的网络库. muduo的总体结构时one loop per thread+threadpo ...

  6. 为什么多线程读写 shared_ptr 要加锁?

    https://www.cnblogs.com/Solstice/archive/2013/01/28/2879366.html 为什么多线程读写 shared_ptr 要加锁? 陈硕(giantch ...

  7. C++11 shared_ptr 智能指针 的使用,避免内存泄露

    多线程程序经常会遇到在某个线程A创建了一个对象,这个对象需要在线程B使用, 在没有shared_ptr时,因为线程A,B结束时间不确定,即在A或B线程先释放这个对象都有可能造成另一个线程崩溃, 所以为 ...

  8. 如何在多线程leader-follower模式下正确的使用boost::asio。

    #include <assert.h> #include <signal.h> #include <unistd.h> #include <iostream& ...

  9. shared_ptr:资源管理利器

    如果你还在使用传统的C++,那么可以肯定堆内存的管理让你头痛过!在传统的C++领域,堆内存管理上我们能借用的现成工具就只有auto_ptr.但是很不幸用auto_ptr管理堆内存简直就是个错误.aut ...

随机推荐

  1. JavaScript中的防篡改对象

    由于JavaScript共享的特性,任何对象都可以被放在同一环境下运行的代码修改. 例如: var person = {name:"caibin'} person.age = 21; 即使第 ...

  2. JQuery_DOM 节点操作之复制、替换和 删除节点

    一.复制节点 <script type="text/javascript" src="jquery-1.12.3.min.js"></scri ...

  3. Working with Data » Getting started with ASP.NET Core and Entity Framework Core using Visual Studio » 读取关系数据

    Reading related data¶ 9 of 9 people found this helpful The Contoso University sample web application ...

  4. 3个sprint的团队贡献分

    第一次冲刺贡献分   成员名字 贡献分 101丘娟 23 108周诗琦 26 107杨晓霞 24 124陈程 27     第二次冲刺贡献分   成员名字 贡献分 101丘娟 23 108周诗琦 27 ...

  5. 【教程】16岁黑客如何把Windows 95装进智能手表?【转】

    来自美国佐治亚州的16岁黑客Corbin Davenport十分喜欢摆弄电子产品,最近他刚到手了一台三星Gear Live,并开始把玩起来.他发现Android Wear作为Android系统的改版并 ...

  6. 苹果MacBook Air安装win7

    同事的一台mba,说iOS不习惯,希望装一个win7系统.机器看上去很小巧精致,运行iOS速度飞快.试着点了下鼠标,没反应,翻过来看了下,有个电源开关.拨了一下,细小的指示灯闪了闪,应该加上电了.唉, ...

  7. IdentityServer4 简单使用,包括api访问控制,openid的授权登录,js访问

    写在前面 先分享一首数摇:http://music.163.com/m/song?id=36089751&userid=52749763 其次是:对于identityServer理解并不是特别 ...

  8. vue.common.js?e881:433 TypeError: Cannot read property 'nodeName' of undefined

    我觉得吧,是这么个原因,就是响应式要找这个node改它的内容,没找着,就报错了. 用computed监控vuex的state属性,绑定到页面上,如果这个属性改了,因为响应式,那么就要更改页面,如果页面 ...

  9. UEditor-从客户端(editorValue="<p>asd</p>")中检测到有潜在危险的 Request.Form 值。

    在用富文本编辑器时经常会遇到的问题是asp.net报的”检测到有潜在危险的 Request.Form 值“一般的解法是在aspx页面   page  标签中加上 validaterequest='fa ...

  10. 【转】如何保护自己的QQ号

    账号丢失的原因 账号被注销 长时间未登陆 如果你的QQ号是普通号码,在连续三个月不登陆的情况下,腾讯公司会自动收回你的账号,也就意味着这个QQ号码从此再也不属于你了,会员号码是不会被收回的,要想不被收 ...