从零构建通讯器--6.1到6.2业务逻辑之多线程、线程池实战(接受数据放到消息队列中,主要讲了线程池的创建和初始化)

本文详细介绍了多线程在处理业务逻辑中的应用,如充值场景,强调了线程池的使用以提高程序稳定性和效率。线程池通过预先创建线程并统一管理,避免动态创建,减少了资源消耗。文章还讨论了线程池的初始化、工作激发和线程同步机制,包括互斥量和条件变量,并提到了线程池在处理消息队列时可能遇到的唤醒丢失和惊群现象。最后,提出了使用C++11原子操作优化数据处理的建议。
摘要由CSDN通过智能技术生成

(1)学习方法
学习解决问题的思路:多学多思考
(2)多线程的提出
①上次5.9收到数据包,调用ngx_wait_handler_proc_p1收到数据包,然后调用ngx_wait_request_handler_proc_plast把数据包扔到消息队列inMsgRecvQueue中来,接下来解析数据包,处理结果《《–用线程来处理问题
②一个进程 跑起来之后缺省 就自动启动了一个 “主线程”,也就是我们一个worker进程一启动就等于只有一个“主线程”在跑;我们现在涉及到了业务逻辑层面,这个就要用多线程处理,所谓业务逻辑:充值,抽卡,战斗;
1)例如:充值,需要本服务器和专门的充值服务器通讯,一般需要数秒到数十秒的通讯时间。此时,我们必须采用多线程【100个多线程】处理方式;
2)一个线程因为充值被卡住,还有其他线程可以提供给其他玩家及时的服务(这里线程于iocp没关系,只处理逻辑);
(iocp(windows)启动线程数为cpu*2+2;)
3)互斥:
a)主线程 往消息队列中用inMsgRecvQueue()扔完整包(用户需求),那么一堆线程要从这个消息对列中取走这个包,所在必须要用互斥;
b)引入变量m_recvMessageQ,定义消息队列互斥量
在这里插入图片描述
1))在ngx_c_socket.cxx构造类中初始化互斥量在这里插入图片描述
2))在ngx_c_socket.cxx析构函数中销毁在这里插入图片描述
3))在ngx_c_socket_requests.cxx中的inMsgRecvQueue()启动CLock(实现自动加锁解锁,在ngx_c_lockmutex.h中)
在这里插入图片描述
自动加锁解锁,irmqc把消息队列的数字返回去在这里插入图片描述
4))outMsgRecvQueue()从消息队列中取数据,这个函数返回的是字符串
在这里插入图片描述
(一个主线程放消息,多个子线程从消息队列中取消息)

4)多线程名词
a)POSIX:表示可移植操作系统接口(Portable Operating System Interface of UNIX)。
b)POSIX线程:是POSIX的线程标准【大概在1995年左右标准化的】;它定义了创建和操纵线程的一套API(Application Programming Interface:应用程序编程接口),说白了 定义了一堆我们可以调用的函数,一般是以pthread_开头,比较成熟,比较好用;我们就用这个线程标准;
c)现在是NPTL线程

(3)线程池实战代码
(3.1)为什么引入线程池
1)线程池:说白了 就是 我们提前创建好一堆线程,并搞一个雷来统一管理和调度这一堆线程【这一堆线程我们就叫做线程池】,
①当来了一个任务【来了一个消息】的时候,我从这一堆线程中找一个空闲的线程去做这个任务【去干活/去处理这个消息】,
②活干完之后,我这个线程里边有一个循环语句,我可以循环回来等待新任务,再有新任务的时候再去执行新的任务;
③就好像这个线程可以回收再利用 一样;
2)线程池存在意义和价值;
①实现创建好一堆线程,避免动态创建线程来执行任务,提高了程序的稳定性;有效的规避程序运行之中创建线程有可能失败的风险;
②提高程序运行效率:线程池中的线程,反复循环再利用;
但是说到根上,用线程池的目的无非就两条:提高稳定性,提升整个程序运行效率,容易管理【使编码更清晰简单】

(4)线程池的使用
1)引入misc中的新文件ngx_c_threadpool.cxx建立线程池和头文件ngx_c_threadpool.h
还要在common.mk中72行加上-lpthread
在这里插入图片描述
2)ngx_c_threadpool.h有一个类叫CTreadPool
Create创建线程池
StopAll()使线程池所有线程退出
Call()来任务了,调用线程池的一个线程出来工作
在这里插入图片描述
3)在类中定义了类的结构
在这里插入图片描述
4)配置文件nginx.conf中配置线程池数量
在这里插入图片描述
5)bool CTreadPool线程池管理类:用来管理线程池
6)线程池的线程
为了避免线程池的异常,所有线程必须都卡在pthread_cond_wait(&m_pthreadCond,&mpthreadMutex);才认为线程启动起来,这个函数会释放互斥量,都会卡在这
7)静态成员函数,是不存在this指针的
在这里插入图片描述
8)线程池精华代码
在这里插入图片描述
从消息队列中拿出一个请求,且系统不是关闭状态的
(4.1)线程池的初始化
1)在nginx.cxx中定义了线程池的全局对象
在这里插入图片描述
2)
①子线程的for循环在ngx_worker_process_init()中初始化线程池,
②然后读配置文件中线程池的数量,
③之后触发epoll的初始化,epoll触发就来事件了(得来事件之前创建号线程池)
在这里插入图片描述
④补充上线程池创建失败就退出
在这里插入图片描述
⑤再等待sleep1秒,让所有线程都卡在pthread_cond_wait()(前面有提到)
⑥在worker进程的循环创建线程的for之外用g_threadpool.StopAll()终止线程池,释放所有线程,用brocast激活所有卡在pthread_cond_wait()的线程并都退出,
用for循环去等线程退出,一个个pthread_join掉线程的句柄
在这里插入图片描述
⑦释放锁和条件变量,释放new出来的线程
在这里插入图片描述
(4.2)线程池工作的激发
1)ngx_wait_request_handler_proc_plast()函数的inMsgRecvQueue()中来一个数据包,接下来我就激发线程池的某个线程来处理业务逻辑(irmqc为消息队列中的消息数)
在这里插入图片描述
2)用pthread_cond_signal激发某一个线程
3)单一线程线程被激活开始干活
在这里插入图片描述
补充:call函数主体

//来任务了,调一个线程池中的线程下来干活
void CThreadPool::Call(int irmqc)
{
    //ngx_log_stderr(0,"m_pthreadCondbegin--------------=%ui!",m_pthreadCond);  //数字5,此数字不靠谱
    //for(int i = 0; i <= 100; i++)
    //{
    int err = pthread_cond_signal(&m_pthreadCond); //唤醒一个等待该条件的线程,也就是可以唤醒卡在pthread_cond_wait()的线程
    if(err != 0 )
    {
        //这是有问题啊,要打印日志啊
        ngx_log_stderr(err,"CThreadPool::Call()中pthread_cond_signal()失败,返回的错误码为%d!",err);
    }
    //}
    //唤醒完100次,试试打印下m_pthreadCond值;
    //ngx_log_stderr(0,"m_pthreadCondend--------------=%ui!",m_pthreadCond);  //数字1

    
    //(1)如果当前的工作线程全部都忙,则要报警
    //bool ifallthreadbusy = false;
    if(m_iThreadNum == m_iRunningThreadNum) //线程池中线程总量,跟当前正在干活的线程数量一样,说明所有线程都忙碌起来,线程不够用了
    {        
        //线程不够用了
        //ifallthreadbusy = true;
        time_t currtime = time(NULL);
        if(currtime - m_iLastEmgTime > 10) //最少间隔10秒钟才报一次线程池中线程不够用的问题;
        {
            //两次报告之间的间隔必须超过10秒,不然如果一直出现当前工作线程全忙,但频繁报告日志也够烦的
            m_iLastEmgTime = currtime;  //更新时间
            //写日志,通知这种紧急情况给用户,用户要考虑增加线程池中线程数量了
            ngx_log_stderr(0,"CThreadPool::Call()中发现线程池中当前空闲线程数量为0,要考虑扩容线程池了!");
        }
    } //end if 

/*
    //-------------------------------------------------------如下内容都是一些测试代码;
    //唤醒丢失?--------------------------------------------------------------------------
    //(2)整个工程中,只在一个线程(主线程)中调用了Call,所以不存在多个线程调用Call的情形。
    if(ifallthreadbusy == false)
    {
        //有空闲线程  ,有没有可能我这里调用   pthread_cond_signal(),但因为某个时刻线程曾经全忙过,导致本次调用 pthread_cond_signal()并没有激发某个线程的pthread_cond_wait()执行呢?
           //我认为这种可能性不排除,这叫 唤醒丢失。如果真出现这种问题,我们如何弥补?
        if(irmqc > 5) //我随便来个数字比如给个5吧
        {
            //如果有空闲线程,并且 接收消息队列中超过5条信息没有被处理,则我总感觉可能真的是 唤醒丢失
            //唤醒如果真丢失,我是否考虑这里多唤醒一次?以尝试逐渐补偿回丢失的唤醒?此法是否可行,我尚不可知,我打印一条日志【其实后来仔细相同:唤醒如果真丢失,也无所谓,因为ThreadFunc()会一直处理直到整个消息队列为空】
            ngx_log_stderr(0,"CThreadPool::Call()中感觉有唤醒丢失发生,irmqc = %d!",irmqc);

            int err = pthread_cond_signal(&m_pthreadCond); //唤醒一个等待该条件的线程,也就是可以唤醒卡在pthread_cond_wait()的线程
            if(err != 0 )
            {
                //这是有问题啊,要打印日志啊
                ngx_log_stderr(err,"CThreadPool::Call()中pthread_cond_signal 2()失败,返回的错误码为%d!",err);
            }
        }
    }  //end if

    //(3)准备打印一些参考信息【10秒打印一次】,当然是有触发本函数的情况下才行
    m_iCurrTime = time(NULL);
    if(m_iCurrTime - m_iPrintInfoTime > 10)
    {
        m_iPrintInfoTime = m_iCurrTime;
        int irunn = m_iRunningThreadNum;
        ngx_log_stderr(0,"信息:当前消息队列中的消息数为%d,整个线程池中线程数量为%d,正在运行的线程数量为 = %d!",irmqc,m_iThreadNum,irunn); //正常消息,三个数字为 1,X,0
    }
    */
    return;
}

//唤醒丢失问题,sem_t sem_write;
//参考信号量解决方案:https://blog.csdn.net/yusiguyuan/article/details/20215591  linux多线程编程--信号量和条件变量 唤醒丢失事件

(4.3)线程池完善和测试
a)我只开一个线程【线程中暂时测只有一个线程】,需要验证知否遗漏消息–》》
在这里插入图片描述
添加C++11中原子整型数量,比互斥量要快(头文件添加atomic,C++中原子操作),有数据处理+1,无数据处理要减1
在这里插入图片描述
在这里插入图片描述
common.mk修改加上-std-c++11
在这里插入图片描述
gcc改成(CC)
在这里插入图片描述
若线程池全被调用了,要么减少连入的客户端数量,要么增加线程数,10秒报一下错误
在这里插入图片描述
b)惊群(也叫虚假唤醒):处于cond_wait状态的线程,收到call的signal函数会激活多个线程。
但是: 不用担心,一般都会设置拿到消息队列的数据才会跳出循环开始执行,没拿到消息队列的数据还是处于循环当中
在这里插入图片描述
c)前面a)中提到的多个消息用了原子操作后会堆积,但不会丢消息,消息会逐条处理

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值