整个客户端到服务器的线程池使用概念模型,大体可以分为四部分,创建链接- 任务分配- 线程处理且归还子线程-进入下一个任务周期。(可以从这几个方面去看,毕竟整体问题是在服务器和客户端之间发生的事件而且服务器大部分都是一直在运行的,线程的销毁还是需要再具体情况具体分析,这次先写前三条的一些内容,其他的还在查看资料学习。)
1、建立链接接收任务(主线程进行)
2、资源分配(线程的资源分配,回收)
3、使用完将线程还回线程池(感觉线程池,说是池化,只是需要其他组件一起实现,核心还是创建多线程,进行任务匹配,处理,然后线程空余出来等待下一波工作的开始)
4、线程使用的过程中也需要注意用户态和内核态数据的复制(文件描述符的复制,数据的读写等)
5、单纯的使用数据的复制(非内核态),如果数据过大或者申请空间频率较高的时候,效率明显不行,而且也浪费很多空间。
6、上下文的切换(简单说就是CPU处理任务的过程中,寄存器状态的切换)
在使用线程池的时候,一开始考虑到两个问题(任务的产生和处理:当时因为在写服务器,也有考虑线程分配的问题,不管我们用的是select去监听不同的链接,还是epoll,甚至自己写任务队列去管理线程和任务分配,都不能理解客户任务请求,是怎么和线程匹配上的,或者有人考虑过随机的去匹配,但是当别的线程被占用的时候,我又怎么去找到空闲的线程去和任务匹配呢?)
目前先看下select的内容,其他的有准备在记录了。
创建链接我们都知道步骤,但是针对多链接方面,需要使用到IO多路复用机制去监听链接。
至于什么是IO多路复用呢:(在网上搜索一直没有找到相应的材料,最简单的介绍就是:使用单线程/单进程去监听若干个文件描述符是否有可以执行IO操作的能力)。我自己的感觉来说,这个说法,对IO多路复用的解释有点差强人意。
另外找到的解释感觉比较合适,是复用的线程:让少量的线程能够处理多个IO链接(或者说,让同一个线程能够多次复用去处理IO链接,这样就可以使用少量的线程,处理多个链接任务;而不需要每一个任务都调用一次pthread_create 创建一次线程了)
所以说,这个技术可以去辅助我们实现对线程的复用和任务的匹配(这个函数并不创建线程,只监听IO链接)
线程和任务的匹配,更多的应该会使用负载均衡的技术,这个最近在研究了。大量的高并发系统中如果没有好的匹配机制,难以想象怎么实现高效的任务处理和资源调用。
这个函数的具体调用就不写了。
#include<sys/select.h>
int select(int nfds, fd_set*readfds, fd_set*writefds, fd_set*exceptfds, struct timeval*timeout);
当这个函数调用返回时,内核会修改监听集合来通知程序有那些文件描述符已经就绪
(select使用的轮询机制,且监听的文件描述符最大有1024个,暂时这样认为吧),且这个函数全部都是传入参数
这个参考书籍上的内容:表示有哪些情况认为文件描述符可读,可写,或者异常(因为是网络编程使用这个技术,所以文件描述符,也是从socket和accept来的。
因为accept成功时,返回一个新的连接socket的fd,该socketfd 唯一地标识了这个被接受的连接,服务器可通过读写该socket来与被接受连接的客户端通信
至于怎么判断链接可以读,可以写了呢。这个我们暂时是没法实现的,只能是依靠内核了。
每个socket在创建的时候,都会自动创建相应的读和写缓冲区,对应socket的读写操作。
这样,我们使用recv,或者read去从已连接的就绪 connfd 文件描述符 中读取读取信息时,当此就绪sockfd的读缓冲区的字节数,达到最低读取水平时,就可以读取到了;达不到就是无法读取,recv返回值就是0,或者错误的话就是 -1
if(FD_ISSET(connfd,&read_fds)) { ret=recv(connfd,buf,sizeof(buf)-1,0); if(ret<=0) { break; } printf("get%d bytes of normal data:%s\n",ret,buf); }
大方向上的资源分配就是处理任务的线程分配了。我们接收到不同的链接请求,后续接收到不同的客户端任务需要分配到子线程来处理任务请求。(关于用多线程策略的原因网上有很多,注意分别就好)
主进程/主线程,需要通过某种方式选择线程池中的一个子线程来为之服务。而这个方式,目前可以分为两种:
1、使用算法来选择子线程:轮询,或者随机算法。(这个的话,算法设计目前还没了解,有时间会多研究研究)
(本来负载均衡的话题可能是针对分布式服务器开发的任务分配概念,但是当我们进行单服务器多客户端的时候,需要做的就是将任务分配给空闲的线程,也较为类似)
2、或者使用任务队列(任务队列简单来看,只负责存储链接任务就行)
大体就是将链接来的netfd加入到队列中,处理的时候挨个提取就好,也可能需要设计个标记值,表示队列的空满啥的。
3、创建线程池有一个点,就是我们需要一个方式去管理创建的多个线程,pthread_t * tid 使用malloc/calloc给它分配相应数量的空间就可以(一个pthread_t * 类型的数组),每一个地址空间都是pthread_t* 类型的,正好可以用来存储线程的id这样就可以创建多线程了。
typeedf threadPool{ pthread_t *tid ; //线程的数组 int maxNums ; //线程的数量 //... }; //申请maxNums个pthread_t * 类型的空间 pthread_t * tid = (pthread_t *)malloc(sizeof(maxNums)); //创建线程 pthread_create(tid[i], NULL, func, arg);
之前好久没写c代码,忽略了这个点。这个数组在调用的时候就可以给我们一些反馈标识,比如每占用一个线程,我们就将计数值加1,都是可以的。
当我们创建线程后,就会自动调用子线程的函数入口,子线程进入子线程的执行过程中,主线程仍然继续执行去获取客户端链接(任务)。这个过程里面,我们需要先对子线程加锁(因为子线程的操作会对任务队列产生影响,使用任务队列时,任务队列是一个共享资源,主线程和其他线程都可以访问,所以需要加锁去保护资源占用它)。
通常我们会使用互斥锁的相关的函数(pthread_mutex_lock/unlock,pthread_cond_wait,pthread_cond_signal)。这里面当使用pthread_cond_wait 时,会解锁,然后等到子线程被唤醒的时候又会自动加锁。
至于代码的话,网上有很多,可以直接查C语言任务队列实现线程池。后续也会补上
至于复用呢,等子线程运行完,是不是也就代表这这个任务结束了(如果简单些说,是不是就相当于和客户端断开了链接,变成了一个空闲线程,又可以使用了)
一个线程再高效,本身代码运行的顺序也是固定的,所以子线程的函数终归是要运行完,才能进行下一次运行。这样就可以达到一个线程,循环执行处理多个IO链接的的目的了。
也可以考虑下,类似的 :服务器只有一个线程,且一次只能创建一次链接的时候,当执行完客户端的请求,客户端断开链接(服务器断开链接后,这个服务器线程是不是就可以接收其他链接了)
暂时先到这里了,学习笔记和感受仅作参考!!!。