编程语言、语法这种东西如果你不会,可以通过学习来解决,但是这种 解决问题的思路, 是一种只可意会难以言传的东西,却恰恰能够决定你在开发道路上走多远的东西,搞程序开发一定要培养自己非常清晰的逻辑思维,不然,这条程序开发之路你会走的特别艰辛;
多线程多进程的提出在linux系统线程专题给出历史演进过程,可以参考Linux内核进程线程掌握底层原理 。基于现有接口多线程编程在C/C++语言专题链接并发与多进程多线程 - C++多线程编程(一)_生活需要深度的博客-CSDN博客中包含。
1. 多线程的提出
我们这里用 “线程” 来解决客户端发送过来的 数据包。一个进程 跑起来之后缺省 就自动启动了一个 “主线程”,也就是我们一个worker进程一启动就等于只有一个“主线程”在跑;我们现在涉及到了业务逻辑层面,这个就要用多线程处理,所谓业务逻辑:充值,抽卡,战斗;
充值,需要本服务器和专门的充值服务器通讯,一般需要数秒到数十秒的通讯时间。此时,我们必须采用多线程【100个多线程】处理方式; 一个线程因为充值被卡住,还有其他线程可以提供给其他玩家及时的服务;
所以,我们服务器端处理用户需求【用户逻辑/业务】的时候一般都会启动几十甚至上百个线程来处理,以保证用户的需求能够得到及时处理;
epoll, iocp(windows),启动线程数cpu*2+2;
主线程 往消息队列中用inMsgRecvQueue()扔完整包(用户需求),那么一堆线程要从这个消息对列中取走这个包,所在必须要用互斥;
互斥技术在《c++从入门到精通 c++98/11/14/17》的并发与多线程一章详细介绍过;
多线程名词
a)POSIX:表示可移植操作系统接口(Portable Operating System Interface of UNIX)。
b)POSIX线程:是POSIX的线程标准【大概在1995年左右标准化的】;它定义了创建和操纵线程的一套API(Application Programming Interface:应用程序编程接口),
说白了 定义了一堆我们可以调用的函数,一般是以pthread_开头,比较成熟,比较好用;我们就用这个线程标准;
2. 线程池实战代码
线程池的详细说明在数据结构章节链接给出,详细内容可以查看这里完成整体内容的学习。
2.1 为什么引入线程池
我们完全不推荐用单线程的方式解决逻辑业务问题,我们推荐多线程开发方式;
线程池:说白了 就是 我们提前创建好一堆线程,并搞一个雷来统一管理和调度这一堆线程【这一堆线程我们就叫做线程池】,
当来了一个任务【来了一个消息】的时候,我从这一堆线程中找一个空闲的线程去做这个任务【去干活/去处理这个消息】,
活干完之后,我这个线程里边有一个循环语句,我可以循环回来等待新任务,再有新任务的时候再去执行新的任务;
就好像这个线程可以回收再利用 一样;
线程池存在意义和价值;
a)实现创建好一堆线程,避免动态创建线程来执行任务,提高了程序的稳定性;有效的规避程序运行之中创建线程有可能失败的风险;
b)提高程序运行效率:线程池中的线程,反复循环再利用;
大家有兴趣,可以百度 线程池; 但是说到根上,用线程池的目的无非就两条:提高稳定性,提升整个程序运行效率,容易管理【使编码更清晰简单】
【pthread多线程库】 gcc 末尾要增加 -lpthread;
$(CC) - o $@ $^ -lpthread
CThreadPool【线程池管理类】
讲解了 Create(),ThreadFunc(),StopAll();
2.3:线程池的使用
(4.1)线程池的初始化 :Create();
(4.2)线程池工作的激发,所谓激发,就是让线程池开始干活了;
激发的时机:当我收到了一个完整的用户来的消息的时候,我就要激发这个线程池来获取消息开始工作;
那我激发代码放在哪里呢?
(4.3)线程池完善和测试
a)我只开一个线程【线程数量过少,线程池中只有一个线程】,我们需要报告;
b)来多个消息会堆积,但是不会丢消息,消息会逐条处理;
c)开两个线程,执行正常,每个线程,都得到了一个消息并且处理;表面看起来,正常;
程序执行流程【可能不太全,后续讲到哪里缺了再补充不着急】
(i)ngx_master_process_cycle() //创建子进程等一系列动作
(i) ngx_setproctitle() //设置进程标题
(i) ngx_start_worker_processes() //创建worker子进程
(i) for (i = 0; i < threadnums; i++) //master进程在走这个循环,来创建若干个子进程
(i) ngx_spawn_process(i,"worker process");
(i) pid = fork(); //分叉,从原来的一个master进程(一个叉),分成两个叉(原有的master进程,以及一个新fork()出来的worker进程
(i) //只有子进程这个分叉才会执行ngx_worker_process_cycle()
(i) ngx_worker_process_cycle(inum,pprocname); //子进程分叉
(i) ngx_worker_process_init();
(i) sigemptyset(&set);
(i) sigprocmask(SIG_SETMASK, &set, NULL); //允许接收所有信号
(i) g_threadpool.Create(tmpthreadnums); //创建线程池中线程
(i) g_socket.ngx_epoll_init(); //初始化epoll相关内容,同时 往监听socket上增加监听事件,从而开始让监听端口履行其职责
(i) m_epollhandle = epoll_create(m_worker_connections);
(i) ngx_epoll_add_event((*pos)->fd....);
(i) epoll_ctl(m_epollhandle,eventtype,fd,&ev);
(i) ngx_setproctitle(pprocname); //重新为子进程设置标题为worker process
(i) for ( ;; ) {}. .... //子进程开始在这里不断的死循环
(i) sigemptyset(&set);
(i) for ( ;; ) {}. //父进程[master进程]会一直在这里循环
3. pthread_cond_wait与mutex
3.1 pthread_cond_wait函数的原理
pthread_cond_wait()
用于阻塞当前线程,等待别的线程使用pthread_cond_signal()
或pthread_cond_broadcast
来唤醒它pthread_cond_wait()
必须与pthread_mutex
配套使用。pthread_cond_wait()
函数一进入wait
状态就会自动release mutex
。当其他线程通过pthread_cond_signal()
或pthread_cond_broadcast
,把该线程唤醒,使pthread_cond_wait()
通过(返回)时,该线程又自动获得该mutex
。pthread_cond_signal
函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行.如果没有线程处在阻塞等待状态,pthread_cond_signal
也会成功返回。
使用pthread_cond_signal
一般不会有“惊群现象”产生,他最多只给一个线程发信号。假如有多个线程正在阻塞等待着这个条件变量的话,那么是根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。但无论如何一个pthread_cond_signal
调用最多发信一次。- 但是
pthread_cond_signal
在多处理器上可能同时唤醒多个线程,当你只能让一个线程处理某个任务时,其它被唤醒的线程就需要继续wait
,而且规范要求pthread_cond_signal
至少唤醒一个pthread_cond_wait
上的线程,其实有些实现为了简单在单处理器上也会唤醒多个线程. 另外,某些应用,如线程池,pthread_cond_broadcast
唤醒全部线程,但我们通常只需要一部分线程去做执行任务,所以其它的线程需要继续wait.所以强烈推荐对pthread_cond_wait()
使用while
循环来做条件判断.
一:线程池代码调整及补充说明
支撑线程池的运作主要靠两个函数:pthread_cond_signal(&m_pthreadCond); 触发
pthread_cond_wait(&m_pthreadCond, &m_pthreadMutex); 等待
《Unix环境高级编程》 11章 线程,11.6.6: 条件变量:m_pthreadCond:
条件变量,是线程可用的另一种同步机制,条件变量给多个线程提供了一个会合的场所,条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生;
a)条件本身【while ( (pThreadPoolObj->m_MsgRecvQueue.size() == 0) && m_shutdown == false)】 是由互斥量保护的。
线程在改变条件状态之前必须首先锁住互斥量,其他线程在获取到互斥量之前不会觉察到这种改变,因为互斥量必须在锁定以后才能计算条件;
c++11,也有条件变量的说法 my_cond.wait(....), my_cond.notify_one(...)
大家如果有兴趣可以用c++11多线程开发技术实现 自己的跨平台的线程池代码;
b)传递给pthread_cond_wait的互斥量m_pthreadMutex对条件【 while ( (pThreadPoolObj->m_MsgRecvQueue.size() == 0) && m_shutdown == false)】进行保护的,
调用者把锁住的互斥量传递给函数pthread_cond_wait,函数 然后自动把调用线程 放在 等待条件的 线程列表 上,对互斥量解锁,
这就关闭了 条件检查 和 线程进入休眠状态等待条件改变 这两个操作之间的时间通道,
这样线程就不会错过条件的任何变化。pthread_cond_wait返回时,互斥量再次被锁定;
二:线程池实现具体业务之准备代码
(2.1)一个简单的crc32校验算法介绍
CCRC32类:主要目的是对收发的数据包进行一个简单的校验,以确保数据包中的内容没有被篡改过;
Get_CRC():给你一段buffer,也就是一段内存,然后给你这段内存长度,该函数计算出一个数字来(CRC32值)返回来;
(2.2)引入新的CSocket子类
真正项目中要把CSocekt类当成父类使用,具体业务逻辑代码应该放在CSocket的子类中;
threadRecvProcFunc()收到消息之后的处理函数;
(2.3)设计模式题外话
有很多人善于,乐于:抽象;把一个一个小功能封装成一个一个类;往设计模式上套 进行所谓的面向对象程序设计;
最能体现面向对象的 多态【虚函数】;
写程序:每个人有每个人的喜好;老师最喜欢的就是简单粗暴有效的程序设计方式,完全不喜欢动不动就封装一个类的这种写法;
a)类太多,别人理解起来就非常困难,另外类太多,对程序效率影响很大;
b)几十万,上百万上代码,里边很多部件需要灵活调整,经常变动,不稳定的部分,才需要抽象出来,用虚函数,通过设计模式来灵活解决;
不要乱用设计模式,不要乱封装;
(2.4)消息的具体设计
为了能够根据客户端发送过来的消息代码 迅速定位到要执行的函数,我们就把客户端发送过来的 消息代码直接当做 一个数组的下标来用;
最终认识:咱们的服务器开发工作【业务逻辑】,主要集中在三个文件中:ngx_logiccomm.h,ngx_c_slogic.cxx,ngx_c_slogic.h
三:threadRecvProcFunc()函数讲解
四:整体测试工作的开展
服务器开发工作,公司 配备专门客户端开发人员来开发客户端工作;
c/s配合工作,配合指定通讯协议;协议的制定一般是 服务器程序员来主导;
(1)确定通讯格式是 包头+包体,包头固定多少个字节,这种规则是服务器端 来制定并在开发一个项目之前,要明确的 和客户端交代好;
要求客户端给服务器发送数据包时严格遵循这种格式;
(2)注册,登录,都属于具体的业务逻辑 命令;这种命令一般都是由服务器牵头来制定;
(4.1)不做背锅侠
服务器开发难度往往比客户端大很多,责任也重很多;要求也高得多;
讲清楚:服务器端要负责通讯协议的制定工作,以免跟客户端推诿扯皮
服务器有能力站在客户端的角度去制定各种通讯协议;
商量,共同指定协议和数据结构;共同制定协议;
(4.2)客户端测试代码的调整
服务器端有责任把crc32算法给到客户端;