线程并发会导致线程创建过多,浪费过多的系统资源,为了解决线程创建过多,我们引入了池的概念。
一、线程池概念
【1.原理:】
在程序启动之初,就创建出多个(3~10个)线程,由系统或管理员创建。当一个客户端连接后,就在池中调配一个线程为此客户端服务。
【 2. 和多线程的对比:】
- 不会存在多线程的创建和销毁,它是启动前创建,程序退出销毁。
- 有限线程,由客户端控制,速度快。客户端来了它就给分配
二、代码实现思想:
- 大体框架还是socket(),bind(),listen(),while(1){accept(),recv(),send()}
- 在程序开始时 创建5个线程,通过pthread_create()创建。
- 定义一个数组保存文件描述符,所以需要提供方法:初始化(数组元素初始化为-1),插入,获取(搜索一个有效的文件描述符并返回,并且将数组保存置为-1)。
- 这个数组是所有线程共享的,所以我们可以想到一个问题:如果主线程接收多个客户端太快,并且多个客户端同时获取了一个文件描述符,那么就等于多个客户端服务于一个描述符,这个肯定是不对的,所以我们需要采取同步机制来控制它。让插入和获取函数实现互斥,同时只能有一个线程访问。所以我们使用 互斥量pthread_mutex_t mutex,在进入插入函数时加锁pthread_mutex_lock(&mutex),退出时取锁pthread_mutex_unlock(&mutex),获取函数同理。
- 创建时必须指定线程的入口地址,一旦创建成功,线程就自行启动运行,线程必须阻塞在某一个条件,等待主线程接收到客户端连接时唤醒。
- 存在线程唤起和阻塞,所以要引入同步机制,我们使用信号量PV操作来实现。初始为0没有客户端连接,先用P(sem_wait(&sem))把它阻塞,收到客户端连接,V(sem_post(&sem))操作唤醒。最开始为0,阻塞着,当主线程收到连接时,V操作将信号量+1,P操作一次信号量-1,会进行一次处理,其他继续阻塞。
- 将主线程获取到的客户端连接文件描述符传递给函数线程(线程资源共享,所以定义全局变量就可以了,因为是多个线程,所以我们定义一个全局数组来作为主线程到函数线程文件描述符传递的机制)
- 主线程执行accept等待客户端链接,如果有链接,则接收连接后,将此连接传递给函数线程,唤醒一个池中的线程来处理此连接。
所以主要流程就是:和客户端建立连接,V操作唤醒线程,从数组中取文件描述符,再去处理它。所有客户端处理完结束所有线程,主线程结束则所有线程结束。
我们画个图理解一下:
三、代码流程图理解
我们为了理清思路,画出代码流程图:
四、代码
我们写出代码,客户端代码和以前一样
threadPoll.c
# include<stdio.h>
# include<stdlib.h>
# include<string.h>
# include<unistd.h>
# include<assert.h>
# include<sys/types.h>
# include<pthread.h>
# include<sys/socket.h>
# include<arpa/inet.h>
# include<netinet/in.h>
# include<signal.h>
# include<semaphore.h>
#define THREAD_NUM 5//线程池的线程数量
#define BUFFSIZE 10 //文件描述符数量
sem_t sem;//信号量
pthread_mutex_t mutex; //互斥锁
int Buff_Fd[BUFFSIZE]; //线程共享。
//针对文件描述符的操作
//初始化
void InitBuffFd()
{
int i=0;
for(;i<BUFFSIZE;i++)
{
Buff_Fd[i]=-1;
}
}
//插入,防止在插入时有另一个线程获取,所以加上互斥量
void InsertBuffFd(int fd)
{
pthread_mutex_lock(&mutex);//加锁
int i=0;
for(;i<BUFFSIZE;++i)
{
if(Buff_Fd[i]==-1)
{
Buff_Fd[i]=fd;
break;
}
}
pthread_mutex_unlock(&mutex);//去锁
}
//获得文件描述符
int GetBuffFd()
{
pthread_mutex_lock(&mutex);
int i=0,fd=-1;
for(;i<BUFFSIZE;++i)
{
if(Buff_Fd[i]!=-1)
{
fd=Buff_Fd[i];
Buff_Fd[i]=-1;
break;
}
}
pthread_mutex_unlock(&mutex);
return fd;
}
int InitSocket()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1) return -1;
struct sockaddr_in ser;
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));
if(res==-1) return -1;
res=listen(sockfd,5);
if(res==-1) return -1;
return sockfd;
}
//线程工作
void* work_thread(void* arg)
{
while(1)
{
//P操作阻塞它
sem_wait(&sem);
//获取文件描述符
int fd=GetBuffFd();
if(fd==-1) continue;
//完成数据的收发
while(1)
{
char buff[128]={0};
int n=recv(fd,buff,127,0);
if(n<=0)
{
printf("one client over\n");
close(fd);
break;
}
printf("%d:%s\n",fd,buff);
int res=send(fd,"OK",2,0);
if(res<=0)
{
printf("send error\n");
close(fd);
break;
}
}
}
}
int main()
{
//初始化信号量
sem_init(&sem,0,0);
//初始化互斥锁
pthread_mutex_init(&mutex,NULL);
//初始化描述符数组
InitBuffFd();
int sockfd=InitSocket();
//创建线程池
pthread_t id[THREAD_NUM];
int i=0;
for(;i<THREAD_NUM;++i)
{
int res=pthread_create(&id[i],NULL,work_thread,NULL);
assert(res==0);
}
while(1)
{
struct sockaddr_in cli;
socklen_t len=sizeof(cli);
int c=accept(sockfd,(struct sockaddr*)&cli,&len);
if(c<0)
{
continue;
}
printf("%d连接\n",c);
//把C插入数组中
InsertBuffFd(c);
//启动一个线程去处理它,V操作
sem_post(&sem);
}
close(sockfd);
exit(0);
}
五、演示
运行服务端和客户端:
我们可以明显的看到成功了,但也看到了它的缺陷,我们在代码中设置的线程数目是5,这就意味着它最多处理5个客户端端,多了就不行了,所以出现了第6次连接连接成功,但数据不能处理。我们试着关闭一个:
所以线程池的缺陷是:每次只能处理数组大小的客户端,解决这个问题可以引入I/O复用,可以监听多个客户端,同时处理数组大小的就绪事件。
加油哦!✍。