当析构函数遇到多线程──C++ 中线程安全的对象回调

本文探讨了C++多线程环境下,如何确保对象析构的安全,尤其是在涉及到回调函数和Observer模式时。文章指出,简单的互斥锁无法解决析构过程中的线程安全问题,并提出使用`shared_ptr`和`weak_ptr`来解决对象生命周期管理和线程安全回调的挑战。通过`enable_shared_from_this`和`weak_ptr`的结合,实现了在对象析构时的弱回调,确保了即使对象被销毁,也不会触发未预期的成员函数调用。此外,文章还讨论了其他语言的处理方式和一些设计原则,强调了在多线程编程中避免使用跨线程对象的重要性。
摘要由CSDN通过智能技术生成

 当析构函数遇到多线程
── C++ 中线程安全的对象回调

 

陈硕 (giantchen_AT_gmail)

Blog.csdn.net/Solstice

请尽量阅读本文 PDF 版:http://www.cppblog.com/Files/Solstice/dtor_meets_mt.pdf 

豆丁亦可,内容略微滞后: http://www.docin.com/p-42460300.html

这里是从 word 直接粘贴过来,脚注链接都丢失了。

 

摘要

编写线程安全的类不是难事,用同步原语保护内部状态即可。但是对象的生与死不能由对象自身拥有的互斥器来保护。如何保证即将析构对象 x  的时候,不会有另一个线程正在调用 的成员函数?或者说,如何保证在执行 的成员函数期间,对象 不会在另一个线程被析构?如何避免这种竞态条件是 C++ 多线程编程面临的基本问题,可以借助 boost 的 shared_ptr 和 weak_ptr 完美解决。这也是实现线程安全的 Observer 模式的必备技术。


本文源自我在 2009  12 月上海 C++ 技术大会的一场演讲《当析构函数遇到多线程》,内容略有增删。原始 PPT 可从 http://download.csdn.net/source/1982430 下载,或者在 http://www.docin.com/p-41918023.html 直接观看。

 

本文读者应具有 C++ 多线程编程经验,熟悉互斥器、竞态条件等概念,了解智能指针,知道 Observer 设计模式。

目录

 

1 多线程下的对象生命期管理 2

线程安全的定义 3

Mutex 与 MutexLock 3

一个线程安全的 Counter 示例 3

2 对象的创建很简单 4

3 销毁太难 5

Mutex 不是办法 5

作为数据成员的 Mutex 6

4 线程安全的 Observer 有多难? 6

5 一些启发 8

原始指针有何不妥? 8

一个“解决办法” 8

一个更好的解决办法 9

一个万能的解决方案 9

6 神器 shared_ptr/weak_ptr 10

7 插曲:系统地避免各种指针错误 10

8 应用到 Observer 11

解决了吗? 11

9 再论 shared_ptr 的线程安全 12

10 shared_ptr 技术与陷阱 13

对象池 15

enable_shared_from_this 17

弱回调 17

11 替代方案? 19

其他语言怎么办 19

12 心得与总结 19

总结 20

13 附录:Observer 之谬 20

14 后记 21

 

多线程下的对象生命期管理

与其他面向对象语言不同,C++ 要求程序员自己管理对象的生命期,这在多线程环境下显得尤为困难。当一个对象能被多个线程同时看到,那么对象的销毁时机就会变得模糊不清,可能出现多种竞态条件:


在即将析构一个对象时,从何而知是否有外的线程正在执行该对象的成员函数?

如何保证在执行成员函数期间,对象不会在另一个线程被析构?

在调用某个对象的成员函数之前,如何得知这个对象还活着?

 

解决这些 race condition 是 C++ 多线程编程面临的基本问题。本文试图以 shared_ptr 一劳永逸地解决这些问题,减轻 C++ 多线程编程的精神负担。


线程安全的定义

 

依据《Java 并发编程实践》/Java Concurrency in Practice》一书,一个线程安全的 class 应当满足三个条件:

从多个线程访问时,其表现出正确的行为

无论操作系统如何调度这些线程,无论这些线程的执行顺序如何交织

调用端代码无需额外的同步或其他协调动作

 

依据这个定义,C++ 标准库里的大多数类都不是线程安全的,无论 std::string 还是 std::vector 或 std::map,因为这些类通常需要在外部加锁。


Mutex 与 MutexLock

 

为了便于后文讨论,先约定两个工具类。我相信每个写C++ 多线程程序的人都实现过或使用过类似功能的类,代码从略。

 

Mutex 封装临界区(Critical secion),这是一个简单的资源类,用 RAII 手法 [CCS:13]封装互斥器的创建与销毁。临界区在 Windows 上是 CRITICAL_SECTION,是可重入的;在 Linux 下是 pthread_mutex_t,默认是不可重入的。Mutex 一般是别的 class 的数据成员。

 

MutexLock 封装临界区的进入和退出,即加锁和解锁。MutexLock 一般是个栈上对象,它的作用域刚好等于临界区域。它的构造函数原型为 MutexLock::MutexLock(Mutex& m);

 

这两个 classes 都不允许拷贝构造和赋值。


一个线程安全的 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 Mutex mutex_;

}

 

int64_t Counter::value() const

{

  MutexLock lock(mutex_);

  return value_;

}

 

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 仍然存在。


对象的创建很简单

 

对象构造要做到线程安全,惟一的要求是在构造期间不要泄露 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);

  }

};

评论 59
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值