生产者、消费者多线程模型性能验证

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 InstanceIDInstanceID % 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. 1个线程模拟生产者,for循环生产出1000万条消息,即执行1000万次MessageDispatch()
  2. 消息平均分散给1万个对象处理;
  3. n个工作线程消费消息,华为云主机和HP主机开16线程,阿里云主机双核开4线程;
  4. 仅为验证消息吞吐效率,采用简单的消息体定义:
    typedef struct{
    	int num;
    }TEST_MSG_BLK, *lpTEST_MSG_BLK;
    
    

3种测试方法:

  1. 单纯new和delete(测试用)
  2. 生产、不消费(测试用)
  3. 生产、消费,这是生产用的场景,生产者、消费者线程都工作

在不同的测试环境下,耗时差异较大,统计如下:

测试项(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万条/秒
  1. 以上表格的前2行,没有生产意义,仅用于确认线程工作的性能消耗的时间分布。
  2. 华为云主机比阿里云主机也差得太多了吧?!阿里云机器2核,跑4个线程;华为云机器,4~16线程的效果几乎一样,具体配置看文后注释

4.2 工作线程是否启用信号量的性能差异

以上测试,工作线程在取不到消息时,将主动Sleep 2毫秒,然后再查询线程队列是否有消息。这样做一定程度上会增加CPU消耗。如果改用信号量通知,将不需要Sleep 2毫秒。
时间对比(以阿里云机器测试数据为例):

测试项 Sleep 2毫秒 等待信号量(Windows) 等待信号量(CentOS)
生产、消费 6.1秒 16.7秒 30~34秒

结论:采用信号量会大幅降低工作线程的消费能力。

5 待确认的测试

以下修改或许可提高性能,待测:

  1. 针对消息,采用内存池,避免每次的new和delete;
  2. 针对对象,引入对象池或提前分配对象;
  3. 用spinlock替代mutex(2020-07-14已验证,可提高处理性能20%)

  1. 华为云主机,8核16G Intel® Xeon® Gold 6161 CPU @ 2.20GHz ↩︎

  2. 阿里云主机,2核8G Intel® Xeon® Platinum 8163 CPU @ 2.50GHz ↩︎

  3. HP笔记本测试,i7-6700H,4核8线程,16G内存 ↩︎

你可能感兴趣的:(性能架构)