Linux C++通讯架构【六】:多线程服务业务处理逻辑

235 阅读6分钟

线程池

  1. 业务逻辑线程:
  • 和系统线程概念不一样,用户线程和系统线程有1:n、m:n、1:1,Linux和windows一般都用的1:1模型,执行效率块,但有最大线程限制。

  • iocp(windows)启动时就会开启cpu*2+2个线程,这是操作系统的线程,和业务处理(充值、抽卡)无关。

  1. 主线程往消息队列扔包,其他线程从里面取走这个包(互斥)
  • posix线程:标准化的线程标准,说白了就是一堆我们可以调用的函数,一般是以pthread_开头=》posix库并不是Linux默认的库

  • 所以编译的时候,makefile要指令 -lpthread

  1. 线程池:
  • 结构:消息队列、消息队列计数器、消息队列互斥量

  • 线程操作:

    • 向消息队列添加包:一个主线程(也要先加互斥锁),要告诉线程池来干活

    • 从消息队列拿包:多个子线程,先加互斥锁(也就是初始化互斥量,函数结束后,这个互斥量会被自动析构(析构函数))

  • 线程池的好处:

    • 提升稳定性:避免创建线程失败
    • 提升效率:复用
  1. 线程池类:
  • 线程池创建在epoll之前,因为epoll来数据了你必须要处理,threadPool_init 和 epoll_init必须加个sleep(1),免得主线程已经初始化了,线程池还没创建完

  • 成员:

    • 一个结构ThreadItem: =》 和一个线程绑定起来
    • 线程句柄 : 需要一块内存啦
    • 记录线程池的指针
    • 线程池是否正式启动的标志
    • 构造和析构函数
    • 线程同步互斥量(线程同步锁)
    • 线程同步条件变量
    • 线程退出标志
    • 要创建的线程数量(配置文件中,不建议超过300)、运行的线程数量thread_busy_num(atomic原子操作,多线程里需要)
    • 线程容器:vector<ThreadItem *>
  • 成员函数:

    • 创建线程中所有线程:

      • 一个线程的创建:创建一个新线程对象(空间以及这个空间有哪些数据),添加到容器;根据这个线程对象,创建线程。

      • pthread_create:线程对象句柄(pthread_t线程空间),线程入口函数,函数参数

      • 创建所有线程后,线程就开始执行入口函数,所有线程需要都卡在一个地方休眠

    • 精华:

    202308032216789
    • 达到所有线程都休眠的初始状态:

      • 进入while循环(消息队列可获取数据包,且线程池需处于开启状态m_shut=false){pthread_cond_wait(条件变量,释放互斥量)},每创建一个线程,都会休眠,并释放互斥锁,使第二个也能成功休眠,达到所有子线程都成功休眠的初始状态。
    • 达到所有线程都释放的结束状态:

      • pthread_cond_broadcast(条件变量);以广播的方式唤醒所有持有条件变量的线程。

      • 将m_shut=ture,这样所有等待的线程会被唤醒跳出循环(也是一个一个跳出的),没休眠的线程也不会进入循环,会执行执行下面的线程池析构代码。

      • 返回一个线程 pthread_join(线程句柄,null),直到所有句柄全部退出,开始销毁线程池(条件变量和互斥量)

      • 优雅退出:写在主线程的while循环后,两个while:外面的是while(true){获取消息队列互斥锁;while();执行任务(取包)。。。。}

        1. 条件变量提供了一个多线程汇合的场所,条件变量+互斥变量可实现线程无竞争执行
        2. c++ 11就是wait()和notify_one()、notify_all()
    • 调用线程编程busy:

      • pthread_cond_singal:至少唤醒 一个线程(多核cpu中可能唤醒多个=》虚假唤醒=》惊群),while循环取消息队列数据

      • 惊群也不怕,因为只有一个线程能获取互斥锁,另外的线程即使被唤醒,也无法执行outMsgRecvQueue()函数,拿不到数据,将继续进入循环休眠。

LT发数据:

  1. 服务器接受到客户端的数据后,向socket(结构体)添加一个可写事件,
  2. socket可写事件:每个tcp连接都有一个接受和发送缓冲区,一般几十k。
  3. send() 或 write() 就是把数据放到发送缓存区,然后就返回,客服端内核接收(recv或read)到这些数据后,服务端才会真正把发送缓冲区的数据删掉。此时,如果发送缓存区如果有空间,服务端就可以继续send()了
  4. 所以,如果服务端发送太快,发送缓冲区就满了。socket可写就是(发送缓存区没满不停触发socket可写事件)
  5. 不停触发:写日志的话,一下就几百M
  6. 解决:
  • send()或write后,把写事件从红黑树中的socket节点移除。事件处理前后都要操作epoll,效率不太高。
  • 开始不把写事件加入到epoll,当我需要写数据时,直接调用write/send发送数据;如果发送缓存区已满,就添加到epoll中,再执行上一个方法。

信号量:

  1. 互斥量:线程间的同步
  2. 信号量:提供进程之间的同步,也能提供线程之间的同步
  • 初始化信号量(信号量地址,线程0还是进程同步,信号量初始参数0),用完后释放
  • smt_post():将指定信号量值加+1,即便没有其他线程在等待该信号
  • smt_wait():
    • 测试指定信号量的值,如果值大于0,值-1,返回理解返回
    • 如果等于0,将睡眠,直到这个值大于0

连接池中连接的回收:

  1. 如果客户端A断线,服务端立即回收连接,这个连接被B使用
  • A的进程执行10s,在第5秒就断了
  • A断线,epoll_wait是可以理解感知到的,理解收回A的连接
  • 第7s,连接给B了,这个线程并不知道这个连接归属B了;可能往错误的地址写了数据,造成服务器崩溃。
  1. 所以,连接池中的连接,如果回收了,不能立即加入到空闲链表,而是放到其他地方(延时回收链表),过个60s才放到里面去
  2. 在niginx里,是一个线程绑定一块内存,解决这个问题(这样就很好了,延时回收很麻烦)
  3. 两种情况:立即回收(accept没有接入)、延迟回收(已经开始有数据收发了)
  • 立即回收:扔到连接池中
  • 延迟回收:添加一个延迟回收链表
  1. 连接的延迟回收会不会使得连接不够用:在延迟回收链表中连接较多时,可以稍微超过最大连接数