加入收藏 | 设为首页 | 会员中心 | 我要投稿 我爱制作网_沈阳站长网 (https://www.024zz.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 服务器 > 搭建环境 > Linux > 正文

Linux- C语言服务器简单模型(epoll+线程池)

发布时间:2022-09-19 16:12:04 所属栏目:Linux 来源:
导读:  Linux简单服务器模型搭建

  使用 Epoll 的缘由:

  当多个任务到来时需要对其进行及时响应,并将任务下发给特定的处理线程,完成对应的任务。如果只用单线程进行listen轮询监听的话,那效率上实在是
  Linux简单服务器模型搭建
 
  使用 Epoll 的缘由:
 
  当多个任务到来时需要对其进行及时响应,并将任务下发给特定的处理线程,完成对应的任务。如果只用单线程进行listen轮询监听的话,那效率上实在是太低。而我们借助epoll的话就会很完美的解决这个问题(epoll详细太过复杂,而且这里主要目的是使用epoll,所以就不再赘述)。
 
  使用 线程池 的缘由:
 
  当实现高并发服务器时,当然需要实现多?线程并发工作来处理多个客户端的连接。那么方法有如下两种
 
  每当客户端发送一个请求,服务器创建一个线程来处理客户端的连接请求。
 
  先预先创建若干个线程,并让线程先阻塞。每当一个请求到来唤醒一个线程,对其进行处理,处理完毕后继续阻塞。对比:1.是动态创建,当一个任务到达,就需要创建线程,当任务处理完毕后对线程进行销毁。2.而是先预先创建,线程处理完任务后无需销毁,可以继续阻塞等待下一个任务。很明显1的开销会远大于2.从这一点来看需要选择2。但是1也有优点。那就是他可以动态创建,保证每个任务在内核承载能力允许的情况下,可以及时处理。所以怎么样才可以将二者的优点结合呢?那就是在2的基础上再创建一个线程,一直轮询监听任务数,来动态增加或者删减任务数。这样也就是线程池。
 
  思路:一:
 
  首先,我们是利用生产者消费者模型来实现这个服务器模型。那么哪两个模块分别来充当生产者和消费者呢?我们在讨论这个问题之前,我们再简单介绍一下生产者和消费者在本模块的大致功能。
 
  生产者功能:
 
  对客户端的连接请求进行监听。
 
  将客户端的连接请求存储到一个容器中。
 
  当任务不为空时发送不为空信号来通知消费者来接收。
 
  消费者功能:
 
  监听生产者发送的不为空信号。
 
  当收到信号时,从容器取走任务并对其处理。
 
  向生产者发送容器未满信号。(因为任务如果过快的话容器可能已经放满,生产者无法接着接受客户端请求)
 
  那让我们再回到刚才问题,答案就显而易见。
 
  生产者就是epoll模型,消费者就是线程池。
 
  二:
 
  其次我们讨论如何将两个部分实现:
 
  1.线程池(一个结构体)。
 
  既然是线程池,首先需要一个标识符来表明这个线程池是否已经开始使用或者是否已经被关闭。其次我们需要一个记录每个线程的tid的数组,和存放管理线程的id的mtid变量,来方便管理线程对普通i线程的回收。那么就需要以下结构体成员:
 
  int thread_shutdown;//1为关闭线程池,0为打开线程池
  pthread_t *tids;//tid数组
  pthread_t mtid;//管理者线程的id
  int thread_max;//最大的线程数
  int thread_min;//最小的线程数(销毁时需要)
  int thread_alive;//存活的线程数
  int?thread_busy;//正在处理任务的线程数
  生产者需要将任务存放到一个容器中,如果我们将容器单独考虑,那么就会很麻烦(如何对其进行加锁,函数传参等问题)。所以不如直接将它扔到这个结构体中(我这里使用的是循环队列,当然其他的容器也可以)。既然我们是多线程访问这个临界资源,哪么也少不了一把锁了。所以还需要以下结构体成员:
 
  task_t *task_queue;
  int queue_cur;//当前存放的任务数
  int queue_max;//队列允许存放的最大容器数
  int queue_front;//队列头部指针(类似于栈顶)
  int queue_rear;//队列的尾部指针
 
 
  pthread_mutex_t?plock;//锁
  我们在上文中提到生产者需要将来任务了告诉消费者,而且消费者需要阻塞等待这个信号。那么我们该如何实现呢?,这时候我们使用条件变量就可以很好地满足。因为条件变量一方面可以让消费者阻塞。二来可以让生产者通过pthread_cond_signal()函数来通知消费者停止挂起(**小心惊群问题!!!**后文会详细解释这个问题的避免)。所以我们就需要两个条件变量来实现:1.生产者告诉消费者容器有任务了可以去处理了。2.消费者告诉生产者容器没有满可以继续放任务了。
 
  pthread_cond_t pnot_full;//消费者告诉生产者容器没有满可以继续放任务了
  pthread_cond_t?pnot_empty;//生产者告诉消费者可以去处理了
  最后因为管理者线程需要普通线程进行销毁工作(只有少量的线程在工作,而大部分都被挂起),那我们只需要设置一个 int变量表明需要销毁的线程数
 
  int?thread_exitcode;
  OK 线程池结构体封装完事了。我们接着实现下一个。任务
 
  这里的任务就是上面的task_t,也是一个结构体。
 
  因为我们需要将这个模型实现普及化,那么任务必然可以改变的。所以我们就可以在这个结构体封装两个成员即可,一个函数指针来表示对应的任务。还有一个void*指针来存放该函数需要的参数。
 
  typedef struct{
  void * (*job)(void*);
  void *arg;
  }?task_t;
  OK全部实现完毕。可能有读者会问一句生产者呢?熟悉epoll的朋友都知道,epoll主要就只有一个epfd文件描述符来标明红黑树的根。我们在主函数声明一个即可(要是c++实现的话就可以将其封装成一个类的成员变量)。
 
  typedef struct {
    void * (*job)(void *);
    void *arg;
  }task_t;
 
 
  typedef struct {
    int thread_shutdown;
    int thread_max;
    int thread_min;//初始化時的線程數
    int thread_alive;
    int thread_busy;
    int thread_exitcode;
 
 
    task_t *task_queue;
    int queue_max;
    int queue_cur;//當前正在等待被除離的任務數
    int queue_front;
    int queue_rear;
 
 
    pthread_t *tids;
    pthread_t mtid;
 
 
    pthread_mutex_t plock;
    pthread_cond_t pnot_full;
    pthread_cond_t pnot_empty;
  }pool_t;
  三:
 
  既然基本结构都已经封装好,那么剩下的就是主逻辑的实现(因为代码篇幅较长,就不再一一赘述,博主在这里大致叙述一下,每个函数的主逻辑,以方便大家理解代码):
 
  注意一下序号:
 
  1.main函数:
 
  1.0. 对pool的网络端进行初始化(thread_pool_netinit)
 
  1.0.1. thread_pool_netinit:这里就是基本的socket通信的server端,将初始化好的serverfd返回给主函数
 
  1.1. 对task进行初始化线程池linux,对于task的函数博主这里就是一个简单的小写字母转大写(thread_user_job),参数为一个serverfd。
 
  1.1.1. thread_user_job:由于epoll已经监听到客户端的连接请求,那么只需要对serverfd进行监听即可,剩余的就是处理主逻辑(博主的是一个小写转大写逻辑)
 
  1.2 . 其次对pool进行初始化(thread_pool_init)
 
  1.2.1. thread_pool_init:这里主要是为线程池分配空间(比如:pool结构体,tids数组,task_queue循环队列进行malloc分配空间)将各个成员初始化(thread_shutdown这个成员一定要初始化为0,表述打开状态),最后按照参数建立指定个数的消费者线程(thread_customer_job),以及一个管理者线程(thread_manger_job)。最后结构体的地址返回给main函数。
 
  1.2.1.1. thread_customer_job:首先判断线程池是否打开,然后先获得锁(因为条件变量本身也是临界资源)然后pthread_cond_wait(pnot_empty,&plock)。阻塞等待pnot_empty条件变量的到来。注意:上面的惊群问题将在这里进行描述。因为pthread_cond_waitAPI手册中描述的是,将会对若干个(注意这个若干个)阻塞等待该条件变量的线程进行唤醒。所以每当一个条件变量到达,原理上只需要一个线程即可,而这里会唤醒若干个线程,所以会导致惊群问题,那么我们这里可以加一个while循环在此判断数据thread_cur(当前任务数是否为空),只有第一个获得信号量的会跳出while循环然后对数据thread_cur–避免其他的线程跳出循环,(但博主认为,在跳出while循环和执行cur–期间也可能会有其他线程跳出循环,这里留个坑先)。处理完逻辑是注意将线程数和循环队列对应的数据进行更新即可。最后会继续阻塞等待该条件变量。
 
  1.2.1.2. :thread_manger_job:先判断线程池是否打开。抢占锁并对数据进行更新保留。再判断逻辑结构(博主这里是通过存活线程数thread_alive,当前任务数thread_cur和忙碌线程数,thread_busy,进行简单数学比例运算来判断是否销毁线程)。
 
  1.3. :对epoll进行初始化(thread_epoll_init)
 
  1.3.1. :thread_epoll_init 主要就是epoll_create 以及将serverfd通过epoll_ctl放到红黑树上进行监听。最后将epfd返回(值得注意的是,需要将epoll改成ET模式 因为我添加任务后一定会解决,不必阻塞等待解决)
 
  1.4. :epoll开始监听(thread_epll_start)
 
  1.4.1. :thread_epoll_start:首先判断线程池是否打开,然后epoll_wait阻塞监听serverfd,如果返回值>0那么有连接请求(thread_epoll_addtask)。
 
  1.4.1.1. :thread_epoll_addtask。首先判断线程池是否关闭,再判断容器是否已经满了,没满的话添加任务到队列中。满了的话pthread_cond_wait阻塞等待 pnot_full。
 
  1.5.: 销毁掉那个全局锁
 
  代码链接:
 
  注意点一下master分支
 
  推荐:
 
  C/C++Linux服务器开发/高级架构师 学习资料、视频教程
 

(编辑:我爱制作网_沈阳站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!