在Linux的下,当我们启动HTTP服务后,其实系统就为我们提供了一个线程池,我们的请求服务也是由线程池里面的线程来处理的。
我们先来看一下HTTP启动后我的系统给我分配的线程:
查看httpq启动后所有的进程使用的命令:ps -ef | grep httpd
可以看到由根创建出了八个线程为我们服务,这是http启动后自己创建的,那么我们要如何来模拟设计线程池呢,我们继续往下看。
设计思想
在考虑进程池与线程池的设计思想之前,我们先来看一下多进程与多线程的的工作模式:在服务器程序启动时,创建出多个进程或者线程,将其维护在池中,当有客户连接时,系统为这个用户提供进程或者线程,为客户端服务,当与客户端完成交互后,创建的进程或者线程也就随之被释放,我们的设计思想也由此而来,具体一点就是就是主线程或父进程负责与客户端的连接,函数线程或子进程负责与特定的客户端交互。
进程池与线程池的选择
参照另一篇博文:https://blog.csdn.net/pretysunshine/article/details/83475370
线程池的实现
线程池的实现主要由四个难点:
1.主线程需要将文件描述符传递给函数线程
2.函数线程启动必须阻塞在获取文件名描述符之前
3.信号量来控制主线程向函数线程通知获取文件描述符事件
4.主线程在数组中插入数据以及函数线程获取数组中的数据都是互斥的
简单粗暴,先看代码,我们代码里说
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<pthread.h>
#include<sys/socket.h>
#include<semaphore.h>
pthread_mutex_t mutex;//保证给数组插入数据和获取数据互斥
sem_t sem;
//全局变量 线程通讯
int clilink[10]; //全局变量,为客户端套接字维护一个数组
void initclilink()
{
int i=0;
for(;i<10;i++)
{
clilink[i]=-1;
}
}
int insert(int c)
{
pthread_mutex_lock(&mutex);
int i=0;
for(;i<10;i++)
{
if(clilink[i]==-1)
{
clilink[i]=c;
break;
}
}
pthread_mutex_unlock(&mutex);
if(i>=10)
return -1;
return 0;
}
int getcli()
{
pthread_mutex_lock(&mutex);
int i=0;
int c=0;
for(;i<10;i++)
{
c=clilink[i];
clilink[i]=-1;
break;
}
pthread_mutex_unlock(&mutex);
if(i>=10)
return -1;
return c;
}
void * pthread_fun(void *arg)
{
while(1)
{//对信号量的P,保证线程阻塞于获取文件描述符之前
sem_wait(&sem);
int c=getcli();
while(1)
{
char buff[128]={0};
int n=recv(c,buff,127,0);
if(n<=0)
{
close(c);
break;
}
printf("%d:%s\n",c,buff);
}
}
}
int main()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
assert(sockfd!=-1);
struct sockaddr_in ser,cli;
memset(&ser,0,sizeof(ser));
ser.sin_family=AF_INET;
ser.sin_port=htons(6000);
ser.sin_addr.s_addr=inet_addr("127.0.0.1");
int res=bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));
assert(res!=-1);
listen(sockfd,5);
int i=0;
for(;i<3;i++)//创建出三个线程,组成线程池
{
pthread_t id;
res=pthread_create(&id,NULL,pthread_fun,NULL);
}
initclilink();//客户端套接字数组初始值全为-1
sem_init(&sem,0,0);
while(1)
{
int len=sizeof(cli);
int c=accept(sockfd,(struct sockaddr*)&cli,&len);
if(c<0)
{
continue;
}
if(insert(c)==-1)//将接收到的客户端套接字存储与于数组中
{
close(c);
continue;
}
//对信号量的v操作
sem_post(&sem);//将阻塞线程转台改变开始处理数据
}
close(sockfd);
}
果然我这个人还是不会写注释,也不知道能不能看懂,再简单说下吧。
对于我们提到的第一个问题,我们为客户端套接子维护一个数组由于多线程的数据空间共享性,我们就可以将文件描述符(客户端套接字)传递给函数线程。
第二个问题,我们在void * pthread_fun(void * arg)函数中一开始就调用sem_wait()函数,直到客户端套接字存储于为它维护的数组中后,我们再进行sem_post(),再调用函数获取客户端套接字处理事件,这也就解决了第三个问题。
对于第四个问题,我们采用最简单粗暴的加锁就可以解决互斥问题。
进程池的实现原理上和线程是一样的,但是在细节上还是有一些区别,主要难点有以下几点:
1.进程启动时,创建多个进程,将进程池维护在子进程中
2.进程池中的进程必须阻塞在获取到客户端套接字之前
3.主进程负责接受客户连接,并将获取到的客户连接套接字传递给进程池中的进程。
4.进程之间的通讯。
具体的实现就不上代码了。