标签:
Please indicate the source: http://blog.csdn.net/gaoxiangnumber1
Welcome to my github: https://github.com/gaoxiangnumber1
?读者应具有C++多线程编程经验,熟悉互斥器、竞态条件等概念,了解智能指针,知道Observer设计模式。
1.1 当析构函数遇到多线程
?C++要求程序员自己管理对象的生命期。当一个对象能被多个线程同时看到时,对象的销毁时机就会变得模糊不清,可能出现多种竞态条件(race condition):
?在析构一个对象前,如何知道此刻是否有别的线程正在执行该对象的成员函数?
?如何保证在执行成员函数时,对象不会在另一个线程被析构?
?在调用某个对象的成员函数之前,如何得知这个对象还活着?它的析构函数会不会碰巧执行到一半?
1.1.1 线程安全的定义
?依据[JCP],一个线程安全的class应当满足以下三个条件:
1.多个线程同时访问时,其表现出正确的行为。
2.无论操作系统如何调度这些线程,无论这些线程的执行顺序如何交织(interleaving)。
3.调用端代码无须额外的同步或其他协调动作。
?C++标准库里的大多数class都不是线程安全的,包括std::string、std::vector、std::map等,因为这些class通常需要在外部加锁才能供多个线程同时访问。
1.1.2MutexLock与MutexLockGuard
?两个工具类代码见§2.4。
?MutexLock封装临界区(critical section),是一个资源类,用RAII手法[CCS,条款13] 封装互斥器的创建与销毁。临界区在Windows上是struct CRITI-CAL_SECTION,是可重入的;在 Linux 下是 pthread_mutex_t,默认是不可重入的(可重入与不可重入的讨论见 §2.1.1)。MutexLock 一般是别的 class 的数据成员。
?Resource Acquisition Is Initialization机制解决的是这样一个问题:
在C++中,如果在这个程序段结束时需要完成资源释放工作,那么正常情况下没有问题,但是当一个异常抛出时,释放资源的语句就不会被执行。确保能运行资源释放代码的地方就是在这个程序段(栈帧)中放置的对象的析构函数了,因为stack winding会保证它们的析构函数都会被执行。
?RAII (Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。RAII的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。
?将初始化和资源释放都移动到一个包装类中的好处:
1.保证了资源的正常释放
2.省去了在异常处理中冗长、重复的清理逻辑,确保了代码的异常安全。
3.简化代码体积。
?资源管理技术的关键在于:要保证资源的释放顺序与获取顺序严格相反。这使我们联想到局部对象的创建和销毁过程。管理局部对象的任务非常简单,因为它们的创建和销毁工作是由系统自动完成的。我们只需在某个作用域(scope)中定义局部对象(这时系统自动调用构造函数以创建对象),然后就可以放心地使用;当控制流程超出这个作用域的范围时,系统会自动调用析构函数,从而销毁该对象。将资源抽象为类,用局部对象来表示资源,把管理资源的任务转化为管理局部对象的任务。这就是 RAII 惯用法的真谛。
?MutexLockGuard封装临界区的进入和退出,即加锁和解锁。MutexLockGuard 一般是个栈上对象,它的作用域刚好等于临界区域。
?这两个 class 都不允许拷贝构造和赋值,它们的使用原则见 § 2.1。
1.1.3 一个线程安全的 Counter 示例
编写单个的线程安全的 class只需用同步原语保护其内部状态。
Code
#include<pthread.h>
#include<assert.h>
#include <boost/noncopyable.hpp>
class MutexLock// : boost ::noncopyable
{
private:
pthread_mutex_t mutex_;
pid_t holder_;
public:
MutexLock() : holder_(0)
{
pthread_mutex_init(&mutex_, NULL);
}
~MutexLock()
{
assert(holder_ == 0);
pthread_mutex_destroy(&mutex_);
}
void Lock()
{
pthread_mutex_lock(&mutex_);
// holder_ = CurrentThread::tid();
}
void Unlock()
{
holder_ = 0;
pthread_mutex_unlock(&mutex_);
}
};
class MutexLockGuard : boost::noncopyable
{
private:
MutexLock &mutex_;
public:
explicit MutexLockGuard(MutexLock &mutex) : mutex_(mutex)
{
mutex_.lock();
}
~MutexLockGuard()
{
mutex_.unlock();
}
};
#define MutexLockGuard(x) static_assert(false, "missing mutex guard var name")
class Counter : boost::noncopyable
{
private:
int64_t value_;
mutable MutexLock mutex_;
public:
Counter() : value_(0) {}
int64_t Value() const
{
MutexLockGuard lock(mutex_);
return value_;
}
int64_t GetAndIncrease()
{
MutexLockGuard lock(mutex_);
int64_t ret = value_++;
return ret;
}
};
int main()
{
return 0;
}
1.assert.h干什么的?
?assert.h是C标准库的一个头文件,该头文件的目的是提供一个assert的宏定义。assert只是对所给的表达式求值,然后如果该值为真则正常运行,否则报错,并调用abort(),产生异常中断,exit出来。该宏可以屏蔽掉,只需在包含assert.h之前#define NDEBUG,想开再#undef。
2.assert(holder == 0);的意思
?#include
Widget w1;
Widget w2(w1); // error
Widget w2 = w1; // error
w2 = w1; // error
5.int64_t代表什么?
?8字节的signed integer
?这个 class 是线程安全的。每个 Counter 对象有自己的 mutex_,因此不同对象之间不构成锁争用(lock contention)。两个线程有可能同时执行 L24(int64_t ret = value_++;),前提是它们访问的不是同一个Counter 对象。mutex_ 成员是 mutable,意味着 const 成员函数如 Counter::value() 也能直接使用 non-const 的 mutex_。
?尽管这个 Counter 本身是线程安全的,但如果 Counter 是动态创建的并通过指针来访问,前面提到的对象销毁的 race condition 仍然存在。
1.2 对象的创建很简单
?对象构造要做到线程安全,唯一的要求是在构造期间不要泄露 this 指针,即
1.不要在构造函数中注册任何回调;
2.不要在构造函数中把 this 传给跨线程的对象;
3.即便在构造函数的最后一行也不行。
?之所以这样规定是因为在构造函数执行期间对象还没有完成初始化,如果this被泄露(escape)给了其他对象(其自身创建的子对象除外),那么别的线程有可能访问这个半成品对象,这会造成难以预料的后果。
recipes/thread/test/Observer.cc
#include <algorithm>
#include <vector>
#include <stdio.h>
class Observable;
class Observer
{
public:
virtual ~Observer();
virtual void update() = 0;
void observe(Observable* s);
protected:
Observable* subject_;
};
class Observable
{
public:
void register_(Observer* x);
void unregister(Observer* x);
void notifyObservers()
{
for (size_t i = 0; i < observers_.size(); ++i)
{
Observer* x = observers_[i];
if (x)
{
x->update(); // (3)
}
}
}
private:
std::vector<Observer*> observers_;
};
Observer::~Observer()
{
subject_->unregister(this);
}
void Observer::observe(Observable* s)
{
s->register_(this);
subject_ = s;
}
void Observable::register_(Observer* x)
{
observers_.push_back(x);
}
void Observable::unregister(Observer* x)
{
std::vector<Observer*>::iterator it = std::find(observers_.begin(), observers_.end(), x);
if (it != observers_.end())
{
std::swap(*it, observers_.back());
observers_.pop_back();
}
}
// ---------------------
class Foo : public Observer
{
virtual void update()
{
printf("Foo::update() %p\n", this);
}
};
int main()
{
Foo* p = new Foo;
Observable subject;
p->observe(&subject);
subject.notifyObservers();
delete p;
subject.notifyObservers();
}
1.virtual void update() = 0;
?纯虚函数(pure virtual function).申明格式如下:
class Shape
{
public:
virtual void Show() = 0;
};
?在普通的虚函数后面加上” = 0”这样就声明了一个pure virtual function.
?什么情况下使用纯虚函数(pure virtual function)?
1)想在基类中抽象出一个方法,且该基类只能被继承,而不能被实例化;
2)这个方法必须在派生类(derived class)中被实现;
2.printf(“Foo::update() %p\n”, this);
?p 显示一个指针,即地址。
?// 不要这么做(Don’t do this.)
class Foo : public Observer // Observer 的定义见第 10 页
{
public:
Foo(Observable* s)
{
s->register_(this); // 错误,非线程安全
}
virtual void update();
};
?对象构造的正确方法:
// 要这么做(Do this.)
class Foo : public Observer
{
public:
Foo();
virtual void update();
// 另外定义一个函数,在构造之后执行回调函数的注册工作
void observe(Observable* s)
{
s->register_(this);
}
};
Foo* pFoo = new Foo;
Observable* s = getSubject();
pFoo->observe(s); // 二段式构造,或者直接写 s->register_(pFoo);
?二段式构造(构造函数+ initialize())有时是好办法,这虽然不符合C++教条,但多线程下别无选择。既然允许二段式构造,那么构造函数不必主动抛异常,调用方靠initialize()的返回值来判断对象是否构造成功,这能简化错误处理。
?即使构造函数的最后一行也不要泄露 this,因为 Foo 有可能是个基类,基类先于派生类构造,执行完 Foo::Foo() 的最后一行代码还会继续执行派生类的构造函数,这时 most-derived class 的对象还处于构造中,仍然不安全。
1.3 销毁太难
?对象析构在单线程里不构成问题,最多需要注意避免空悬指针和野指针。空悬指针(dangling pointer)指向已经销毁的对象或已经回收的地址,野指针(wild pointer)指的是未经初始化的指针(http://en.wikipedia.org/wiki/Dangling_pointer)。
?在多线程程序中,对一般成员函数而言,做到线程安全的办法是让它们顺次执行,而不要并发执行(关键是不要同时读写共享状态),也就是让每个成员函数的临界区不重叠。隐含条件:成员函数用来保护临界区的互斥器本身必须是有效的。而析构函数破坏了这一假设,它会把 mutex 成员变量销毁掉。
1.3.1 mutex 不是办法
?mutex只能保证函数一个接一个地执行,下面的代码试图用互斥锁来保护析构函数:(注意(1)和(2)两处标记)
Foo::~Foo()
{
MutexLockGuard lock(mutex_);
// free internal state (1)
} void Foo::update()
{
MutexLockGuard lock(mutex_); // (2)
// make use of internal state
}
?此时,有 A、B 两个线程都能看到 Foo 对象 x,线程 A 即将销毁 x,而线程 B 正准备调用 x->update()。
extern Foo* x; // visible by all threads
// thread A
delete x;
x = NULL; // helpless // thread B
if (x)
{
x->update();
}
?尽管线程 A 在销毁对象之后把指针置为了 NULL,线程 B 在调用 x 的成员函数之前检查了指针 x 的值,但还是无法避免一种 race condition:
1.线程 A 执行到了析构函数的 (1) 处,已经持有了互斥锁,即将继续往下执行。
2.线程 B 通过了 if (x) 检测,阻塞在 (2) 处。
?接下来会发生什么未知。因为析构函数会把 mutex_ 销毁,那么 (2) 处有可能永远阻塞下去,有可能进入“临界区”,然后 core dump,或者发生其他更糟糕的情况。
?core:内存、核心;
dump:抛出,扔出;
core dump:当某程序崩溃的一瞬间,内核会抛出当时该程序进程的内存详细情况,存储在一个名叫core.xxx(xxx为一个数字,比如core.699)的文件中。
?core dump是指对应程序由于各种异常或者bug导致在运行过程中异常退出或者中止,并且在满足一定条件下会产生一个叫做core的文件。
?通常情况下,core文件会包含程序运行时的内存,寄存器状态,堆栈指针,内存管理信息还有各种函数调用堆栈信息等,我们可以理解为是程序工作当前状态存储生成的文件,通过工具分析这个文件,我们可以定位到程序异常退出的时候对应的堆栈调用等信息,找出问题所在并解决。
?这个例子说明 delete 对象之后把指针置为 NULL 根本没用,如果一个程序要靠这个来防止二次释放,说明代码逻辑出了问题。
1.3.2 作为数据成员的 mutex 不能保护析构
?前面的例子说明,作为 class 数据成员的 MutexLock 只能用于同步本 class 的其他数据成员的读和写,它不能保护安全地析构。因为 MutexLock 成员的生命期最多与对象一样长,而析构动作可说是发生在对象身亡之后(或者身亡之时)。
?对于基类对象,调用到基类析构函数的时候,派生类对象的那部分已经析构了,那么基类对象拥有的 MutexLock 不能保护整个析构过程。
?析构过程本来也不需要保护,因为只有别的线程都访问不到这个对象时,析构才是安全的,否则会有§1.1谈到的竞态条件发生。
?如果要同时读写一个 class 的两个对象, 有潜在的死锁可能。比方说有swap()这个函数:
void swap(Counter& a, Counter& b)
{
MutexLockGuard aLock(a.mutex_); // potential dead lock
MutexLockGuard bLock(b.mutex_);
int64_t value = a.value_;
a.value_ = b.value_;
b.value_ = value;
}
?如果线程 A 执行 swap(a, b); 而同时线程 B 执行 swap(b, a);,就有可能死锁。operator=() 也是类似的道理。
Counter& Counter::operator=(const Counter& rhs)
{
if (this == &rhs)
return *this;
MutexLockGuard myLock(mutex_); // potential dead lock
MutexLockGuard itsLock(rhs.mutex_);
value_ = rhs.value_; // 改成 value_ = rhs.value() 会死锁
return *this;
}
?一个函数如果要锁住相同类型的多个对象,为了保证始终按相同的顺序加锁,我们可以比较 mutex 对象的地址,始终先加锁地址较小的 mutex。
1.4 线程安全的 Observer 有多难
?一个动态创建的对象是否还活着,光看指针是看不出来的,引用也一样看不出来。指针就是指向了一块内存,这块内存上的对象如果已经销毁,那么就根本不能访问 [CCS,条款 99] (就像 free(3) 之后的地址不能访问一样),既然不能访问又如何知道对象的状态呢?
?判断一个指针是不是合法指针没有高效的办法,这是C/C++指针问题的根源。万一原址又创建了一个新的对象呢?再万一这个新的对象的类型异于老的对象呢?
?在面向对象程序设计中,对象的关系主要有三种: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.1 谈到的竞态条件。
?一个简单的办法是:只创建不销毁。程序使用一个对象池来暂存用过的对象,下次申请新对象时,如果对象池里有存货,就重复利用现有的对象,否则就新建一个。对象用完了,不是直接释放掉,而是放回池子里。这个办法有缺点,但至少能避免访问失效对象的情况发生。缺点有:
1.对象池的线程安全,如何安全地、完整地把对象放回池子里,防止出现“部分放回”的竞态?(线程 A 认为对象 x 已经放回了,线程 B 认为对象 x 还活着。)
2.全局共享数据引发的 lock contention,这个集中化的对象池会不会把多线程并发的操作串行化?
3.如果共享对象的类型不止一种,那么是重复实现对象池还是使用类模板?
4.会不会造成内存泄漏与分片?因为对象池占用的内存只增不减,而且多个对象池不能共享内存。
?如果对象 x 注册了任何非静态成员函数回调,那么必然在某处持有了指向 x 的指针,这就暴露在了 race condition 之下。
?一个典型的场景是 Observer 模式(代码见 recipes/thread/test/Observer.cc)。
?当 Observable 通知每一个 Observer 时 (L17),它从何得知 Observer 对象 x 还活着?试试在 Observer 的析构函数里调用 unregister() 来解注册?恐难奏效。
?我们试着让 Observer 的析构函数去调用 unregister(this),这里有两个 race conditions。
其一:L32 如何得知 subject_ 还活着?
其二:就算 subject_指向某个永久存在的对象,那么还是险象环生:
1.线程 A 执行到 L32 之前,还没有来得及 unregister 本对象。
2.线程 B 执行到 L17,x 正好指向是 L32 正在析构的对象。
?这时悲剧又发生了,既然 x 所指的 Observer 对象正在析构,调用它的任何非静态成员函数都是不安全的,何况是虚函数(C++标准对在构造函数和析构函数中调用虚函数的行为有明确规定,但是没有考虑并发调用的情况)。更糟糕的是, Observer 是个基类,执行到 L32 时,派生类对象已经析构掉了,这时候整个对象处于将死未死的状态,core dump 恐怕是最幸运的结果。
?这些 race condition 似乎可以通过加锁来解决,但在哪儿加锁,谁持有这些互斥锁,又不是那么显而易见的。要是有什么活着的对象能帮我们就好了,它提供一个 isAlive() 之类的程序函数,告诉我们那个对象还在不在。可惜指针和引用都不是对象,它们是内建类型。
1.5 原始指针有何不妥
?指向对象的原始指针(raw pointer)是坏的,尤其当暴露给别的线程时。Observable 应当保存的不是原始的 Observer*,而是别的什么东西,能分辨 Observer 对象是否存活。类似地,如果 Observer 要在析构函数里解注册(这虽然不能解决前面提到的 race condition,但是在析构函数里打扫战场还是应该的),那么 subject_ 的类型也不能是原始的 Observable*。
空悬指针
?有两个指针 p1 和 p2,指向堆上的同一个对象 Object,p1 和 p2 位于不同的线程中(图 1-1 的左图)。假设线程 A 通过 p1 指针将对象销毁了(尽管把 p1 置为了NULL),那p2就成了空悬指针(图 1-1 的右图) 。这是一种典型的C/C++内存错误。
?要想安全地销毁对象,最好在别人(线程)都看不到的情况下完成。这正是垃圾回收的原理,所有人都用不到的东西一定是垃圾。
一个“解决办法”
?解决空悬指针的办法是引入一层间接性,让 p1 和 p2 所指的对象永久有效。比如图 1-2 中的 proxy 对象,这个对象,持有一个指向 Object 的指针。p1 和 p2 都是二级指针。
?当销毁 Object 之后,proxy 对象继续存在,其值变为 0(见图 1-3)。而 p2 也没有变成空悬指针,它可以通过查看 proxy 的内容来判断 Object 是否还活着。
?线程安全地释放Object也不容易,race condition 依旧存在。比如 p2看第一眼的时候 proxy 不是零,正准备去调用 Object 的成员函数,期间对象已经被p1 给销毁了。问题在于,何时释放 proxy 指针呢?
一个更好的解决办法
?为了安全地释放 proxy,我们可以引入引用计数(reference counting),再把 p1和 p2 都从指针变成对象 sp1 和 sp2。proxy 现在有两个成员,指针和计数器。
1.一开始,有两个引用,计数值为 2(见图 1-4) 。
2.sp1 析构了,引用计数的值减为 1(见图 1-5)。
3.sp2 也析构了,引用计数降为 0,可以安全地销毁 proxy 和 Object了(见图 1-6)。
一个万能的解决方案
?引入另外一层间接性(another layer of indirection http://en.wikipedia.org/wiki/Abstraction_layer),用对象来管理共享资源(如果把Object看作资源的话),亦即 handle/body 惯用技法(idiom)。C++的TR1标准库里提供了一对“神兵利器”,可助我们完美解决这个头疼的问题。
1.6 神器 shared_ptr/weak_ptr
?shared_ptr 是引用计数型智能指针,在 Boost 和 std::tr1 里均提供,也被纳入C++11 标准库,现代主流的 C++ 编译器都能很好地支持。shared_ptr 是一个类模板(class template),它只有一个类型参数,使用方便。
?引用计数是自动化资源管理的常用手法,当引用计数降为 0 时,对象(资源)即被销毁。weak_ptr 也是一个引用计数型智能指针,但是它不增加对象的引用次数,即弱(weak)引用。
?几个关键点。
1.shared_ptr控制对象的生命期。shared_ptr是强引用,只要有一个指向 x 对象的 shared_ptr 存在,该 x 对象就不会析构。当指向对象 x 的最后一个 shared_ptr 析构或 reset() 的时候,x 保证会被销毁。
2.weak_ptr不控制对象的生命期,但是它知道对象是否还活着。如果对象还活着,那么它可以提升(promote)为有效的shared_ptr;如果对象已经死了,提升会失败,返回一个空的 shared_ptr。“提升/lock()”行为是线程安全的。
3.shared_ptr/weak_ptr 的“计数”在主流平台上是原子操作,没有用锁,性能不俗。
4.shared_ptr/weak_ptr的线程安全级别与std::string和STL容器一样,后面会讲。
?孟岩《垃圾收集机制批判》(http://blog.csdn.net/myan/article/details/1906)中点出智能指针的优势:“C++利用智能指针达成的效果是:一旦某对象不再被引用,系统立刻回收内存。这通常发生在关键任务完成后的清理(clean up)时期,不会影响关键任务的实时性,同时,内存里所有的对象都是有用的,绝对没有垃圾空占内存。”
1.7 系统地避免各种指针错误
?C++里可能出现的内存问题有:
1.缓冲区溢出(buffer overrun)。
2.空悬指针/野指针。
3.重复释放(double delete)。
4.内存泄漏(memory leak)。
5.不配对的 new[]/delete。
6.内存碎片(memory fragmentation)。
?正确使用智能指针能解决前面5个问题,第6个问题在§ 9.2.1和§ A.1.8探讨。
1.缓冲区溢出:用std::vector/std::string 或自己编写 Buffer class 来管理缓冲区,自动记住用缓冲区的长度,并通过成员函数而不是裸指针来修改缓冲区。
2.空悬指针/野指针:用 shared_ptr/weak_ptr,这正是本章的主题。
3.重复释放:用 scoped_ptr,只在对象析构的时候释放一次。
4.内存泄漏:用 scoped_ptr,对象析构的时候自动释放内存。
5.不配对的 new[]/delete:把 new[] 统统替换为 std::vector/scoped_array。
?在这几种错误里边,内存泄漏相对危害性较小,因为它只是借了东西不归还,程序功能在一段时间内还算正常。其他如缓冲区溢出或重复释放等致命错误可能会造成安全性(security和data safety)方面的严重后果。
?注意:scoped_ptr/shared_ptr/weak_ptr都是值语意,要么是栈上对象,或是其他对象的直接数据成员,或是标准库容器里的元素。不会有下面这种用法:
shared_ptr* pFoo = new shared_ptr(new Foo); // WRONG semantic
?还要注意,如果这几种智能指针是对象 x 的数据成员,而它的模板参数 T 是个incomplete 类型,那么 x 的析构函数不能是默认的或内联的,必须在 .cpp 文件里边显式定义,否则会有编译错或运行错(原因见 § 10.3.2) 。
Please indicate the source: http://blog.csdn.net/gaoxiangnumber1
Welcome to my github: https://github.com/gaoxiangnumber1
标签:
原文地址:http://blog.csdn.net/gaoxiangnumber1/article/details/51284990