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

来源:互联网 发布:transmit mac 编辑:程序博客网 时间:2024/06/06 01:00

作者:yurunsun@gmail.com 新浪微博@孙雨润 新浪博客 CSDN博客日期: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_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配对使用,不控制对象生命期,但是知道对象是否还存在:

    bool isAlive(weak_ptr<Obj> weakObj) {    return (weakObj.lock() != nullptr);}// exampleshared_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队列上,其中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毕竟不是原生指针,对于继承、多态的表现与原生指针完全不同。例如

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新特性介绍中一起讨论。


  • 如果这篇文章对您有帮助,请到CSDN博客留言;
  • 转载请注明:来自雨润的技术博客 http://blog.csdn.net/sunyurun
原创粉丝点击