1 生产者、消费者模型
多年以来,我写的服务端程序经常用到生产者、消费者模型,比如IMS的各种微服务。
讲生产者、消费者模型机制的文章太多,此处省略,本文主要是记录测试性能,权当笔记。
1.1 多线程
以模板方法实现一个生产者、消费者模型,消费者模型只是一个函数指针,该指针在模型被继承时由子类指定实际的工作线程。
工作线程的数量按需指定,通常等于CPU核数的2倍。
1.2 对象管理
消息达到时,需要由指定的某个对象来处理,比如用户A发送的消息总是由A的对象
处理。
这些处理消息的对象,可能数量巨大,对象的数量将决定系统的处理容量,如果单进程计划支持N个用户,那么可分配一个大小为N的数组m_lpObjectManage_T
来管理这些对象,节点定义:
typedef struct{
LPVOID ObjectAdr; //对象的地址
THREAD_ID ThreadID; //对象对应的线程ID
INSTANCE_STATUS Status; //对象的状态,初始EMPTY,空闲FREE,工作中BUSY
} OBJ_MNG_INFO, *lpOBJ_MNG_INFO;
刚new出数组m_lpObjectManage_T
时,数组的每个节点的Status
是EMPTY,节点的ObjectAdr
都是空指针,只有当需要某个对象来处理消息时才会new出该对象,并存储在节点的ObjectAdr
,延迟分配的目的是为了降低内存占用。
1.3 线程与对象的关系
每个对象在被生成时将获得一个对象ID InstanceID
。InstanceID
% ObjectNum
得到的值即为线程ID,即每个对象由哪个线程处理是在new对象时就决定的。
1.4 消息与线程的关系
每个线程有自己的消息队列。
当收到一条消息时,需确认由哪个对象处理。先根据业务规则计算出对象InstanceID
(如果InstanceID
不存在,则new出新对象),根据InstanceID
计算出线程ID(前述求余方法),并派发消息到该线程的消息队列中。每个线程从各自的队列按FIFO顺序消费消息,并用关联的对象来处理消息。
2 线程对象模型的虚函数接口
在开发实际业务时,上层对象的类型各不相同,因此其构造、初始化、删除等方法需由开发者提供;同理,消息体可能各不相同,消息如何释放也须由开发者提供。
//创建对象 (由业务层提供)
virtual LPVOID CreateObjectSub( int ObjectID, LPVOID userData = nullptr) = 0;
//销毁对象 (由业务层提供)
virtual BOOL DeleteObjectSub( LPVOID lpObjAdr ) = 0;
//初始化对象 (由业务层提供)
virtual BOOL InitInstance( LPVOID lpObjAdr, LPVOID userOutput ) = 0;
//释放对象 (由业务层提供)
virtual BOOL ReleaseInstance( LPVOID lpObjAdr ) = 0;
//销毁消息 (由业务层提供)
virtual BOOL DeleteMessage( LPVOID lpMessage ) = 0;
3 线程对象模型的public接口
public:
ObjThread( int ObjectType,
int ThreadMax,
int ObjectMax,
int waitTime,
int queuingNum,
int DeadlockCoredown );
BOOL RegistThread(CPA_THREADPROC ThreadAdr, char* ThreadName );
BOOL DeleteData( void );
BOOL PushMessage( LPVOID lpMessage, UINT ObjectID );
int PopMessage( THREAD_ID ThreadID,
LPVOID* lplpObjectAdr,
LPVOID* lplpMessage );
BOOL DelMessage( int ObjectID, LPVOID lpMessage, BOOL bDeleteObj );
int CreateObject( lpCREATE_OBJ_INFO lpObjectInfo = nullptr );
BOOL DeleteObject( int ObjectID );
void ReleaseAllMutex();
void ClearAllMutex();
4 性能测试
2020-07-14注:今天在阿里云机器上意外发现性能比华为云机器有大幅提升,不得不重新补做性能测试。
4.1 三个环境的测试对比
测试条件:
- 1个线程模拟生产者,for循环生产出1000万条消息,即执行1000万次
MessageDispatch()
; - 消息平均分散给1万个对象处理;
- n个工作线程消费消息,华为云主机和HP主机开16线程,阿里云主机双核开4线程;
- 仅为验证消息吞吐效率,采用简单的消息体定义:
typedef struct{ int num; }TEST_MSG_BLK, *lpTEST_MSG_BLK;
3种测试方法:
- 单纯new和delete(测试用)
- 生产、不消费(测试用)
- 生产、消费,这是生产用的场景,生产者、消费者线程都工作
在不同的测试环境下,耗时差异较大,统计如下:
测试项(1000万条消息) | 华为8核云主机1 | 阿里云2核云主机2 | 4核HP主机3 |
---|---|---|---|
单纯new和delete(测试用) | 0.8秒 = 1250万条/秒 | 0.053秒 = 18867万条/秒 | - |
生产、不消费(测试用) | 1.5秒 = 666万条/秒 | 1.63秒 = 613万条/秒 | - |
生产、消费(生产用) | 6.1秒 = 164万条/秒 | 2.5秒 = 400万条/秒 | 8秒 = 125万条/秒 |
- 以上表格的前2行,没有生产意义,仅用于确认线程工作的性能消耗的时间分布。
- 华为云主机比阿里云主机也差得太多了吧?!阿里云机器2核,跑4个线程;华为云机器,4~16线程的效果几乎一样,具体配置看文后注释
4.2 工作线程是否启用信号量的性能差异
以上测试,工作线程在取不到消息时,将主动Sleep 2毫秒,然后再查询线程队列是否有消息。这样做一定程度上会增加CPU消耗。如果改用信号量通知,将不需要Sleep 2毫秒。
时间对比(以阿里云机器测试数据为例):
测试项 | Sleep 2毫秒 | 等待信号量(Windows) | 等待信号量(CentOS) |
---|---|---|---|
生产、消费 | 6.1秒 | 16.7秒 | 30~34秒 |
结论:采用信号量会大幅降低工作线程的消费能力。
2020-09-04 用一台新的C6(2核4G),跑出每秒512万条,哇!很爽啊。(同样开4线程消费者)
5 待确认的测试
以下修改或许可提高性能,待测:
- 针对消息,采用内存池,避免每次的new和delete;
- 针对对象,引入对象池或提前分配对象;
- 用spinlock替代mutex(2020-07-14已验证,可提高处理性能20%)