【c++重写Skynet底层03】skynet实战---全局消息队列的插入与弹出,模仿skynet发送消息,服务间的消息传送和消息处理

系列文章目录

【c++重写Skynet底层01】skynet实战—sunnet基本框架,创建,开启,等待线程退出方法,模仿skynet写消息类
【c++重写Skynet底层02】skynet实战—模仿skynet写服务类,多线程下的对象管理,自旋锁互斥锁程序编写,哈希表管理对象,总结程序运行步骤
【c++重写Skynet底层03】skynet实战—全局消息队列的插入与弹出,模仿skynet发送消息,服务间的消息传送和消息处理
【c++重写Skynet底层04】skynet实战—演示程序PingPong,工作线程的等待与唤醒,改进版调度功能



前言

本章会在之前的基础上编写全局消息队列的插入与弹出,使工作线程能够调度各种服务,然后模仿skynet发送消息,编写工作线程的调度代码。


一、全局消息队列

要完成服务间的消息传送和消息处理,就需要有一个全局队列来处理调度各个服务,工作线程必须知道哪些服务有消息需要处理,哪些服务可以暂时忽略。因此可以将待处理的服务存入到全局队列当中,由工作线程在全局队列中取出服务进行处理。
以下代码为sunnet.h新增的变量和方法:
globalQueue代表全局队列,使用智能指针引用着“存有待处理消息”的服务。由于多个线程可能同时读写全局队列,因此定义了自旋锁globalLocal进行加锁。

//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);
 
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;   //锁


private:
    //开启工作线程
    void StartWorker();
       //获取服务
    shared_ptr<Service> GetService(uint32_t id);

};

既然定义了锁,就需要在初始化函数中初始化锁,目前sunnet类需要初始化两个锁:


//开启系统
void Sunnet::Start() {
    cout << "Hello Sunnet" << endl;
    //锁
    pthread_rwlock_init(&servicesLock, NULL);
    pthread_spin_init(&globalLock, PTHREAD_PROCESS_PRIVATE);  
    //开启Worker
    StartWorker();
}

工作线程每次会处理一定数量的消息,再根据服务是否空闲,决定是否将它重新插会全局队列(后面实现)

二、全局队列的插入与弹出

2.1. 全局队列的弹出

由于涉及多线程操作,因此插入和弹出全局队列的操作必须加锁,以下代码展示了弹出队列的方法:

//Sunnet.cpp
//弹出全局队列
shared_ptr<Service> Sunnet::PopGlobalQueue(){
    shared_ptr<Service> srv = NULL;
    pthread_spin_lock(&globalLock);
    {
        if (!globalQueue.empty()) {
            srv = globalQueue.front();
            globalQueue.pop();
            globalLen--;
        }
    }
    pthread_spin_unlock(&globalLock);
    return srv;
}

2.2 全局队列的插入

与弹出操作十分相似:

//Sunnet.cpp
//插入全局队列
void Sunnet::PushGlobalQueue(shared_ptr<Service> srv){
    pthread_spin_lock(&globalLock);
    {
        globalQueue.push(srv);
        globalLen++;
    }
    pthread_spin_unlock(&globalLock);
}

2.3 标志位

按照目前的设计,工作线程可以很轻易获取到"待处理的服务",但如果想要知道一个服务是否在全局队列中,只能遍历,很慢。因此给服务类添加一个成员变量inGlobal,用于标注服务是否在队列中。
由于inGlobal也有可能被多个线程同时访问,因此也需要定义自旋锁inGlobalLock,以锁住inGlobal:

//Service.h
#pragma once
#include <queue>
#include <thread>
#include "Msg.h"

using namespace std;

class Service {
public:
    //为效率灵活性放在public

    //唯一id
    uint32_t id;
    //类型
    shared_ptr<string> type;
    // 是否正在退出
    bool isExiting = false;
    //消息列表和锁
    queue<shared_ptr<BaseMsg>> msgQueue;
    pthread_spinlock_t queueLock;

    //新增
    //标记是否在全局队列  true:在队列中,或正在处理
    bool inGlobal = false;
    pthread_spinlock_t inGlobalLock;
public:       
    //构造和析构函数
    Service();
    ~Service();
    //回调函数(编写服务逻辑)
    void OnInit();
    void OnMsg(shared_ptr<BaseMsg> msg);
    void OnExit();
    //插入消息
    void PushMsg(shared_ptr<BaseMsg> msg);
    //执行消息
    bool ProcessMsg();
    void ProcessMsgs(int max);  
    //全局队列
    void SetInGlobal(bool isIn);
private:
    //取出一条消息
    shared_ptr<BaseMsg> PopMsg();
};

千万不要忘记将锁进行初始化和销毁:

//Service.cpp
//构造函数
Service::Service() {
    //初始化锁
    pthread_spin_init(&queueLock, PTHREAD_PROCESS_PRIVATE);//看看参数有什么区别,Skynet怎么用的
    //新增
    pthread_spin_init(&inGlobalLock, PTHREAD_PROCESS_PRIVATE);
}

//析构函数
Service::~Service(){
    pthread_spin_destroy(&queueLock);
        //新增
    pthread_spin_destroy(&inGlobalLock);
}

接下来实现方法,方法非常简单,就是将inGlobal放到锁中:

//Service.cpp
void Service::SetInGlobal(bool isIn) {
    pthread_spin_lock(&inGlobalLock);
    {
        inGlobal = isIn;
    }
    pthread_spin_unlock(&inGlobalLock);
}

三、模仿skynet发送消息

服务间的发送消息的全过程:服务1向服务2发送消息,将消息插入到服务2的消息队列中,如果服务2不在全局队列中,将它插入到全局队列,使工作线程能处理它。

send方法的时序流程是这样的:

① GetService查找目标服务,GetService里面用到了读写锁,但速度很快

② 调用目标服务的PushMsg向消息队列中插入数据,期间也会用自旋锁锁住消息队列

③ 由于任务服务都有可能用send方法,程序先对服务的inGlobal 加锁,判断服务是否已经在全局队列之中,如果不在调用PushGlobalQueue添加到全局队列中并设置inGlobal的值。

//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);
}

临界去的代码很巧妙,先给inGlobal加锁,再给globalQueue(PushGlobalQueue方法中的锁),加锁

四、工作线程的调度

只差最后一步,即实现工作线程的调度,让它们读取全局队列中的服务,处理服务消息
以下代码重写Worker线程的线程函数:

//Worker.cpp
#include "Service.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全局队列中获取一个服务,调用ProcessMsgs方法处理eachNum条消息,
②处理完后,调用CheckAndPutGlobal(后面实现),它会判断服务是否还有未处理的消息,如果有,重新放回全局队列中等待下一次处理。
③如果队列为空,线程将会等待100微妙,然后进入下一次循环。

在开启worker线程的时候,会给不同的线程的eachNum 分别赋值id的几次方(0,1,2,4,8)等
目的是:由于从全局队列取出服务,把服务重新放回全局队列都用到了锁,因此也会有一定的时间开销,worker[0]表示每处理一条就会有一次损耗,worker[1]表示每处理两条才会有一次损耗,worker[2]表示每处理四条消息才会有一次损耗,因此worker[2]的。
但并不是eachNum越大越好,过大的eachNum会导致worker线程一直在处理某几个服务,那些在全局队列中等待的服务会得不到及时的处理,会有较高的延迟。

 worker->eachNum = 2 << i;

以下是CheckAndPutGlobal的实现:
在工作线程处理完消息后,必要时把服务重新放回全局队列当中,判断服务是否还有为处理完的消息,如有,把它放回全局队列。

先在.h文件中声明:

//Worker.h
#pragma once
#include <thread> 
#include "Sunnet.h"
#include "Service.h"
class Sunnet;
using namespace std;
class Worker { 
public:
    int id;             //编号
    int eachNum;        //每次处理多少条消息
    void operator()();  //线程函数
private:
    //辅助函数
    void CheckAndPutGlobal(shared_ptr<Service> srv);
};
//Worker.cpp
//那些调Sunnet的通过传参数解决
//状态是不在队列中,global=true
void Worker::CheckAndPutGlobal(shared_ptr<Service> srv) {
    //退出中(只能自己调退出,isExiting不会线程冲突)
    if(srv->isExiting){ 
        return; 
    }

    pthread_spin_lock(&srv->queueLock);
    {
        //重新放回全局队列
        if(!srv->msgQueue.empty()) {
            //此时srv->inGlobal一定是true
            Sunnet::inst->PushGlobalQueue(srv);
        }
        //不在队列中,重设inGlobal
        else {
            srv->SetInGlobal(false);
        }
    }
    pthread_spin_unlock(&srv->queueLock);
}

由于Worker和Sunnet是相互引用的,因此需要再Sunnet.h中添加前置声明:

class Worker;

需要注意两点:
①若服务处于退出状态(isExiting为True),工作线程将不把服务放入全局队列中,智能指针计数为0,系统会销毁它
②虽然把inGlobal称为服务是否在全局队列中,但它也包含了服务在全局队列中与正在处理消息两层含义。因此,在重新返回全局队列的过程中,如果尚有未处理的消息,服务的inGlobal一定为True,无需再次设置。

总结

本章在之前的基础上编写全局消息队列的插入与弹出,使工作线程能够调度各种服务,然后模仿skynet发送消息,编写工作线程的调度代码。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值