作者:yurunsun@gmail.com 新浪微博@孙雨润 新浪博客 优快云博客日期:2012年11月11日
1. 回调模型中的线程安全问题
上一节讨论了通过函数指针、成员函数指针、观察者模式、使用接口的观察者模式、signal/slot模式几种从易到难的解决方案,来实现不同场景下的回调模型。这些是基于单线程运行环境,下面我们分析一下多线程环境下回调模型的线程安全问题。
1.1 什么是线程安全
线程安全的类应满足:
- 多个线程访问时,其表现出正确的行为
- 无论操作系统如何调度这些线程,无论这些线程的执行顺序如何交织
- 调用端代码无需额外的同步或其他协调动作
1.2 回调模型可能有哪些线程安全问题
无论以哪种方式实现回调,必不可少的都是将被通知者的对象指针交给其他对象保管 —— 或者是通知者(观察者模式),或者是一个中间对象(signal).因此必须考虑以下问题:
- 在即将析构一个对象时,从何而知是否有另外的线程正在执行该对象的成员函数?
- 如何保证在执行成员函数期间,对象不会在另一个线程被析构?
- 在调用某个对象的成员函数之前,如何得知这个对象还活着?
现在来回顾上一节中几种回调模型。
2 函数指针:没有任何保护措施
-
class Client {
-
public:
-
void startDownload(const string& url) {
-
cout << "start to download file " << url << endl;
-
m_downloader.doDownloadJob(url, this, &Client::onDownloadComplete);
-
}
-
void onDownloadComplete(const string& url, unsigned ec) {
-
cout << "file " << url << " finished downloading, ec = " << ec << endl;
-
}
-
private:
-
Downloader m_downloader;
-
};
我们把startDownload
函数的调用线程叫做threadA,如果doDownloadJob
的实现创建了新线程threadB并将url, this, &Client::onDownloadComplete
几个参数传入,然后doDownloadJob
立即返回,准备等待threadB工作完成之后调用this->onDownloadComplete
进行通知。
2.1 Case1: threadA先析构,threadB再调用
如果在threadB执行download过程中,Client对象由于与服务器断开连接,在threadA中被析构删除,那么threadB拿到的this指针就是野指针,调用一定会导致coredump.
结论1:没有任何保护必然coredump
2.2 Case2: 置nullptr
之前threadB无法测试指针
如果threadA删除Client对象后将其置为nullptr
,threadB在调用时测试 if (this == nullptr)
, 会有帮助么?答案是没有,因为
-
delete pClient;
-
pClient = nullptr;
这两句不是原子操作,threadB可能在两句之间执行回调。
结论2:试图通过测试指针是否为空来判断对象是否存在,毫无意义
2.3 Case:临界区保护很难奏效
如果使用Mutex互斥量保护pClient
指针,在回调函数和析构函数中加锁呢?
-
class Client {
-
public:
-
void startDownload(const string& url) {
-
cout << "start to download file " << url << endl;
-
m_downloader.doDownloadJob(url, this, &Client::onDownloadComplete);
-
}
-
void onDownloadComplete(const string& url, unsigned ec) {
-
MutexLock(&m_mutex);
-
// PointB: 完成回调工作
-
cout << "file " << url << " finished downloading, ec = " << ec << endl;
-
}
-
~Client() {
-
MutexLock(&m_mutex);
-
// PointA:清理工作
-
}
-
private:
-
Downloader m_downloader;
-
Mutex m_mutex;
-
};
threadA运行到PointA,取得mutex,threadB运行到PointB,处于自旋锁或陷入内核态进行等待。然后threadA完成析构,m_mutex销毁,导致threadB在等待一把被销毁的锁,或者coredump或者永远pending.
结论3:作为class成员变量的mutex无法解决回调与析构的同步问题
3 观察者模式:unregister也无能为力
从函数指针的困境中我们不难发现,问题出在threadB不知道threadA中对象是否存在。很自然的想法是:能否在析构时通知threadB自己即将析构,不要再回调自己了呢?
以上一节的交警VS司机观察者模式为例:
-
class TrafficPolice {
-
public:
-
void registerDriver(IDriver* pDriver) {
-
m_drivers.push_back(pDriver);
-
}
-
void PointTo(Direction direction) {
-
for (list<IDriver*>::iterator it = m_drivers.begin(); it != m_drivers.end(); ++it) {
-
(*it)->onPolicePointTo(direction); // PointC
-
}
-
}
-
private:
-
list<IDriver*> m_drivers;
-
};
-
class IDriver {
-
public:
-
virtual void ObservePolice(TrafficPolice* pPolice) {
-
m_pPolice = pPolice;
-
m_pPolice->registerDriver(this);
-
}
-
~IDriver() {
-
m_pPolice->unregisterDriver(this); // PointD
-
}
-
virtual void onPolicePointTo(Direction direction) = 0;
-
protected:
-
TrafficPolice* m_pPolice;
-
};
在上节基础上我们在IDriver
的析构函数中调用m_pPolice
取消注册自己,相当于通知信号源不要再调用自己。问题是反过来threadA
也无法确定m_pPolice
还活着;即使m_pPolic
e是全局永久对象,当threadB执行在PointC时,threadA正在PointD执行析构。此时IDriver的非静态成员变量都有销毁的可能,更关键的是实际使用的是IDriver
接口的派生类DriverA/DriverB
,派生类析构函数要先于基类析构函数调用,因此DriverA/DriverB
的成员可能早已销毁。
上节提到的signal/slot的实现方式只不过是使用模板、复杂一些的观察者模式,当然会有同样的线程安全问题。
4. 究竟如何判断对象的生死
看起来有些绝望,多线程环境下真的没有办法判断对象的生死么?还好有线程安全的智能指针 shared_ptr
(boost 或C++ tr1 或C++0x),具体使用方式网上一搜一大把,这里列出几点必要的信息:
shared_ptr
强引用,拷贝时引用计数自增-
weak_ptr
与shared_ptr
配对使用,不控制对象生命期,但是知道对象是否还存在:-
bool isAlive(weak_ptr<Obj> weakObj) {
-
return (weakObj.lock() != nullptr);
-
}
-
// example
-
shared_ptr<Obj> sharedObj(new Obj);
-
bool bAlive = isAlive(sharedObj);
-
sharedObj.reset();
-
bAlive = isAlive(sharedObj);
-
-
shared_ptr/weak_ptr
的引用计数操作为原子操作,不需要加锁,没有线程安全问题
5. 一个线程安全的观察者模式
有了shared_ptr/weak_ptr
的理论基础,我们重新实现一个线程安全的观察者模式。
5.1 trafficpolice.h
-
#ifndef TRAFFIC_POLICE_H
-
#define TRAFFIC_POLICE_H
-
#include <boost/shared_ptr.hpp>
-
#include <list>
-
#include <vector>
-
#include <map>
-
#include <cstddef>
-
using namespace std;
-
enum Direction {NORTH, EAST, SOUTH, WEST};
-
class IDriver;
-
typedef boost::shared_ptr<IDriver> SharedDriverType;
-
typedef boost::weak_ptr<IDriver> WeakDriverType;
-
typedef vector<boost::shared_ptr<IDriver> > SharedDriverVecType;
-
typedef vector<boost::weak_ptr<IDriver> > WeakDriverVecType;
-
typedef map<uint32_t, boost::weak_ptr<IDriver> > ID2WeakDriverMapType;
-
class TrafficPolice {
-
public:
-
void registerDriver(uint32_t id, WeakDriverType pDriver);//Note1:使用weak_ptr,保持引用计数不变
-
void unRegisterDriver(uint32_t id);
-
void PointTo(Direction direction);
-
private:
-
ID2WeakDriverMapType m_drivers; // Note2:存储weak_ptr
-
};
-
#endif
5.2 trafficpolice.cpp
-
#include "trafficpolice.h"
-
#include "driver.h"
-
void TrafficPolice::registerDriver(uint32_t id, WeakDriverType pDriver) {
-
m_drivers.insert(make_pair(id, pDriver));
-
}
-
void TrafficPolice::unRegisterDriver(uint32_t id) {
-
m_drivers.erase(id);
-
}
-
void TrafficPolice::PointTo(Direction direction) {
-
auto it = begin(m_drivers); // Note3:使用了c++11的auto语义,c++03环境改成一般iterator,下同
-
while (it != end(m_drivers)) {
-
SharedDriverType pDriver = it->second.lock(); // Note4: 对weak_ptr试图提升权限,如果driver已删除则提升失败
-
if (pDriver) {
-
pDriver->onPolicePointTo(direction);
-
++it;
-
} else {
-
it = m_drivers.erase(it);
-
}
-
}
-
}
5.3 driver.h
-
#ifndef DRIVER_H
-
#define DRIVER_H
-
#include <boost/enable_shared_from_this.hpp>
-
#include "trafficpolice.h"
-
#include <iostream>
-
class IDriver :
-
public boost::enable_shared_from_this<IDriver>
-
{
-
public:
-
IDriver(uint32_t id, TrafficPolice* pPolice) : m_id(id), m_pPolice(pPolice) {}
-
void starePolice() {
-
// Note5: shared_from_this()返回this指针的shared_ptr版本
-
m_pPolice->registerDriver(m_id, shared_from_this());
-
}
-
~IDriver() {
-
m_pPolice->unRegisterDriver(m_id);
-
}
-
virtual void onPolicePointTo(Direction direction) {
-
cout << "onPolicePointTo " << direction << " received..." << endl;
-
};
-
private:
-
uint32_t m_id;
-
TrafficPolice* m_pPolice;
-
};
-
#endif
5.4 main.cpp
-
#include "driver.h"
-
void initDriver(unsigned count, TrafficPolice* pPolice, SharedDriverVecType& out) {
-
out.reserve(count);
-
for (unsigned i = 0; i < count; ++i) {
-
SharedDriverType newDriver(new IDriver(i, pPolice)); //Note7:driver生存期开始,refcount == 1
-
newDriver->starePolice();
-
out.push_back(newDriver);
-
}
-
}
-
int main() {
-
TrafficPolice trafficPolice;
-
SharedDriverVecType driverGroup;
-
initDriver(10, &trafficPolice, driverGroup);
-
trafficPolice.PointTo(NORTH);
-
}
上边代码能够保证交警知晓driver对象的生死,如果要做到完全的线程安全,还要加上交警对象的生死判断、交警读写driver容器时加锁。由于和回调模型关系不大,这里略去。
6. shared_ptr
推广到函数指针方式回调
前边说过回调模型的线程安全问题实际是相同的,就是将this指针传给其他对象,其他对象如何得知自己生死的问题。shared_ptr
既然在观察者模式中解决了这个问题,一般的函数指针方式回调就也迎刃而解:只需要“其他对象”使用weak_ptr
取代原生指针。实际上boost::function/boost::bind
是支持这种方式实现安全的函数回调。
boost::bind(&TcpConnection::handleReceiveHead, shared_from_this(), asio::placeholders::error));
与异步网络库asio
结合起来:
-
asio::async_read(m_socket,
-
asio::buffer(&m_readBuf[0], m_readBuf.size()),
-
boost::bind(&TcpConnection::handleReceive,
-
shared_from_this(),
-
asio::placeholders::error)
-
);
这段代码表示如下过程:
- 在
m_socket
套接字发起异步读请求,并绑定读操作完成的处理函数handleReceive
- 调用过
asio::async_read
后立即执行后续代码,而this->handleReceive
则会被asio保存起来。 - 读操作完成或者中途失败,asio会将回调函数
this->handleReceive
放到reactor/proactor队列上,其中this
以weak_ptr
版本存储。reactor/proactor是网络模型设计模式,暂时理解为IO完成队列 - 当调用
asio::async_read
执行了asio::poll
,asio会将将队列中积累的回调函数一个个执行 - 执行时将
weak_ptr
提升权限,如果成功则执行回调函数handleReceive
,如果失败说明TcpConnection
对象已销毁,不再执行回调函数
7. 一定要以weak_ptr
保存Driver
指针吗?
7.1 weak_ptr
与enable_shared_from_this
的局限
boost::bind
支持以weak_ptr
保存指针,但如果有些库 —— 或者自己开发的类 —— 不愿意或者不能以weak_ptr
保存回调的对象指针,该怎么办呢?
以刚刚TcpConnection
调用的asio::async_read
为例,即使对方支持以weak_ptr
保存指针,实际上侧面还要求TcpConnection
类要从boost::enable_shared_from_this
,而且shared_ptr
毕竟不是原生指针,对于继承、多态的表现与原生指针完全不同。例如
-
class Base : public boost::enable_shared_from_this<Base> {
-
//...
-
};
-
class Derived : public Base {
-
// ...
-
};
-
Derived pDerived = new Derived;
-
boost::shared_ptr<Derived> spDerived2 = pDerived1->shared_from_this(); // Error
-
//编译报错 Error: conversion from boost::shared_ptr<Base> to non-scalar type boost::shared_ptr<Derived> requested
7.2 用Probe
(探针)代替weak_ptr/enable_shared_from_this
有一个很巧妙的方法:在Driver类中添加一个探针成员,以shared_ptr
方式保存;注册回调函数时封装一层Handler对象,这个Handler在原来需要执行回调的地方加上判断探针生死的逻辑。模板T为指针类型,A为回调函数参数,可以自行扩展为多个参数。
-
struct Probe{};
-
template<typename T, typename A> class Handler
-
{
-
public:
-
typedef void(T::*MemfnPtr)(A);
-
explicit Handler(MemfnPtr memfn, T * obj, const boost::shared_ptr<Probe>& probe)
-
: m_memfn(memfn), m_this(obj), m_probe(probe){}
-
explicit Handler(const Handler& other)
-
: m_memfn(other.m_memfn), m_this(other.m_this), m_probe(other.m_probe){}
-
Handler& operator=(const SafeHandler& right) {
-
if(this != &right) {
-
m_memfn = right.m_memfn;
-
m_this = right.m_this;
-
m_probe = right.m_probe;
-
}
-
return *this;
-
}
-
void operator()(A a) {
-
if(boost::shared_ptr<Probe> probe = m_probe.lock())
-
(m_this->*m_memfn)(a);
-
}
-
void operator()(A a) const {
-
if(boost::shared_ptr<Probe> probe = m_probe.lock())
-
(m_this->*m_memfn)(a);
-
}
-
private:
-
MemfnPtr m_memfn;
-
T * m_this;
-
boost::weak_ptr<Probe> m_probe;
-
};
在TcpConnection
类中加入一个成员变量:
boost::shared_ptr<Probe> m_probe;
在TcpConnection
对象的构造函数中为m_probe
创建对象,使引用计数为1:
TcpConnection() : m_probe(new Probe){}
现在改写之前调用asio::async_read
时传入回调的部分,将sharedfromthis()改为原生指针this:
-
asio::async_read(m_socket,
-
asio::buffer(&m_readBuf[0], m_readBuf.size()),
-
Handler<TcpConnection, const asio::error_code&>(&TcpConnection::handleReceive, this, m_probe)
-
);
Probe
探针解决了两个问题:
TcpConnection
不需要在从boost::enable_shared_from_this
继承- 只需要在最底层的基类提供
probe
成员,就可以支持任意派生类对象的安全回调
7.3 C++11中匿名函数lambda语义
C++11的新特性中lambda几乎是专门用来解决回调函数的安全问题,会在后续C++11新特性介绍中一起讨论。