首页 析构函数遇到多线程

析构函数遇到多线程

举报
开通vip

析构函数遇到多线程 1 当析构函数遇到多线程 ── C++ 中线程安全的对象回调 陈硕 (giantchen_AT_gmail) Blog.csdn.net/Solstice 摘要 编写线程安全的类不是难事,用同步原语保护内部状态即可。但是对象的生与死不能由 对象自身拥有的互斥器来保护。如何保证即将析构对象 x 的时候,不会有另一个线程正在 调用 x 的成员函数?或者说,如何保证在执行 x 的成员函数期间,对象 x 不会在另一个 线程被析构?如何避免这种竞态条件是 C++ 多线程编程面临的基本问题,可以借助 boos...

析构函数遇到多线程
1 当析构函数遇到多线程 ── C++ 中线程安全的对象回调 陈硕 (giantchen_AT_gmail) Blog.csdn.net/Solstice 摘要 编写线程安全的类不是难事,用同步原语保护内部状态即可。但是对象的生与死不能由 对象自身拥有的互斥器来保护。如何保证即将析构对象 x 的时候,不会有另一个线程正在 调用 x 的成员函数?或者说,如何保证在执行 x 的成员函数期间,对象 x 不会在另一个 线程被析构?如何避免这种竞态条件是 C++ 多线程编程面临的基本问 快递公司问题件快递公司问题件货款处理关于圆的周长面积重点题型关于解方程组的题及答案关于南海问题 ,可以借助 boost 的 shared_ptr 和 weak_ptr 完美解决。这也是实现线程安全的 Observer 模式的必备技术。 本文源自我在 2009 年 12 月上海 C++ 技术大会的一场演讲《当析构函数遇到多线 程》,内容略有增删。原始 PPT 可从 http://download.csdn.net/source/1982430 下载。 本文读者应具有 C++ 多线程编程经验,熟悉互斥器、竞态条件等概念,了解智能指针, 知道 Observer 设计模式。 当析构函数遇到多线程 by 陈硕 Blog.csdn.net/Solstice 2 目录 1 多线程下的对象生命期管理.................................................................................................2 线程安全的定义.................................................................................................................3 Mutex 与 MutexLock........................................................................................................3 一个线程安全的 Counter 示例........................................................................................3 2 对象的创建很简单.................................................................................................................4 3 销毁太难................................................................................................................................. 5 Mutex 不是办法................................................................................................................ 5 作为数据成员的 Mutex.................................................................................................... 6 4 线程安全的 Observer 有多难?.......................................................................................... 6 5 一些启发................................................................................................................................. 8 原始指针有何不妥?.........................................................................................................8 一个“解决办法”.............................................................................................................8 一个更好的解决办法.........................................................................................................9 一个万能的解决 方案 气瓶 现场处置方案 .pdf气瓶 现场处置方案 .doc见习基地管理方案.doc关于群访事件的化解方案建筑工地扬尘治理专项方案下载 .........................................................................................................9 6 神器 shared_ptr/weak_ptr......................................................................................................9 7 插曲:系统地避免各种指针错误.......................................................................................10 8 应用到 Observer 上............................................................................................................ 11 解决了吗?....................................................................................................................... 11 9 再论 shared_ptr 的线程安全..............................................................................................12 10 shared_ptr 技术与陷阱.......................................................................................................13 对象池............................................................................................................................... 15 enable_shared_from_this.................................................................................................. 16 弱回调............................................................................................................................... 17 11 替代方案?.........................................................................................................................19 其他语言怎么办...............................................................................................................19 12 心得与总结.........................................................................................................................20 总结................................................................................................................................... 20 13 附录:Observer 之谬........................................................................................................ 21 14 后记..................................................................................................................................... 21 1 多线程下的对象生命期管理 与其他面向对象语言不同,C++ 要求程序员自己管理对象的生命期,这在多线程环境 下显得尤为困难。当一个对象能被多个线程同时看到,那么对象的销毁时机就会变得模糊不 清,可能出现多种竞态条件: � 在即将析构一个对象时,从何而知是否有另外的线程正在执行该对象的成员函数? � 如何保证在执行成员函数期间,对象不会在另一个线程被析构? � 在调用某个对象的成员函数之前,如何得知这个对象还活着?它的析构函数会不会 刚执行到一半? 解决这些 race condition 是 C++ 多线程编程面临的基本问题。本文试图以 shared_ptr 一劳永逸地解决这些问题,减轻 C++ 多线程编程的精神负担。 当析构函数遇到多线程 by 陈硕 Blog.csdn.net/Solstice 3 线程安全的定义 依据《Java 并发编程实践》/《Java Concurrency in Practice》一书,一个线程安全的 class 应当满足三个条件: � 从多个线程访问时,其表现出正确的行为 � 无论操作系统如何调度这些线程,无论这些线程的执行顺序如何交织 � 调用端代码无需额外的同步或其他协调动作 依据这个定义,C++ 标准 excel标准偏差excel标准偏差函数exl标准差函数国标检验抽样标准表免费下载红头文件格式标准下载 库里的大多数类都不是线程安全的,无论 std::string 还是 std::vector 或 std::map,因为这些类通常需要在外部加锁。 Mutex 与 MutexLock 为了便于后文讨论,先约定两个工具类。我相信每个写C++ 多线程程序的人都实现过 或使用过类似功能的类,代码从略。 Mutex 封装临界区(Critical secion),这是一个简单的资源类,用 RAII 手法 [CCS:13]1111 封装互斥器的创建与销毁。临界区在 Windows 上是 CRITICAL_SECTION,是可重入的;在 Linux 下是 pthread_mutex_t,默认是不可重入的。Mutex 一般是别的 class 的数据成员。 MutexLock 封装临界区的进入和退出,即加锁和解锁。MutexLock 一般是个栈上对象, 它的作用域刚好等于临界区域。它的构造函数原型如下。这两个类都不允许拷贝构造和赋值。 explicit MutexLock::MutexLock(Mutex& m); 一个线程安全的 Counter 示例 编写单个的线程安全的 class 不算太难,只需用同步原语保护其内部状态。例如下面 这个简单的计数器类 Counter: class Counter : boost::noncopyable { // copy-ctor and assignment should be private by default for a class. public: Counter(): value_(0) {} int64_t value() const; int64_t increase(); int64_t decrease(); private: int64_t value_; mutable mutablemutable mutable Mutex mutex_; } int64_t Counter::value() constconstconstconst { MutexLock lock(mutex_); return value_; } 1 11 1 《C++ 编程 规范 编程规范下载gsp规范下载钢格栅规范下载警徽规范下载建设厅规范下载 》/《C++ Coding Standards》, by Herb Sutter and AndreiAlexandrescu, 2005. 条款 13. 当析构函数遇到多线程 by 陈硕 Blog.csdn.net/Solstice 4 int64_t Counter::increase() { MutexLock lock(mutex_); int64_t ret = value_++; return ret; } // In a real world, atomic operations are perferred. // 当然在实际项目中,这个 class 用原子操作更合理,这里用锁仅仅为了举例。 这个 class 很直白,一看就明白,也容易验证它是线程安全的。注意到它的 mutex_ 成 员是 mutable 的,意味着 const 成员函数如 Counter::value() 也能直接使用 non-const 的 mutex_。 尽管这个 Counter 本身毫无疑问是线程安全的,但如果 Counter 是动态创建的并透过 指针来访问,前面提到的对象销毁的 race condition 仍然存在。 2 对象的创建很简单 对象构造要做到线程安全,惟一的要求是在构造期间不要泄露 this 指针,即 � 不要在构造函数中注册任何回调 � 也不要在构造函数中把 this 传给跨线程的对象 � 即便在构造函数的最后一行也不行 之所以这样规定,是因为在构造函数执行期间对象还没有完成初始化,如果 this 被泄 露 (escape) 给了其他对象(其自身创建的子对象除外),那么别的线程有可能访问这个半 成品对象,这会造成难以预料的后果。 // 不要这么做 Don't do this. class Foo : public Observer { public: Foo(Observable* s) { s->register(this); // 错误 } virtual void update(); }; // 要这么做 Do this. class Foo : public Observer { // ... void observe(Observable* s) { // 另外定义一个函数,在构造之后执行 s->register(this); } }; Foo* pFoo = new Foo; Observable* s = getIt(); pFoo->observe(s); // 二段式构造 这也说明,二段式构造——即构造函数+initialize()——有时会是好办法,这虽然不符合 当析构函数遇到多线程 by 陈硕 Blog.csdn.net/Solstice 5 C++ 教条,但是多线程下别无选择。另外,既然允许二段式构造,那么构造函数不必主动 抛异常,调用端靠 initialize() 的返回值来判断对象是否构造成功,这能简化错误处理。 即使构造函数的最后一行也不要泄露 this,因为 Foo 有可能是个基类,基类先于派生 类构造,执行完 Foo::Foo() 的最后一行代码会继续执行派生类的构造函数,这时 most- derived class 的对象还处于构造中,仍然不安全。 相对来说,对象的构造做到线程安全还是比较容易的,毕竟曝光少,回头率为 0。而析 构的线程安全就不那么简单,这也是本文关注的焦点。 3 销毁太难 对象析构,这在单线程里不会成为问题,最多需要注意避免空悬指针(和野指针)。而 在多线程程序中,存在了太多的竞态条件。对一般成员函数而言,做到线程安全的办法是让 它们顺次执行,而不要并发执行,也就是让每个函数的临界区不重叠。这是显而易见的,不 过有一个隐含条件或许不是每个人都能立刻想到:函数用来保护临界区的互斥器本身必须是 有效的。而析构函数破坏了这一假设,它会把互斥器销毁掉。悲剧啊! Mutex 不是办法 Mutex 只能保证函数一个接一个地执行,考虑下面的代码,它试图用互斥锁来保护析 构函数: 有 A 和 B 两个线程,线程 A 即将销毁对象 x,而线程 B 正准备调用 x->update()。 尽管线程 A 在销毁对象之后把指针置为了 NULL,尽管线程 B 在调用 x 的成员函数之前 检查了指针 x 的值,还是无法避免一种 race condition: 1. 线程 A 执行到了析构函数的 (1) 处,已经持有了互斥锁,即将继续往下执行 2. 线程 B 通过了 if (x) 检测,阻塞在 (2) 处 接下来会发生什么,只有天晓得。因为析构函数会把 mutex_ 销毁,那么 (2) 处有可 能永远阻塞下去,有可能进入“临界区”然后 core dump,或者发生其他更糟糕的情况。 这个例子至少说明 delete 对象之后把指针置为 NULL 根本没用,如果一个程序要靠这 个来防止二次释放,说明代码逻辑出了问题。 Foo::~Foo() { MutexLock lock(mutex_); // free internal state (1)(1)(1)(1) } void Foo::update() { MutexLock lock(mutex_); // (2)(2)(2)(2) // make use of internal state } extern Foo* x; // visible by all threads // thread A delete x; x = NULL; // helpless // thread B if (x) { x->update(); } 当析构函数遇到多线程 by 陈硕 Blog.csdn.net/Solstice 6 作为数据成员的 Mutex 前面的例子说明,作为 class 数据成员的 Mutex 只能用于同步本 class 的其他数据成 员的读和写,它不能保护安全地析构。因为成员 mutex 的生命期最多与对象一样长,而析 构动作可说是发生在对象身故之后(或者身亡之时)。另外,对于基类对象,那么调用到基 类析构函数的时候,派生类对象的那部分已经析构了,那么基类对象拥有的 mutex 不能保 护整个析构过程。再说,析构过程本来也不需要保护,因为只有别的线程都访问不到这个对 象时,析构才是安全的,否则会有第 1 节谈到的竞态条件发生。 另外如果要同时读写本 class 的两个对象,有潜在的死锁可能,见 PPT2222 第 12 页的 swap() 和 operator=()。 4 线程安全的 Observer 有多难? 一个动态创建的对象是否还活着,光看指针(引用也一样)是看不出来的。指针就是指 向了一块内存,这块内存上的对象如果已经销毁,那么就根本不能访问 [CCS:99]3333(就像 free 之后的地址不能访问一样),既然不能访问又如何知道对象的状态呢?换句话说,判断一个 指针是不是野指针没有高效的办法。(万一原址又创建了一个新的对象呢?再万一这个新的 对象的类型异于老的对象呢?) 在面向对象程序设计中,对象的关系主要有三种: composition, aggregation 和 association。Composition(组合/复合)关系在多线程里不会遇到什么麻烦,因为对象 x 的 生命期由其惟一的拥有者 owner 控制,owner 析构的时候会把 x 也析构掉。从形式上看, x 是 owner 的直接数据成员,或者 scoped_ptr 成员,或者 owner 持有的容器的元素。 后两种关系在 C++ 里比较难办,处理不好就会造成内存泄漏或重复释放。Association (关联/联系)是一种很宽泛的关系,它表示一个对象 a 用到了另一个对象 b,调用了后者 的成员函数。从代码形式上看,a 持有 b 的指针(或引用),但是 b 的生命期不由 a 单 独控制。Aggregation(聚合)关系从形式上看与 association 相同,除了 a 和 b 有逻辑上 的整体与部分关系。如果 b 是动态创建的并在整个程序结束前有可能被释放,那么就会出 现第 1 节谈到的竞态条件。 那么似乎一个简单的解决办法是:只创建不销毁。程序使用一个对象池来暂存用过的对 象,下次申请新对象时,如果对象池里有存货,就重复利用现有的对象,否则就新建一个。 对象用完了,不是直接释放掉,而是放回池子里。这个办法当然有其自身的很多缺点,但至 少能避免访问失效对象的情况发生。 这种山寨办法的问题有: � 对象池的线程安全,如何安全地完整地把对象放回池子里,不会出现“部分放回” 的竞态?(线程 A 认为对象 x 已经放回了,线程 B 认为对象 x 还活着) � thread contention,这个集中化的对象池会不会把多线程并发的操作串行化? � 如果共享对象的类型不止一种,那么是重复实现对象池还是使用类模板? � 会不会造成内存泄露与分片?因为对象池占用的内存只增不减,而且多个对象池不 能共享内存(想想为何)。 2 22 2 下载网址见封面 3 33 3 同 1, 条款 99. 当析构函数遇到多线程 by 陈硕 Blog.csdn.net/Solstice 7 回到正题上来,如果对象 x 注册了任何非静态成员函数回调,那么必然在某处持有了 指向 x 的指针,这就暴露在了 race condition 之下。 一个典型的场景是 Observer 模式。 class Observer { public: virtual ~Observer() { } virtual void update() = 0; }; class Observable { public: void register(Observer* x); void unregister(Observer* x); void notifyObservers() { foreach Observer* x { // 这行是伪代码 x->update(); // (3)(3)(3)(3) } } // ... } 当 Observable 通知每一个 Observer 时 (3),它从何得知 Observer 对象 x 还活着? 要不在 Observer 的析构函数里解注册 (unregister)?恐难奏效。 struct Observer { virtual ~Observer() { } virtual void update() = 0; void observe(Observable* s) { s->register(this); subject_ = s; } virtual ~Observer() { // (4)(4)(4)(4) subject_->unregister(this); } Observable* subject_; }; 我们试着让 Observer 的析构函数去 unregister(this),这里有两个 race conditions。 其一:(4) 处如何得知 subject_ 还活着?其二:就算 subject_ 指向某个永久存在的对象, 那么还是险象环生: 1. 线程 A 执行到 (4) 处,还没有来得及 unregister 本对象 2. 线程 B 执行到 (3) 处,x 正好指向是 (4) 处正在析构的对象 当析构函数遇到多线程 by 陈硕 Blog.csdn.net/Solstice 8 那么悲剧又发生了,既然 x 所指的 Observer 对象正在析构,调用它的任何非静态成 员函数都是不安全的,何况是虚函数(C++ 标准对在构造函数和析构函数中调用虚函数的 行为有明确的规定,但是没有考虑并发调用的情况)。更糟糕的是,Observer 是个基类,执 行到 (4) 处时,派生类对象已经析构掉了,这时候整个对象处于将死未死的状态,core dump 恐怕是最幸运的结果。 这些 race condition 似乎可以通过加锁来解决,但在哪儿加锁,谁持有这些互斥锁, 又似乎不是那么显而易见的。要是有什么活着的对象能帮帮我们就好了,它提供一个 isAlive() 之类的程序函数,告诉我们那个对象还在不在。可惜指针和引用都不是对象,它 们是内建类型。 5 一些启发 指向对象的原始指针 (raw pointer) 是坏的,尤其当暴露给别的线程时。Observable 应 当保存的不是原始的 Observer*,而是别的什么东西,能分别 Observer 对象是否存活。类 似地,如果 Observer 要在析构函数里解注册(这虽然不能解决前面提到的 race condition, 但是在析构函数里打扫战场还是应该的),那么 subject_ 的类型也不能是原始的 Observable*。 有经验的 C++ 程序员或许会想到用智能指针,没错,这是正道,但也没那么简单,有 些关窍需要注意。这两处直接使用 shared_ptr 是不行的,会形成循环引用,直接造成资源 泄漏。别着急,后文会一一讲到。 原始指针有何不妥? 有两个指针 p1 和 p2,指向堆上的同一个对象 Object,p1 和 p2 位于不同的线程中 (左图)。假设线程 A 透过 p1 指针将对象销毁了(尽管把 p1 置为了 NULL),那么 p2 就 成了空悬指针(右图)。这是一种典型的 C/C++ 内存错误。 要想安全地销毁对象,最好让在别人(线程)都看不到的情况下,偷偷地做。4444 一个“解决办法” 一个解决空悬指针的办法是,引入一层间接性,让 p1 和 p2 所指的对象永久有效。 比如下图的 proxy 对象,这个对象,持有一个指向 Object 的指针。(从 C 语言的角度,p1 和 p2 都是二级指针。) 4 44 4 这正是垃圾回收的原理,所有人都用不到的东西一定是垃圾。 当析构函数遇到多线程 by 陈硕 Blog.csdn.net/Solstice 9 当销毁 Object 之后,proxy 对象继续存在,其值变为 0。而 p2 也没有变成空悬指针, 它可以通过查看 proxy 的内容来判断 Object 是否还活着。要线程安全地释放 Object 也不 是那么容易,race condition 依旧存在。比如 p2 看第一眼的时候 proxy 不是零,正准备 去调用 Object 的成员函数,期间对象已经被 p1 给销毁了。 问题在于,何时释放 proxy 指针呢? 一个更好的解决办法 为了安全地释放 proxy,我们可以引入引用计数,再把 p1 和 p2 都从指针变成对象 sp1 和 sp2。proxy 现在有两个成员,指针和计数器。 1. 一开始,有两个引用,计数值为 2, 2. sp1 析构了,引用计数的值减为 1, 3. sp2 也析构了,引用计数的值为 0,可以安全地销毁 proxy 和 Object 了。 慢着!这不就是引用计数型智能指针吗? 一个万能的解决方案 引入另外一层间接性,another layer of indirection,用对象来管理共享资源(如果把 Object 看作资源的话),亦即 handle/body 手法 (idiom)。当然,编写线程安全、高效的引 用计数 handle 的难度非凡,作为一名谦卑的程序员,用现成的库就行。 万幸,C++ 的 tr1 标准库里提供了一对神兵利器,可助我们完美解决这个头疼的问题。 6 神器 shared_ptr/weak_ptr shared_ptr 是引用计数型智能指针,在 boost 和 std::tr1 里都有提供,现代主流的 C++ 编译器都能很好地支持。shared_ptr 是一个类模板 (class template),它只有一个 类型参数,使用起来很方便。引用计数的是自动化资源管理的常用手法,当引用计数降为 0 时,对象(资源)即被销毁。weak_ptr 也是一个引用计数型智能指针,但是它不增加引用 次数,即弱 (weak) 引用。 当析构函数遇到多线程 by 陈硕 Blog.csdn.net/Solstice 10 shared_ptr 的基本用法和语意请参考手册或 教程 人力资源管理pdf成真迷上我教程下载西门子数控教程protel99se入门教程fi6130z安装使用教程 ,本文从略,这里谈几个关键点。 � shared_ptr 控制对象的生命期。shared_ptr 是强引用(想象成用铁丝绑住堆上的对象), 只要有一个指向 x 对象的 shared_ptr 存在,该 x 对象就不会析构。当指向对象 x 的 最后一个 shared_ptr 析构或 reset 的时候,x 保证会被销毁。 � weak_ptr 不控制对象的生命期,但是它知道对象是否还活着(想象成用棉线轻轻拴住 堆上的对象)。如果对象还活着,那么它可以提升 (promote) 为有效的 shared_ptr;如 果对象已经死了,提升会失败,返回一个空的 shared_ptr。“提升”行为是线程安全的。 � shared_ptr/weak_ptr 的“计数”在主流平台上是原子操作,没有用锁,性能不俗。 � shared_ptr/weak_ptr 的线程安全级别与 string 等 STL 容器一样,后面还会讲。 7 插曲:系统地避免各种指针错误 我同意孟岩说的“大部分用 C 写的上规模的软件都存在一些内存方面的错误,需要花 费大量的精力和时间把产品稳定下来。”内存方面的问题在 C++ 里很容易解决,我第一次 也是最后一次见到别人的代码里有内存泄漏是在 2004 年实习那会儿,自己写的C++ 程序 从来没有出现过内存方面的问题。 C++ 里可能出现的内存问题大致有这么几个方面: 1. 缓冲区溢出 2. 空悬指针/野指针 3. 重复释放 4. 内存泄漏 5. 不配对的 new[]/delete 6. 内存碎片 正确使用智能指针能很轻易地解决前面 5 个问题,解决第 6 个问题需要别的思路, 我会另文探讨。 1. 缓冲区溢出 ⇒ 用 vector/string 或自己编写 Buffer 类来管理缓冲区,自动记住 用缓冲区的长度,并通过成员函数而不是裸指针来修改缓冲区。 2. 空悬指针/野指针 ⇒ 用 shared_ptr/weak_ptr,这正是本文的主题 3. 重复释放 ⇒ 用 scoped_ptr,只在对象析构的时候释放一次 4. 内存泄漏 ⇒ 用 scoped_ptr,对象析构的时候自动释放内存 5. 不配对的 new[]/delete ⇒ 把 new[] 统统替换为 vector/scoped_array 正确使用上面提到的这几种智能指针并不难,其难度大概比学习使用 vector/list 这些 标准库组件还要小,与 string 差不多,只要花一周的时间去适应它,就能信手拈来。我认 为,在现代的 C++ 程序中一般不会出现 delete 语句,资源(包括复杂对象本身)都是通 过对象(智能指针或容器)来管理的,不需要程序员还为此操心。 需要注意一点:scoped_ptr/shared_ptr/weak_ptr 都是值语意,要么是栈上对象,或是 其他对象的直接数据成员,或是标准库容器里的元素。几乎不会有下面这种用法: shared_ptr* pFoo = new shared_ptr(new Foo); // WRONG semantic 当析构函数遇到多线程 by 陈硕 Blog.csdn.net/Solstice 11 还要注意,如果这几种智能指针是对象 x 的数据成员,而它的模板参数 T 是个 incomplete 类型,那么 x 的析构函数不能是默认的或内联的,必须在 .cpp 文件里边显式 定义,否则会有编译错或运行错。(原因请见 boost::checked_delete) 8 应用到 Observer 上 既然透过 weak_ptr 能探查对象的生死,那么 Observer 模式的竞态条件就很容易解 决,只要让 Observable 保存 weak_ptr 即可: class Observable // not 100% thread safe! { public: void register(weak_ptr x); void unregister(weak_ptr x); // 可用 std::remove/vector::erase 实现 void notifyObservers() { MutexLock lock(mutex_); Iterator it = observers_.begin(); while (it != observers_.end()) { shared_ptr obj(it->lock()); // 尝试提升,这一步是线程安全的 if (obj) { // 提升成功,现在引用计数值至少为 2 (想想为什么?) obj->update(); // 没有竞态条件,因为 obj 在栈上,对象不可能在本作用域内销毁 ++it; } else { // 对象已经销毁,从容器中拿掉 weak_ptr it = observers_.erase(it); } } } private: std::vector > observers_; // (5)(5)(5)(5) mutable Mutex mutex_; }; 就这么简单。前文代码 (3) 处的竞态条件已经弥补了。 解决了吗? 把 Observer* 替换为 weak_ptr 部分解决了 Observer 模式的线程安全, 但还有几个疑点: 不灵活,强制要求 Observer 必须以 shared_ptr 来管理; 不是完全线程安全,Observer 的析构函数会调用 subject_->unregister(this),万一 subject_ 已经不复存在了呢?为了解决它,又要求 Observable 本身是用 shared_ptr 管理 的,并且 subject_ 是个 weak_ptr; 线程争用 (thread contention),即 Observable 的三个成员函数都用了互斥器来同步, 这会造成 register 和 unregister 等待 notifyObservers,而后者的执行时间是无上限的, 因为它同步回调了用户提供的 update() 函数。我们希望 register 和 unregister 的执行时 当析构函数遇到多线程 by 陈硕 Blog.csdn.net/Solstice 12 间不会超过某个固定的上限,以免即便殃及无辜群众。 死锁,万一 update() 虚函数中调用了 (un)register 呢?如果 mutex_ 是不可重入的, 那么会死锁;如果 mutex_ 是可重入的,程序会面临迭代器失效(core dump 是最好的结 果),因为 vector observers_ 在遍历期间被无意识地修改了。这个问题乍看起来似乎没有解 决办法,除非在文档里做要求。(一个办法是:用可重入的 mutex_,把容器换为 std::list, 并把 ++it 往前挪一行。) 这些问题留到本文附录中去探讨,每个问题都是能解决的。 我个人倾向于使用不可重入的 Mutex,例如 pthreads 默认提供的那个,因为“要求 Mutex 可重入”本身往往意味着设计上出了问题。Java 的 intrinsic lock 是可重入的,因 为要允许 synchronized 方法相互调用,我觉得这也是无奈之举。 思考:如果把 (5) 处改为 vector > observers_;,会有什么后 果? 9 再论 shared_ptr 的线程安全 虽然我们借 shared_ptr 来实现线程安全的对象释放,但是 shared_ptr 本身不是 100% 线程安全的。它的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,读写操作不能原子化。 根据文档5555,shared_ptr 的线程安全级别和内建类型、标准库容器、string 一样,即: � 一个 shared_ptr 实体可被多个线程同时读取; � 两个的 shared_ptr 实体可以被两个线程同时写入,“析构”算写操作; � 如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁。 请注意,这是 shared_ptr 对象本身的线程安全级别,不是它管理的对象的线程安全级别。 要在多个线程中同时访问同一个 shared_ptr,正确的做法是: shared_ptr globalPtr; Mutex mutex; // No need for ReaderWriterLock void doit(const shared_ptr& pFoo); globalPtr 能被多个线程看到,那么它的读写需要加锁。注意我们不必用读写锁,而只 用最简单的互斥锁,这是为了性能考虑,因为临界区非常小,用互斥锁也不会阻塞并发读。 void read() { shared_ptr ptr; { MutexLock lock(mutex); ptr = globalPtr; // read globalPtr } // use ptr since here doit(ptr); } 5 55 5 http://www.boost.org/doc/libs/1_41_0/libs/smart_ptr/shared_ptr.htm 当析构函数遇到多线程 by 陈硕 Blog.csdn.net/Solstice 13 写入的时候也要加锁: void write() { shared_ptr newptr(new Foo); { MutexLock lock(mutex); globalPtr = newptr; // write to globalPtr } // use newptr since here doit(newptr); } 注意到 read() 和 write() 在临界区之外都没有再访问 globalPtr,而是用了一个指向 同一 Foo 对象的栈上 shared_ptr local copy。下面会谈到,只要有这样的 local copy 存在, shared_ptr 作为函数参数传递时不必复制,用 reference to const 即可。 10 shared_ptr 技术与陷阱 意外延长对象的生命期。shared_ptr 是强引用(铁丝绑的),只要有一个指向 x 对象 的 shared_ptr 存在,该对象就不会析构。而 shared_ptr 又是允许拷贝构造和赋值的(否 则引用计数就无意义了),如果不小心遗留了一个拷贝,那么对象就永世长存了。例如前面 提到如果把 (5) 处 observers_ 的类型改为 vector >,那么除非手 动调用 unregister,否则 Observer 对象永远不会析构。即便它的析构函数会调用 unregister,但是不去 unregister 就不会调用析构函数,这变成了鸡与蛋的问题。这也是 Java 内存泄露的常见原因。 另外一个出错的可能是 boost::bind,因为 boost::bind 会把参数拷贝一份,如果参数 是个 shared_ptr,那么对象的生命期就不会短于 boost::function 对象: class Foo { void doit(); }; boost::function func; shared_ptr pFoo(new Foo); func = bind(&Foo::doit, pFoo); // long life foo 这里 func 对象持有了 shared_ptr 的一份拷贝,有可能会不经意间延长倒数第 二行创建的 Foo 对象的生命期。 函数参数。因为要修改引用计数(而且拷贝的时候通常要加锁),shared_ptr 的拷贝开 销比拷贝原始指针要高,但是需要拷贝的时候并不多。多数情况下它可以以 reference to const 方式传递,一个线程只需要在最外层函数有一个实体对象,之后都可以用 reference to const 来使用这个 shared_ptr。例如有几个个函数都要用到 Foo 对象: void save(const shared_ptr& pFoo); void validateAccount(const Foo& foo); bool validate(const shared_ptr& pFoo) { 当析构函数遇到多线程 by 陈硕 Blog.csdn.net/Solstice 14 // ... validateAccount(*pFoo); // ... } 那么在通常情况下, void onMessage(const string& buf) { shared_ptr pFoo(new Foo(buf)); // 只要在最外层持有一个实体,安全不成问题 if (validate(pFoo)) { save(pFoo); } } 遵照这个规则,基本上不会遇到反复拷贝 shared_ptr 导致的性能问题。另外由于 pFoo 是栈上对象,不可能被别的线程看到,那么读取始终是线程安全的。 析构动作在创建时被捕获。这是一个非常有用的特性,这意味着: � 虚析构不再是必须的。 � shared_ptr 可以持有任何对象,而且能安全地释放 � shared_ptr 对象可以安全地跨越模块边界,比如从 DLL 里返回,而不会造成从模 块 A 分配的内存在模块 B 里被释放这种错误。 � 二进制兼容性,即便 Foo 对象的大小变了,那么旧的客户代码任然可以使用新的 动态库,而无需重新编译(这要求 Foo 的头文件中不出现访问对象的成员的 inline 函数)。 � 析构动作可以定制。 这个特性的实现比较巧妙,因为 shared_ptr 只有一个模板参数,而“析构行为” 可以是函数指针,仿函数 (functor) 或者其他什么东西。这是泛型编程和面向对象编程的一 次完美结合。有兴趣的同学可以参考 Scott Meyers 的文章6666。 这个技术在后面的对象池中还会用到。 析构所在的线程。对象的析构是同步的,当最后一个指向 x 的 shared_ptr 离开其作 用域的时候,x 会同时在同一个线程析构。这个线程不一定是对象诞生的线程。这个特性是 把双刃剑:如果对象的析构比较耗时,那么可能会拖慢关键线程的速度(如果最后一个 shared_ptr 引发的析构发生在关键线程);同时,我们可以用一个单独的线程来专门做析构, 通过一个 BlockingQueue > 把对象的析构都转移到那个专用线程,从而 解放关键线程。 现成的 RAIIRAIIRAIIRAII handlehandlehandlehandle。我认为 RAII (资源获取即初始化)是 C++ 语言区别与其他所 有编程语言的最重要的手法,一个不懂 RAII 的 C++ 程序员不是一个合格的 C++ 程序员。 原来的 C++ 教条是“new 和 delete 要配对,new 了之后要记着 delete”,如果使用 RAII, 要改成“每一个明确的资源配置动作(例如 new)都应该在单一语句中执行,并在该语句 中立刻将配置获得的资源交给 handle 对象(如 shared_ptr),程序中一般不出现 delete” (出处见脚注 1)。 shared_ptr 是管理共享资源的利器,需要注意避免循环引用,通常的做 法是 owner 持有指向 A 的 shared_ptr,A 持有指向 owner 的 weak_ptr。 6 66 6 http://www.artima.com/cppsource/top_cpp_aha_moments.html 当析构函数遇到多线程 by 陈硕 Blog.csdn.net/Solstice 15 对象池 假设有 Stock 类,代表一只股票的价格。每一只股票有一个惟一的字符串标识,比如 Google 的 key 是 "NASDAQ:GOOG",IBM 是 "NYSE:IBM"。Stock 对象是个主动对象,它 能不断获取新价格。为了节省系统资源,同一个程序里边每一只出现的股票只有一个 Stock 对象,如果多处用到同一只股票,那么 Stock 对象应该被共享。如果某一只股票没有再在 任何地方用到,其对应的 Stock 对象应该析构,以释放资源,这隐含了“引用计数”。 为了达到上述要求,我们可以设计一个对象池 StockFactory。它的接口很简单,根据 key 返回 Stock 对象。我们已经知道,在多线程程序中,既然对象可能被销毁,那么返回 shared_ptr 是合理的。自然地,我们写出如下代码。(可惜是错的) class StockFactory : boost::noncopyable { // question
本文档为【析构函数遇到多线程】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
该文档来自用户分享,如有侵权行为请发邮件ishare@vip.sina.com联系网站客服,我们会及时删除。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。
本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。
网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。
下载需要: 免费 已有0 人下载
最新资料
资料动态
专题动态
is_827573
暂无简介~
格式:pdf
大小:545KB
软件:PDF阅读器
页数:22
分类:互联网
上传时间:2011-11-10
浏览量:18