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

  • 业务逻辑线程:

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

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

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

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

  • 线程池:

    • 结构:消息队列、消息队列计数器、消息队列互斥量

    • 线程操作:

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

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

    • 线程池的好处:

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

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

      • 创建线程中所有线程:

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

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

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

      • 精华:Linux C++通讯架构【六】:多线程服务业务处理逻辑_第1张图片

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

          • 进入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. 解决:(并不是很懂。。。)
        1. send()或write后,把写事件从红黑树中的socket节点移除。事件处理前后都要操作epoll,效率不太高。
        2. 开始不把写事件加入到epoll,当我需要写数据时,直接调用write/send发送数据;如果发送缓存区已满,就添加到epoll中,再执行上一个方法。
    • gdb:
      1. 首先,编译时g++ 要带 -g选项
      2. gdb默认调试主进程,gdb7.0可以调试子进程
      3. b loginc/ngx_c_slogic.cxx:198 打断点
      4. run 运行到断点
      5. print 变量值
      6. c 继续运行
    • 信号量:
      1. 互斥量:线程间的同步
      2. 信号量:提供进程之间的同步,也能提供线程之间的同步
        1. 初始化信号量(信号量地址,线程0还是进程同步,信号量初始参数0),用完后释放
        2.  smt_post():
          1. 将指定信号量值加+1,即便没有其他线程在等待该信号
        3. smt_wait():
          1. 测试指定信号量的值,如果值大于0,值-1,返回理解返回
          2. 如果等于0,将睡眠,直到这个值大于0
    • 连接池中连接的回收:
      1. 如果客户端A断线,服务端立即回收连接,这个连接被B使用
        1. A的进程执行10s,在第5秒就断了
        2. A断线,epoll_wait是可以理解感知到的,理解收回A的连接
        3. 第7s,连接给B了,这个线程并不知道这个连接归属B了;可能往错误的地址写了数据,造成服务器崩溃。
      2. 所以,连接池中的连接,如果回收了,不能立即加入到空闲链表,而是放到其他地方(延时回收链表),过个60s才放到里面去
      3. 在niginx里,是一个线程绑定一块内存,解决这个问题(这样就很好了,延时回收很麻烦)
      4. 两种情况:立即回收(accept没有接入)、延迟回收(已经开始有数据收发了)
        1. 立即回收:扔到连接池中
        2. 延迟回收:添加一个延迟回收链表
      5. 连接的延迟回收会不会使得连接不够用:在延迟回收链表中连接较多时,可以稍微超过最大连接数

你可能感兴趣的:(网络编程,linux,c++,架构)