试想一下, 有没有这种需求:
对于每一个新的对象, 我们希望它能够在一定时间后自动销毁, 前提是我们没有在这段时间内给它发出重置信号.
这种需求其实是有的, 比如在电影里, 主角知道了一个反派不希望被揭露的秘密, 同时需要保住自己的性命, 那么就可以构造这样一个对象, 如果24小时内主角不给这个对象发送重置的信号, 它就会将这个秘密公之于众. 再比如, 在网络应用场景里, 我们希望每一个客户端能够定时给我们发送心跳包, 如果长时间不发送的话, 我们就剔除这个客户.
在之前的文章里, 我尝试使用了WIN32的Timer, 但是发现这种做法非常繁琐且容易出错, 你需要给每个对象绑定一个Timer, 同时需要在Timer到期时处理对象, 并且重置Timer的API和设置Timer的API是同一个, 稍有不慎就会搞砸.
现在, 我想出了一种相对简单的实现方式, 虽然精度不是非常理想, 但对于一般应用而言, 足矣.
我们构造一个类, 它有一些私有的数据, 这些可以自定义, 但有一些API是必须的:
class Client { private: // ...Data or something int32_t m_life; int32_t m_max_life; DWORD delete_thread_id; HANDLE count_thread_handle; public: Client(int32_t, DWORD); void reset(void); static WIN32API DWORD countDownEntry(void *); DWORD countDown(void); // ...De-cons... }
1. 构造函数:
Client:Client(int32_t life, DWORD thread_id) { m_max_life = m_life = life; delete_thread_id = thread_id; count_thread_handle = CreateThread(..., ..., Client::countDownEntry, this); }
第二个参数是用来销毁对象的线程ID, 这样设计是考虑到对象有可能保存在一个堆, 如果我们简单地调用析构函数, 那么对象本身所占据的空间就无法被释放了, 所以我们通知这么一个线程来完成所有的析构操作.
注意到我们使用的是countDownEntry()而不是countDown(), 因为CreateThread不接受一个非静态的成员函数作为函数入口(无法确认地址).
2. reset()方法, 这方法需要先挂起倒计时的线程, 主要是防止同时访问同一个内存的情况出现:
void Client::reset(void) { SuspendThread(count_thread_handle); m_life = m_max_life; ResumeThread(count_thread_handle); }
3. countDownEntry()方法为何是static的? 很简单, 我们需要在构造函数里使用它来初始化倒计时线程, 而它的实现非常简单, 我们在构造函数里把this指针传递给这个静态方法, 并在静态方法里重新获取这个this代表的对象, 调用这个对象的倒计时函数即可:
static WIN32API DWORD Client::countDownEntry(void *pM) { Client *c = (Client *) pM; return c->countDown(); }
4. 而countDown()方法更加简单, 使用Sleep函数来计时即可, 每计一秒就将life减1:
DWORD Client::countDown() { while (m_life > 0) { Sleep(1000); m_life--; } PostThreadMessageA(delete_thread_id); return 0; }
以上就是这样一个对象的设计思路, 原理比较简单, 也只是写了个大概, 同时需要windows.h的支持.