系列文章目录
【c++重写Skynet底层01】skynet实战—sunnet基本框架,创建,开启,等待线程退出方法,模仿skynet写消息类
【c++重写Skynet底层02】skynet实战—模仿skynet写服务类,多线程下的对象管理,自旋锁互斥锁程序编写,哈希表管理对象,总结程序运行步骤
【c++重写Skynet底层03】skynet实战—全局消息队列的插入与弹出,模仿skynet发送消息,服务间的消息传送和消息处理
【c++重写Skynet底层04】skynet实战—演示程序PingPong,工作线程的等待与唤醒,改进版调度功能
前言
本章中演示了PinPong演示案例,并介绍了条件变量和工作线程的等待和唤醒方法,实现了改进版的调度功能。
一、演示程序PingPong
至此,我们已经完成了Sunnet系统的调度功能,实现了一套Actor模型,接下来通过PingPong示例来演示Actor模型功能:
1.1 辅助函数
为减少代码量,先编写一个辅助方法MakeMsg用于创建消息。MakeMsg只是简单地创建ServiceMsg对象,并给他的属性进行赋值:
需要在Sunnet.h的成员函数中声明:
//Sunnet.h
....
void PushGlobalQueue(shared_ptr<Service> srv);
//新增仅测试
shared_ptr<BaseMsg> MakeMsg(uint32_t source, char* buff, int len);
//Sunnet.cpp
//仅测试用,buff须由new产生
shared_ptr<BaseMsg> Sunnet::MakeMsg(uint32_t source, char* buff, int len) {
auto msg= make_shared<ServiceMsg>();
msg->type = BaseMsg::TYPE::SERVICE;
msg->source = source;
//基本类型的对象没有析构函数
//所以回收基本类型组成的数组空间用delete 和 delete[]都可以
//无需重新析构方法
msg->buff = shared_ptr<char>(buff);
msg->size = len;
return msg;
}
1.2 编写ping服务
在服务类Service.cpp中的OnMsg回调用编写代码:
//Service.cpp
//收到消息时触发
void Service::OnMsg(shared_ptr<BaseMsg> msg) {
// 测试用
if(msg->type == BaseMsg::TYPE::SERVICE) {
auto m = dynamic_pointer_cast<ServiceMsg>(msg);
cout << "[" << id <<"] OnMsg " << m->buff << endl;
auto msgRet = Sunnet::inst->MakeMsg(id,
new char[9999999] { 'p', 'i', 'n', 'g', '\0' }, 9999999);
Sunnet::inst->Send(m->source, msgRet);
}
else {
cout << "[" << id <<"] OnMsg" << endl;
}
}
程序会打印出收到的消息内容,可以将shared_ptr<BaseMsg转换成shared_ptr<ServiceMsg类型,代码中的auto相当于shared_ptr<ServiceMsg
OnMsg方法创建的回应消息msgRet中,其buff长度为999999,是为了能够明显暴露内存泄漏问题。
在main.cpp中添加测试函数:
//main.cpp
#include <string>
#include "Sunnet.h"
using namespace std;
int test() {
auto pingType = make_shared<string>("ping");
uint32_t ping1 = Sunnet::inst->NewService(pingType);
uint32_t ping2 = Sunnet::inst->NewService(pingType);
uint32_t pong = Sunnet::inst->NewService(pingType);
auto msg1 = Sunnet::inst->MakeMsg(ping1,
new char[3] { 'h', 'i', '\0' }, 3);
auto msg2= Sunnet::inst->MakeMsg(ping2,
new char[6] { 'h', 'e', 'l', 'l', 'o', '\0' }, 6);
Sunnet::inst->Send(pong, msg1);
Sunnet::inst->Send(pong, msg2);
return 0;
}
int main(){
new Sunnet();
Sunnet::inst->Start();
test();
Sunnet::inst->Wait();
return 0;
}
发送的流程如下:
ping1和ping2先向pong发送初始消息,然后ping1和pong,ping2和pong之间不断交互
编译运行后:
可见,pong服务先收到hi,和hello两条消息,之后ping1和pong,ping2和pong之间不断发送ping
二、条件变量
条件变量是一种线程同步机制,若暂无待处理服务,工作线程将进入休眠状态,而在将服务插入到全局队列中,除了将服务插入到末端队列以为ia,还会唤醒正在休眠的线程,这样就能实现某种程度上的效率与零延迟。
条件变量机制分为休眠和唤醒两条线程,一条线程进入休眠,另一条线程在必要时唤醒它。
//Sunnet.h
#pragma once
#include <vector>
#include "Worker.h"
#include "Service.h"
#include <unordered_map>
using namespace std;
class Worker;
class Sunnet {
public:
//单例
static Sunnet* inst;
public:
//构造函数
Sunnet();
//初始化并开始
void Start();
//等待运行
void Wait();
//增删服务
uint32_t NewService(shared_ptr<string> type);
void KillService(uint32_t id); //仅限服务自己调用
//发送消息
void Send(uint32_t toId, shared_ptr<BaseMsg> msg);
//全局队列操作
shared_ptr<Service> PopGlobalQueue();
void PushGlobalQueue(shared_ptr<Service> srv);
//让工作线程等待
void WorkerWait();
//唤醒工作线程
void CheckAndWeakUp();
//仅测试
shared_ptr<BaseMsg> MakeMsg(uint32_t source, char* buff, int len);
private:
//工作线程
int WORKER_NUM = 3; //工作线程数(配置)
vector<Worker*> workers; //worker对象
vector<thread*> workerThreads; //线程
//服务列表
unordered_map<uint32_t, shared_ptr<Service>> services; //string还是service?>?>
uint32_t maxId = 0; //最大ID
pthread_rwlock_t servicesLock; //读写锁
//全局队列
queue<shared_ptr<Service>> globalQueue;
int globalLen = 0; //队列长度
pthread_spinlock_t globalLock; //锁
//休眠和唤醒
pthread_mutex_t sleepMtx;
pthread_cond_t sleepCond;
int sleepCount = 0; //休眠工作线程数
private:
//开启工作线程
void StartWorker();
//获取服务
shared_ptr<Service> GetService(uint32_t id);
};
实现休眠-唤醒需要由一个互斥锁和一个条件变量配合工作。sleepcount用于记录sunnet系统中由多少工作线程处于休眠状态,如果全部线程都在工作,就无需浪费资源去唤醒。
既然使用到了锁,就需要在系统中进行初始化:
//Sunnet.cpp
//开启系统
void Sunnet::Start() {
cout << "Hello Sunnet" << endl;
//锁
pthread_rwlock_init(&servicesLock, NULL);
pthread_spin_init(&globalLock, PTHREAD_PROCESS_PRIVATE);
//新增
pthread_cond_init(&sleepCond, NULL);
pthread_mutex_init(&sleepMtx, NULL);
//开启Worker
StartWorker();
}
接下来编写休眠代码WorkerWait()和唤醒代码CheckAndWeakUp():
//Sunnet.cpp
//Worker线程调用,进入休眠
void Sunnet::WorkerWait(){
pthread_mutex_lock(&sleepMtx);
sleepCount++;
pthread_cond_wait(&sleepCond, &sleepMtx);
sleepCount--;
pthread_mutex_unlock(&sleepMtx);
}
//检查并唤醒线程
void Sunnet::CheckAndWeakUp(){
//unsafe
if(sleepCount == 0) {
return;
}
if( WORKER_NUM - sleepCount <= globalLen ) {
cout << "weakup" << endl;
pthread_cond_signal(&sleepCond);
}
}
最后,在恰当的场合调用以上两个函数,用修换替换工作线程的usleep。
//Worker.h
//线程函数
void Worker::operator()() {
while(true) {
shared_ptr<Service> srv = Sunnet::inst->PopGlobalQueue();
if(!srv){
// usleep(100); //0.1s
Sunnet::inst->WorkerWait();
}
else{
srv->ProcessMsgs(eachNum);
CheckAndPutGlobal(srv);
}
//cout << "working id:" <<id <<endl;
}
}
在Sunnet.cpp唤起进程:
//Sunnet.cpp
//发送消息
void Sunnet::Send(uint32_t toId, shared_ptr<BaseMsg> msg){
shared_ptr<Service> toSrv = GetService(toId);
if(!toSrv){
cout << "Send fail, toSrv not exist toId:" << toId << endl;
return;
}
toSrv->PushMsg(msg);
//检查并放入全局队列
//为缩小临界区灵活控制,破坏封装性
bool hasPush = false;
pthread_spin_lock(&toSrv->inGlobalLock);
{
if(!toSrv->inGlobal) {
PushGlobalQueue(toSrv);
toSrv->inGlobal = true;
hasPush = true;
}
}
pthread_spin_unlock(&toSrv->inGlobalLock);
//唤起进程,不放在临界区里面
if(hasPush) {
CheckAndWeakUp();
}
}
编译运行后,就可以看到改进版的调度功能:从日志中可以看到CheckAndWeakUp所打印的weakup
总结
本章中演示了PinPong演示案例,并介绍了条件变量和工作线程的等待和唤醒方法,实现了改进版的调度功能。
在本系列的学习中,我们跟随Sunnet调度系统的案例,学习了C++多线程,锁,条件变量等知识,系列到此先暂时完结,后面可能会以实现Echo为目标,给Sunnet添加网络功能。