文章目录
- 多进程多线程
- **进程池或者线程池:**
- 问题
多进程多线程
多进程或者多线程下的服务器/客户端交互:有客户链接时,系统为这个客户创建出进程或者线程,当与客户端交互完成时,创建进程或线程也就随之释放。
应用场景:
进程之间时相互独立的,不存在数据安全。
进程相对于线程而言,创建时,开辟的资源多,CPU调度时比较慢。
如果多进程要通讯,必须要借助特定的手段(信号,信号量,共享内存,管道,消息队列)
Linux上进程能创建的数量相比于系统进程数量小的多,300多线程。
进程池或者线程池:
池:提前申请的大量资源存放的一个单位
内存池: 使用内存之前,先申请大量的内存,当后续需要使用时,直接从内存池中分配,不需要通过系统分配。
1.设计思想:在服务器程序启动时,创建出多个进程或多线程,将其维护在池中。当有客户链接时,就从池中分配进程或者线程为客户端服务。
系统自身带的Web服务器 ,进程池如下:
线程池如下:
选用进程池/线程池:根据应用场景来定,没有最好,只有最适合。
- 进程间相互独立性,进程池更安全,但是需要借助特定手段,进程切换慢,进程占据操作系统资源大。
- 而线程由于共享资源大,需要线程安全机制控制,就比较麻烦,但是数据共享容易,不需要借助特定手段,切换的时候也是轻量级切换,切换方便,线程占据操作系统资源少。
2.主线程(父进程)/函数线程(子进程)
主线程(父进程):主要负责与客户端连接
函数线程(子进程):与特定客户端交互
进程池实现关键:
1.在程序启动时,创建多个进程,将子进程维护在进程池中
2.进程池中的进程必须阻塞在获取客户端文件描述符前
3.主进程负责接受客户连接,并将获取到的客户连接文件描述符传递给进程池中的进程,就必须借助于进程间通讯,不能仅仅传递值,要传递的是文件描述符,就必须是利用UNIX域socket在进程间传递特殊的辅助数据。
线程池实现关键:
- 主线程需要将文件描述符传递给函数线程。
- 函数线程获取文件描述符的时间控制(函数线程启动后需要阻塞在获取文件描述符之前)
- 使用信号量来控制主线程向函数线程通知文件描述符事件。
- 主线程在数组中插入文件描述符,以及函数线程获取文件描述符时都必须是互斥的。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<pthread.h>
#include<semaphore.h>
sem_t sem;
pthread_mutex_t mutex;
void* pthread_fun(void* arg);
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=-1;
for(;i<10;i++)
{
if(clilink[i]!=-1)
{
c=clilink[i];
clilink[i]=-1;
break;
}
}
pthread_mutex_unlock(&mutex);
if(i>=10)
{
return -1;
}
return c;
}
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();
sem_init(&sem,0,0);
pthread_mutex_init(&mutex,NULL);
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);
}
}
void *pthread_fun(void *arg)
{
while(1)
{
sem_wait(&sem);
//阻塞 信号量P操作
int c=GetCLi();
printf("accept(%d)\n",c);
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);
send(c,"ok",2,0);
}
}
}
问题
一、代码中的全局数组提取与插入时都是从0开始遍历,就可能存在先来的客户端却后服务的情况。
所以:
上述问题及代码均是模型一的实现
模型一:主线程将文件描述符添加到数组,只要线程空闲就可以去处理,但是可能存在客户端来得慢,只有一个线程去服务,其他线程都空闲。
模型二:主线程分配给函数线程,每个函数线程有各自的队列,从各自的队列中取文件描述符。但是也可能存在其中一个函数线程的队列中积压了很多文件描述符待处理,就不能及时响应客户端的请求。
相比来说,第二种更值得实现,因为只要我们确保分配算法优秀,就不会存在挤压的情况。也就提出了负载均衡,将函数线程之间做成简单的负载,使线程工作的周期或者承载的压力相当,就得考验负载均衡算法(将文件描述符分配给队列积压最少的线程,而分配时就可以做一个工作周期的预估,根据时间分配)
二、某一线程/进程一旦与客户端开始通信,就必须等待与之结束通信之后才能与其他客户端通信,就浪费了大量资源。
为了解决这一问题就引出了I/O复用技术。