关闭

事件和回调

82人阅读 评论(0) 收藏 举报

事件模型包括

事件-----------------定义相应事件类型对象

处理程序-----------接收事件响应的处理方法

挂钩----------------关联事件与处理程序

触发器-------------触发事件相应

事件管理----------确保事件过程正确



定义事件---->关联处理------>触发事件------->响应事件





二 代码实现


1 EventManager

 

复制代码
/*----------------------------------------------------------------*/
/* class Object 响应事件函数的类必须是从Object派生下来 */
/*----------------------------------------------------------------*/
class Object
{
};

/*----------------------------------------------------------------*/
/* class Event 模板参数为 返回类型 和响应函数参数类型 */
/* 仅实现一个参数的事件响应函数 */
/*----------------------------------------------------------------*/

template<typename rtnTtpe,typename ArgType>
class Event
{
//使每个事件最多关联响应的函数个数
#define EVENT_LIST_MAX_NUM (10)

typedef rtnTtpe (Object::*pMemFunc)(ArgType arg);

public:
Event()
{
m_totalFunc = 0;
m_obj = NULL;
for (int i = 0; i < EVENT_LIST_MAX_NUM; i++)
{
m_func[i] = NULL;
}
}

//关联回调成员函数
template <class _func_type>

void associate(Object *obj, _func_type func)
{
m_obj = obj;
m_func[m_totalFunc] = static_cast<pMemFunc> (func);
m_totalFunc++;
}
//删除事件关联回调成员函数
template <class _func_type>

void disAssociate(Object *obj, _func_type func)
{
if (obj != m_obj)
{
return;
}

//查找
for (int i = 0; i < m_totalFunc; i++)

{
if (m_func[i] == func)
{
break;
}
}

//移动删除
for (i ; i < m_totalFunc - 1; i++)

{
m_func[i] = m_func[i + 1];
}

m_func[i] = NULL;
m_totalFunc --;
}

//执行关联的回调函数
void sendEvent(ArgType arg)

{
for (int i = 0; i < m_totalFunc; i++)
{
if (m_func[i] != NULL)
{
((m_obj->*pMemFunc(m_func[i])))(arg);
}
}
}
private:
Object* m_obj;
pMemFunc m_func[EVENT_LIST_MAX_NUM];
int m_totalFunc;
};
复制代码


2 测试代码

 

复制代码
/*----------------------------------------------------------------*/
/* class TestEvent */
/*----------------------------------------------------------------*/
class TestEvent
{
public:
void test()
{
//do somsthing
//……

//触发事件
myEvent.sendEvent(100);

myEvent.sendEvent(200);
}
public:
//定义事件
Event<bool,int> myEvent;

};
复制代码

 

复制代码
/*----------------------------------------------------------------*/
/* class TestClass */
/*----------------------------------------------------------------*/
class TestClass:public Object
{
public:
TestClass()
{
//关联事件
m_event.myEvent.associate(this,&TestClass::executeCb1);

m_event.myEvent.associate(this,&TestClass::executeCb2);
}

//事件响应函数
bool executeCb1(int result)

{
cout<<"executeCb1 result = "<<result<<endl;
return true;
}
//事件响应函数
bool executeCb2(int result)

{
cout<<"executeCb2 result = "<<result<<endl;
return true;
}

void execute()
{
m_event.test();
}
void stop()
{
//删除事件关联函数
m_event.myEvent.disAssociate(this,&TestClass::executeCb1);

}
private:
TestEvent m_event;
};
复制代码

 

复制代码
int main()
{
TestClass testObj;

testObj.execute();
testObj.stop();
testObj.execute();

return 0;
}
复制代码


3 输出结果

 

复制代码

<---------------first begin---------------
executeCb1 result = 100
executeCb2 result = 100
executeCb1 result = 200
executeCb2 result = 200
---------------after delete---------------
executeCb2 result = 100
executeCb2 result = 200























===============================================================================================================

===============================================================================================================

===============================================================================================================

===============================================================================================================

1. 什么是回调

开发中经常遇到等待其他模块事件通知的情况,例如:

  • 用户点击UI上button的事件,通知给相关函数处理逻辑
  • Model中数据改变的事件,通知给相关View模块刷新界面
  • 异步IO完成的事件,通知给处理函数确认成功还是失败
  • 客户端向服务器发N种不同请求,服务器为每种请求准备好处理函数

这些等待通知的函数被执行的过程就是回调的过程,所以回调是一个很常见很简单的事情,接下来我们一起看一下C++中有哪些方法可以完成回调。

2. 函数指针

这是最简单最原始的方法,从C语言开始就被广泛使用尽人皆知。比如Windows的窗口过程回调函数,线程运行函数,Linux内核与驱动等。例如下载文件完成的通知:

#include <iostream>
#include <string>
using namespace std;

typedef void (*DownloadHandler)(const string& url, unsigned ec);
void doDownloadJob(const string& url, DownloadHandler pHandler) {
    // be busy doing sth. with downloading
    if (pHandler != nullptr) {
        pHandler(url, 0);
    }
}
void onDownloadComplete(const string& url, unsigned ec) {
    cout << "file " << url << " finished downloading, ec = " << ec << endl;
}

int main() {
    doDownloadJob("http://yy.com/music/spring.mp3", onDownloadComplete);
}

【Note】C++0x标准中新加了关键词nullptr,并推荐用来进行空指针测试。具体原因会在本博客C++0x新feature进行介绍。

使用C++的OO风格,需要将普通函数指针换成成员函数指针。关于函数指针后续会有深入的讨论,这里只用最简单的形式:

class Client;
typedef void (Client::*ClientMemFun)(const string&, unsigned);
class Downloader {
public:
    void doDownloadJob(const string& url, Client* clientOwner, ClientMemFun pHandler) {
        // be busy doing sth. with downloading
        if ((clientOwner != nullptr) && (pHandler != nullptr)) {
            (clientOwner->*pHandler)(url, 0);
        }
    }
};
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;
};

int main() {
    Client c;
    c.startDownload("http://yy.com/music/spring.mp3");
}

在第C和C++的例子中都为为下载成功的通知准备了处理函数onDownloadCompleteClient::onDownloadComplete,实现了下载成功后接受通知被动调用。

问题1: 刚刚例子中是一个用户Client使用自己的下载工具Downloader,只有自己对此感兴趣,下载完成后通知自己很合理;如果是一位交警在指挥,N个司机等待交警的手势通知,上边的函数指针方法就很难实现了。而观察者模式能有效解决这种一对多的通知方式。

3. 观察者模式

观察者模式是最常用的几个设计模式之一,想必大家都不陌生。思路就是一群人作为观察者,盯着一个发布信息的人,并根据信息有所行动。我这里想到的交警司机的例子能够生动的描述这种模式。

#include <iostream>
#include <string>
#include <list>
#include <vector>
using namespace std;

enum Direction {NORTH, EAST, SOUTH, WEST};
class Driver {
public:
    void onPolicePointTo(Direction direction) {
        if (direction == NORTH) {
            cout << "I'm gonna buckle up and go!" << endl;
        }
    }
};
class TrafficPolice {
public:
    void registerDriver(Driver* pDriver) {
        m_drivers.push_back(pDriver);
    }
    void PointTo(Direction direction) {
        for (list<Driver*>::iterator it = m_drivers.begin(); it != m_drivers.end(); ++it) {
            if ((*it) != nullptr) {
                (*it)->onPolicePointTo(direction);
            }
        }
    }
private:
    list<Driver*> m_drivers;
};

int main() {
    // Step1: 一共有20个司机开车在路上
    unsigned driverCount = 20;
    vector<Driver*> driverGroup;
    driverGroup.reserve(driverCount);
    for (unsigned i = 0; i < driverCount; ++i) {
        driverGroup.push_back(new Driver);
    }
    // Step2: 十字路口有一位交警
    TrafficPolice trafficPolice;
    // Step3: 交警把这些司机都注册到自己这边
    for (vector<Driver*>::iterator it = driverGroup.begin(); it != driverGroup.end(); ++it) {
        trafficPolice.registerDriver(*it);
    }
    // Step4: 交警指向北方,司机立刻行动
    trafficPolice.PointTo(NORTH);
}

这段代码能够实现一对多的通知,不过还有些不妥。

问题2:当交警指向北方时,有的司机需要右转 有的需要刹车,有的要启动。如何在一次通知中让不同司机能做不同的事情呢?需要用到接口的概念。

#include <iostream>
#include <string>
#include <list>
#include <vector>
using namespace std;

enum Direction {NORTH, EAST, SOUTH, WEST};
class IDriver {
public:
    virtual void onPolicePointTo(Direction direction) = 0;
};
class DriverA : public IDriver {
public:
    void onPolicePointTo(Direction direction) {
        if (direction == NORTH) {
            cout << "I'm gonna buckle up and go!" << endl;
        }
    }
};
class DriverB : public IDriver {
public:
    void onPolicePointTo(Direction direction) {
        if (direction == NORTH) {
            cout << "Oh shit, I have to stop!" << endl;
        }
    }
};
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);
        }
    }
private:
    list<IDriver*> m_drivers;
};
IDriver* createDriver(const string& type) {
    if (type == "A") {
        return new DriverA;
    } else if (type == "B") {
        return new DriverB;
    } else {
        return nullptr;
    }
}
void initDriver(const string& type, unsigned count, vector<IDriver*>& out) {
    out.reserve(count);
    for (unsigned i = 0; i < count; ++i) {
        out.push_back(createDriver(type));
    }
}

int main() {
    vector<IDriver*> driverGroupA;
    vector<IDriver*> driverGroupB;
    initDriver("A", 5, driverGroupA);
    initDriver("B", 10, driverGroupB);

    TrafficPolice trafficPolice;
    for (vector<IDriver*>::iterator it = driverGroupA.begin(); it != driverGroupA.end(); ++it) {
        trafficPolice.registerDriver(*it);
    }
    for (vector<IDriver*>::iterator jt = driverGroupB.begin(); jt != driverGroupB.end(); ++jt) {
        trafficPolice.registerDriver(*jt);
    }
    trafficPolice.PointTo(NORTH);
}

利用纯虚函数(接口)的概念,现在做到了同一通知,不同观察者做不同的事情。可是还是有一个地方怪怪的:

问题3:交警为什么要承担注册司机的职责呢?事实上现实生活中信号源往往并不关心信号接收者:演唱会歌手不需要对每位观众都做一次类似register这种操作,GPS信号源也不会关心地面上有哪些车载导航;相反是观众对歌手感兴趣、车载导航对GPS信号源感兴趣,所以才接受对方信号。这种松耦合的状态才更为真实地描述了对象之间的关系。

稍微修改一下Driver类可以使交警看起来摆脱注册司机的职责:

class IDriver {
public:
    virtual void ObservePolice(TrafficPolice* pPolice) {
        pPolice->registerDriver(this);
    }
    virtual void onPolicePointTo(Direction direction) = 0;
};

问题4:看起来还是怪怪的,首先交警虽然不主动调用registerDriver,但还是要提供这样一个功能,供Driver注册上来,实际上应该是交警发出的信号(手势、哨子)去提供通知的功能;然后交警还需要知道如何通知给Driver,也就是需要知道回调函数的形式;更主要的是,ObservePolice只能用来接收交警类型的对象指针,Driver还要提供越来越多的接口来观察信号灯、前车车距、后车车距等等。

4. signal/slot (信号/槽)

signal/slot是这样一个朴素的想法:

  • 信号源发出信号,不关心接受者
  • 接受者了解信号并主动接收信号,并决定如何处理
  • 多个接受者可以关注同一个信号源
  • 一个接受者可以关注多个信号源
  • 一个信号源可以关注另一个信号源

很多脚本语言尤其是UI相关的语言,都在语法层面上支持signal/slot,例如as3的addEventListener、C#的delegate等,这项功能也被称为事件驱动。C++要实现signal/slot确实要做一些工作,但是已经有很多个实现版本了:

其中最后一个以其简洁方便安全最受欢迎,被用在google的多个开源项目中,也被本人用在公司的几个项目中取代观察者模式,以松耦合的方式设计架构,思路清晰开发高效。这里着重讲一下这个工具的使用方法与实现方式。

4.1 使用方法

以工作中一个场景为例:在一个聊天室内有多个用户,房间公告、主持人列表的变化都要广播给房间所有用户。

////////////// @file: hostess.h//////////////
#include "sigslot.h"
struct HostessInfo {
    // ...;
};
class Hostess {
public:
    sigslot::signal1<const HostessInfo&> hostessInfoChanged;
};

///////////// @file: bulletin.h//////////////
#include "sigslot.h"
struct BulletinInfo {
    // ...;
};
class Bulletin {
public:
    sigslot::signal1<const BulletinInfo&> BulletinInfoChanged;
};

///////////// @file: room.h///////////////
#include "hostess.h"
#include "bulletin.h"
#include "client.h"
#include <map.h>
class Room {
public:
    void addClient(uint32_t id) {
        m_client.insert(make_pair(id, new Client(this)));
    }
    void removeClient(uint32_t id) {
        m_client.erase(id);
    }
    Hostess m_hostess;
    Bulletin m_bulletin;
    map<uint32_t, Client*> m_client;
}

/////////////// @file: client.h///////////////
#include "hostess.h"
#include "bulletin.h"
class Room;
class Client : public sigslot::has_slots<> {
public:
    explicit Client(Room* pFacade);
    void onHostessInfoChanged(const HostessInfo& info);
    void onBulletinInfoChanged(const BulletinInfo& info);
}

///////////////// @file client.cpp///////////
#include "client.h"
#include "room.h"
Client::Client(Channel* pFacade) {
    pFacade->m_hostess.connect(this, &Client::onHostessChanged);
    pFacade->m_bulletin.connect(this, &Client::onBulletinInfoChanged);
}

当Hostess和Bulletin发生改变时只需要调用两个信号的emit,就能够实现广播回调。

4.2 实现方法

这个库以模板的形式实现了0~8个参数的signal/slot,如果需要更多参数则需要自行扩展。好消息是C++0x模板不定参数的诞生使得以后再不必为了参数个数而将相同逻辑的代码复制8次。我之后准备利用C++0x模板不定参数简化这个库到几百行代码范围内。

还是先来看实现方法:以不带参数的signal/slot为例

4.2.1 _connection_base0

template<class mt_policy>
class _connection_base0
{
public:
    virtual ~_connection_base0() {}
    virtual has_slots<mt_policy>* getdest() const = 0;
    virtual void emit() = 0;
    virtual _connection_base0* clone() = 0;
    virtual _connection_base0* duplicate(has_slots<mt_policy>* pnewdest) = 0;
};

4.2.2 _signal_base0

template<class mt_policy>
class _signal_base : public mt_policy
{
public:
    virtual void slot_disconnect(has_slots<mt_policy>* pslot) = 0;
    virtual void slot_duplicate(const has_slots<mt_policy>* poldslot, has_slots<mt_policy>* pnewslot) = 0;
};
template<class mt_policy>
class _signal_base0 : public _signal_base<mt_policy>
{
public:
    typedef std::list<_connection_base0<mt_policy> *>  connections_list;
    _signal_base0(){;}
    _signal_base0(const _signal_base0& s) : _signal_base<mt_policy>(s) {
        lock_block<mt_policy> lock(this);
        typename connections_list::const_iterator it = s.m_connected_slots.begin();
        typename connections_list::const_iterator itEnd = s.m_connected_slots.end();
        while(it != itEnd) {
            (*it)->getdest()->signal_connect(this);
            m_connected_slots.push_back((*it)->clone());
            ++it;
        }
    }
    ~_signal_base0() {
        disconnect_all();
    }
    bool is_empty()
    {
        lock_block<mt_policy> lock(this);
        typename connections_list::const_iterator it = m_connected_slots.begin();
        typename connections_list::const_iterator itEnd = m_connected_slots.end();
        return it == itEnd;
    }
    void disconnect_all()
    {
        lock_block<mt_policy> lock(this);
        typename connections_list::const_iterator it = m_connected_slots.begin();
        typename connections_list::const_iterator itEnd = m_connected_slots.end();
        while(it != itEnd) {
            (*it)->getdest()->signal_disconnect(this);
            delete *it;
            ++it;
        }
        m_connected_slots.erase(m_connected_slots.begin(), m_connected_slots.end());
    }
    void disconnect(has_slots<mt_policy>* pclass)
    {
        lock_block<mt_policy> lock(this);
        typename connections_list::iterator it = m_connected_slots.begin();
        typename connections_list::iterator itEnd = m_connected_slots.end();
        while(it != itEnd) {
            if((*it)->getdest() == pclass) {
                delete *it;
                m_connected_slots.erase(it);
                pclass->signal_disconnect(this);
                return;
            }
            ++it;
        }
    }
    void slot_disconnect(has_slots<mt_policy>* pslot)
    {
        lock_block<mt_policy> lock(this);
        typename connections_list::iterator it = m_connected_slots.begin();
        typename connections_list::iterator itEnd = m_connected_slots.end();
        while(it != itEnd) {
            typename connections_list::iterator itNext = it;
            ++itNext;
            if((*it)->getdest() == pslot) {
                delete *it;
                m_connected_slots.erase(it);
            }
            it = itNext;
        }
    }
    void slot_duplicate(const has_slots<mt_policy>* oldtarget, has_slots<mt_policy>* newtarget)
    {
        lock_block<mt_policy> lock(this);
        typename connections_list::iterator it = m_connected_slots.begin();
        typename connections_list::iterator itEnd = m_connected_slots.end();

        while(it != itEnd)
        {
            if((*it)->getdest() == oldtarget)
            {
                m_connected_slots.push_back((*it)->duplicate(newtarget));
            }

            ++it;
        }
    }

protected:
    connections_list m_connected_slots;
};

4.2.3 has_slots

template<class mt_policy = SIGSLOT_DEFAULT_MT_POLICY>
class has_slots : public mt_policy
{
private:
    typedef typename std::set<_signal_base<mt_policy> *> sender_set;
    typedef typename sender_set::const_iterator const_iterator;
public:
    has_slots(){;}
    has_slots(const has_slots& hs) : mt_policy(hs) {
        lock_block<mt_policy> lock(this);
        const_iterator it = hs.m_senders.begin();
        const_iterator itEnd = hs.m_senders.end();
        while(it != itEnd) {
            (*it)->slot_duplicate(&hs, this);
            m_senders.insert(*it);
            ++it;
        }
    }
    void signal_connect(_signal_base<mt_policy>* sender) {
        lock_block<mt_policy> lock(this);
        m_senders.insert(sender);
    }
    void signal_disconnect(_signal_base<mt_policy>* sender) {
        lock_block<mt_policy> lock(this);
        m_senders.erase(sender);
    }
    virtual ~has_slots() {
        disconnect_all();
    }
    void disconnect_all() {
        lock_block<mt_policy> lock(this);
        const_iterator it = m_senders.begin();
        const_iterator itEnd = m_senders.end();
        while(it != itEnd) {
            (*it)->slot_disconnect(this);
            ++it;
        }
        m_senders.erase(m_senders.begin(), m_senders.end());
    }
private:
    sender_set m_senders;
};

容易看出,signal/connect/has_slots<>这三个部分还是组成了一个观察者模式。与我们之前自己实现的观察者不同的是:

  • 模板使任意类型参数成为了可能
  • has_slot使所有观察者具有统一接口

google的libjingle开源项目中曾经将这个库添加了一个将近100行的文件,sigslotrepeater.h,用于处理signal A 绑定 signal B 而不用中转到一个slot函数中处理。实际上这个模型并不是线程安全的(尽管开发者曾模棱两可的声明过线程安全)。后续章节将从多线程的角度深入分析C++的回调模型。




  

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

分类: C++ 665人阅读 评论(0) 收藏 举报

目录(?)[+]

作者: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);
    }
    // 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队列上,其中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新特性介绍中一起讨论。



0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:31390次
    • 积分:1392
    • 等级:
    • 排名:千里之外
    • 原创:90篇
    • 转载:109篇
    • 译文:6篇
    • 评论:11条
    最新评论