设计C++回调模型(二):线程安全

作者:yurunsun@gmail.com 新浪微博@孙雨润 新浪博客 优快云博客日期:2012年11月11日

1. 回调模型中的线程安全问题

上一节讨论了通过函数指针、成员函数指针、观察者模式、使用接口的观察者模式、signal/slot模式几种从易到难的解决方案,来实现不同场景下的回调模型。这些是基于单线程运行环境,下面我们分析一下多线程环境下回调模型的线程安全问题。

1.1 什么是线程安全

线程安全的类应满足:

  • 多个线程访问时,其表现出正确的行为
  • 无论操作系统如何调度这些线程,无论这些线程的执行顺序如何交织
  • 调用端代码无需额外的同步或其他协调动作

1.2 回调模型可能有哪些线程安全问题

无论以哪种方式实现回调,必不可少的都是将被通知者的对象指针交给其他对象保管 —— 或者是通知者(观察者模式),或者是一个中间对象(signal).因此必须考虑以下问题:

  • 在即将析构一个对象时,从何而知是否有另外的线程正在执行该对象的成员函数?
  • 如何保证在执行成员函数期间,对象不会在另一个线程被析构?
  • 在调用某个对象的成员函数之前,如何得知这个对象还活着?

现在来回顾上一节中几种回调模型。

2 函数指针:没有任何保护措施

 
  1. class Client {

  2. public:

  3. void startDownload(const string& url) {

  4. cout << "start to download file " << url << endl;

  5. m_downloader.doDownloadJob(url, this, &Client::onDownloadComplete);

  6. }

  7. void onDownloadComplete(const string& url, unsigned ec) {

  8. cout << "file " << url << " finished downloading, ec = " << ec << endl;

  9. }

  10. private:

  11. Downloader m_downloader;

  12. };

我们把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), 会有帮助么?答案是没有,因为

 
  1. delete pClient;

  2. pClient = nullptr;

这两句不是原子操作,threadB可能在两句之间执行回调。

结论2:试图通过测试指针是否为空来判断对象是否存在,毫无意义

2.3 Case:临界区保护很难奏效

如果使用Mutex互斥量保护pClient指针,在回调函数和析构函数中加锁呢?

 
  1. class Client {

  2. public:

  3. void startDownload(const string& url) {

  4. cout << "start to download file " << url << endl;

  5. m_downloader.doDownloadJob(url, this, &Client::onDownloadComplete);

  6. }

  7. void onDownloadComplete(const string& url, unsigned ec) {

  8. MutexLock(&m_mutex);

  9. // PointB: 完成回调工作

  10. cout << "file " << url << " finished downloading, ec = " << ec << endl;

  11. }

  12. ~Client() {

  13. MutexLock(&m_mutex);

  14. // PointA:清理工作

  15. }

  16. private:

  17. Downloader m_downloader;

  18. Mutex m_mutex;

  19. };

threadA运行到PointA,取得mutex,threadB运行到PointB,处于自旋锁或陷入内核态进行等待。然后threadA完成析构,m_mutex销毁,导致threadB在等待一把被销毁的锁,或者coredump或者永远pending.

结论3:作为class成员变量的mutex无法解决回调与析构的同步问题

3 观察者模式:unregister也无能为力

从函数指针的困境中我们不难发现,问题出在threadB不知道threadA中对象是否存在。很自然的想法是:能否在析构时通知threadB自己即将析构,不要再回调自己了呢?

以上一节的交警VS司机观察者模式为例:

 
  1. class TrafficPolice {

  2. public:

  3. void registerDriver(IDriver* pDriver) {

  4. m_drivers.push_back(pDriver);

  5. }

  6. void PointTo(Direction direction) {

  7. for (list<IDriver*>::iterator it = m_drivers.begin(); it != m_drivers.end(); ++it) {

  8. (*it)->onPolicePointTo(direction); // PointC

  9. }

  10. }

  11. private:

  12. list<IDriver*> m_drivers;

  13. };

  14. class IDriver {

  15. public:

  16. virtual void ObservePolice(TrafficPolice* pPolice) {

  17. m_pPolice = pPolice;

  18. m_pPolice->registerDriver(this);

  19. }

  20. ~IDriver() {

  21. m_pPolice->unregisterDriver(this); // PointD

  22. }

  23. virtual void onPolicePointTo(Direction direction) = 0;

  24. protected:

  25. TrafficPolice* m_pPolice;

  26. };

在上节基础上我们在IDriver的析构函数中调用m_pPolice取消注册自己,相当于通知信号源不要再调用自己。问题是反过来threadA也无法确定m_pPolice还活着;即使m_pPolice是全局永久对象,当threadB执行在PointC时,threadA正在PointD执行析构。此时IDriver的非静态成员变量都有销毁的可能,更关键的是实际使用的是IDriver接口的派生类DriverA/DriverB,派生类析构函数要先于基类析构函数调用,因此DriverA/DriverB的成员可能早已销毁。

上节提到的signal/slot的实现方式只不过是使用模板、复杂一些的观察者模式,当然会有同样的线程安全问题。

4. 究竟如何判断对象的生死

看起来有些绝望,多线程环境下真的没有办法判断对象的生死么?还好有线程安全的智能指针 shared_ptr (boost 或C++ tr1 或C++0x),具体使用方式网上一搜一大把,这里列出几点必要的信息:

  • shared_ptr强引用,拷贝时引用计数自增
  • weak_ptrshared_ptr配对使用,不控制对象生命期,但是知道对象是否还存在:

     
      
    1. bool isAlive(weak_ptr<Obj> weakObj) {

    2. return (weakObj.lock() != nullptr);

    3. }

    4. // example

    5. shared_ptr<Obj> sharedObj(new Obj);

    6. bool bAlive = isAlive(sharedObj);

    7. sharedObj.reset();

    8. bAlive = isAlive(sharedObj);

  • shared_ptr/weak_ptr的引用计数操作为原子操作,不需要加锁,没有线程安全问题

5. 一个线程安全的观察者模式

有了shared_ptr/weak_ptr的理论基础,我们重新实现一个线程安全的观察者模式。

5.1 trafficpolice.h

 
  1. #ifndef TRAFFIC_POLICE_H

  2. #define TRAFFIC_POLICE_H

  3. #include <boost/shared_ptr.hpp>

  4. #include <list>

  5. #include <vector>

  6. #include <map>

  7. #include <cstddef>

  8. using namespace std;

  9. enum Direction {NORTH, EAST, SOUTH, WEST};

  10. class IDriver;

  11. typedef boost::shared_ptr<IDriver> SharedDriverType;

  12. typedef boost::weak_ptr<IDriver> WeakDriverType;

  13. typedef vector<boost::shared_ptr<IDriver> > SharedDriverVecType;

  14. typedef vector<boost::weak_ptr<IDriver> > WeakDriverVecType;

  15. typedef map<uint32_t, boost::weak_ptr<IDriver> > ID2WeakDriverMapType;

  16. class TrafficPolice {

  17. public:

  18. void registerDriver(uint32_t id, WeakDriverType pDriver);//Note1:使用weak_ptr,保持引用计数不变

  19. void unRegisterDriver(uint32_t id);

  20. void PointTo(Direction direction);

  21. private:

  22. ID2WeakDriverMapType m_drivers; // Note2:存储weak_ptr

  23. };

  24. #endif

5.2 trafficpolice.cpp

 
  1. #include "trafficpolice.h"

  2. #include "driver.h"

  3. void TrafficPolice::registerDriver(uint32_t id, WeakDriverType pDriver) {

  4. m_drivers.insert(make_pair(id, pDriver));

  5. }

  6. void TrafficPolice::unRegisterDriver(uint32_t id) {

  7. m_drivers.erase(id);

  8. }

  9. void TrafficPolice::PointTo(Direction direction) {

  10. auto it = begin(m_drivers); // Note3:使用了c++11的auto语义,c++03环境改成一般iterator,下同

  11. while (it != end(m_drivers)) {

  12. SharedDriverType pDriver = it->second.lock(); // Note4: 对weak_ptr试图提升权限,如果driver已删除则提升失败

  13. if (pDriver) {

  14. pDriver->onPolicePointTo(direction);

  15. ++it;

  16. } else {

  17. it = m_drivers.erase(it);

  18. }

  19. }

  20. }

5.3 driver.h

 
  1. #ifndef DRIVER_H

  2. #define DRIVER_H

  3. #include <boost/enable_shared_from_this.hpp>

  4. #include "trafficpolice.h"

  5. #include <iostream>

  6. class IDriver :

  7. public boost::enable_shared_from_this<IDriver>

  8. {

  9. public:

  10. IDriver(uint32_t id, TrafficPolice* pPolice) : m_id(id), m_pPolice(pPolice) {}

  11. void starePolice() {

  12. // Note5: shared_from_this()返回this指针的shared_ptr版本

  13. m_pPolice->registerDriver(m_id, shared_from_this());

  14. }

  15. ~IDriver() {

  16. m_pPolice->unRegisterDriver(m_id);

  17. }

  18. virtual void onPolicePointTo(Direction direction) {

  19. cout << "onPolicePointTo " << direction << " received..." << endl;

  20. };

  21. private:

  22. uint32_t m_id;

  23. TrafficPolice* m_pPolice;

  24. };

  25. #endif

5.4 main.cpp

 
  1. #include "driver.h"

  2. void initDriver(unsigned count, TrafficPolice* pPolice, SharedDriverVecType& out) {

  3. out.reserve(count);

  4. for (unsigned i = 0; i < count; ++i) {

  5. SharedDriverType newDriver(new IDriver(i, pPolice)); //Note7:driver生存期开始,refcount == 1

  6. newDriver->starePolice();

  7. out.push_back(newDriver);

  8. }

  9. }

  10. int main() {

  11. TrafficPolice trafficPolice;

  12. SharedDriverVecType driverGroup;

  13. initDriver(10, &trafficPolice, driverGroup);

  14. trafficPolice.PointTo(NORTH);

  15. }

上边代码能够保证交警知晓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结合起来:

 
  1. asio::async_read(m_socket,

  2. asio::buffer(&m_readBuf[0], m_readBuf.size()),

  3. boost::bind(&TcpConnection::handleReceive,

  4. shared_from_this(),

  5. asio::placeholders::error)

  6. );

这段代码表示如下过程:

  • m_socket套接字发起异步读请求,并绑定读操作完成的处理函数handleReceive
  • 调用过asio::async_read后立即执行后续代码,而this->handleReceive则会被asio保存起来。
  • 读操作完成或者中途失败,asio会将回调函数this->handleReceive放到reactor/proactor队列上,其中thisweak_ptr版本存储。reactor/proactor是网络模型设计模式,暂时理解为IO完成队列
  • 当调用asio::async_read执行了asio::poll,asio会将将队列中积累的回调函数一个个执行
  • 执行时将weak_ptr提升权限,如果成功则执行回调函数handleReceive,如果失败说明TcpConnection对象已销毁,不再执行回调函数

7. 一定要以weak_ptr保存Driver指针吗?

7.1 weak_ptrenable_shared_from_this的局限

boost::bind支持以weak_ptr保存指针,但如果有些库 —— 或者自己开发的类 —— 不愿意或者不能以weak_ptr保存回调的对象指针,该怎么办呢?

以刚刚TcpConnection调用的asio::async_read为例,即使对方支持以weak_ptr保存指针,实际上侧面还要求TcpConnection类要从boost::enable_shared_from_this,而且shared_ptr毕竟不是原生指针,对于继承、多态的表现与原生指针完全不同。例如

 
  1. class Base : public boost::enable_shared_from_this<Base> {

  2. //...

  3. };

  4. class Derived : public Base {

  5. // ...

  6. };

  7. Derived pDerived = new Derived;

  8. boost::shared_ptr<Derived> spDerived2 = pDerived1->shared_from_this(); // Error

  9. //编译报错 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为回调函数参数,可以自行扩展为多个参数。

 
  1. struct Probe{};

  2. template<typename T, typename A> class Handler

  3. {

  4. public:

  5. typedef void(T::*MemfnPtr)(A);

  6. explicit Handler(MemfnPtr memfn, T * obj, const boost::shared_ptr<Probe>& probe)

  7. : m_memfn(memfn), m_this(obj), m_probe(probe){}

  8. explicit Handler(const Handler& other)

  9. : m_memfn(other.m_memfn), m_this(other.m_this), m_probe(other.m_probe){}

  10. Handler& operator=(const SafeHandler& right) {

  11. if(this != &right) {

  12. m_memfn = right.m_memfn;

  13. m_this = right.m_this;

  14. m_probe = right.m_probe;

  15. }

  16. return *this;

  17. }

  18. void operator()(A a) {

  19. if(boost::shared_ptr<Probe> probe = m_probe.lock())

  20. (m_this->*m_memfn)(a);

  21. }

  22. void operator()(A a) const {

  23. if(boost::shared_ptr<Probe> probe = m_probe.lock())

  24. (m_this->*m_memfn)(a);

  25. }

  26. private:

  27. MemfnPtr m_memfn;

  28. T * m_this;

  29. boost::weak_ptr<Probe> m_probe;

  30. };

TcpConnection类中加入一个成员变量:

boost::shared_ptr<Probe> m_probe;

TcpConnection对象的构造函数中为m_probe创建对象,使引用计数为1:

TcpConnection() : m_probe(new Probe){}

现在改写之前调用asio::async_read时传入回调的部分,将sharedfromthis()改为原生指针this:

 
  1. asio::async_read(m_socket,

  2. asio::buffer(&m_readBuf[0], m_readBuf.size()),

  3. Handler<TcpConnection, const asio::error_code&>(&TcpConnection::handleReceive, this, m_probe)

  4. );

Probe探针解决了两个问题:

  • TcpConnection不需要在从boost::enable_shared_from_this继承
  • 只需要在最底层的基类提供probe成员,就可以支持任意派生类对象的安全回调

7.3 C++11中匿名函数lambda语义

C++11的新特性中lambda几乎是专门用来解决回调函数的安全问题,会在后续C++11新特性介绍中一起讨论。


(405条消息) 设计C++回调模型(二):线程安全_sunyurun的博客-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值