【c++重写Skynet底层04】skynet实战---演示程序PingPong,工作线程的等待与唤醒,改进版调度功能

系列文章目录

【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添加网络功能。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值