高性能服务器编程-------线程池与进程池_Eunice_fan1207的博客-程序员宝宝

使用多进程或多线程与客户进行交互的时候(子进程/子线程实现并发服务器),每一个客户端链接就会给其分配一个为其服务的进程/线程,有什么弊端?

  • 动态创建线程/进程是比较耗费时间的,这就导致较慢的客户响应
  • 动态创建的子进程/子线程通常只为一个客户服务,这就导致系统上产生大量的进程/线程,程序员难以管理,并且进程/线程间的切换是很耗费CPU时间
  • 对于多进程我们必须要谨慎的管理其分配的文件描述符堆内存等系统资源,否则可能会使得系统的可用资源急剧下降,影响服务器的性能

进程池和线程池可以帮助我们解决如上问题

进程池和线程池概述?

进程池与线程池相似,我们这里以进程池为例。与内存池相似,进程池就是提前创建一组子进程,这些子进程的数目在3~10个之间(与CPU的处理能力有关,不能创建太多,要不然时间也都浪费在cpu切换上了)。可以参考httpd守护进程就是使用包含7个子进程的进程池实现并发的。线程池中的线程数量和CPU数量差不多就行(多了还是得切换,没必要)。进程池中的所有子进程运行着相同的代码,具有相同的属性(优先级,PGID等),进程池在服务器启动之初就创建好了,每个子进程没有从父进程继承一些没有必要的文件描述符,也不会从父进程复制大块的堆内存。当有客户端与服务器交互,主进程就通过某种方式选择进程池中的一个进程为其服务,相比于选择一个已存在的子进程代价显然比动态创建一个进程小的多。

我们看看httpd守护进程的进程池(要使用root用户开启httpd服务)ps -efl |grep httpd可以查看线程号

root为主进程,其他都是进程池中的

线程池实现

1:主线程需要将文件描述符传递给函数线程    使用全局数组即可进行线程间通讯   

2:函数线程启动起来需要阻塞在获取文件描述符之前(没有客户连接就获取不到,没必要轮询做无用功,也即有客户连接函数线程获取这个链接描述符,需要同步控制),信号量控制主线程向函数线程通知获取文件描述符,函数线程即可阻塞。

3:各个函数线程之间从全局数组获取文件描述符需要互斥,另外函数线程获取文件描述符与主线程往全局数组插入文件描述符要互斥,需要加锁进行同步控制。

示例代码:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<pthread.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<semaphore.h>
sem_t sem;   //该信号量是为了阻塞函数线程获取链接描述符
pthread_mutex_t mutex; //加锁的目的为了插入和获取同时只有一个函数对数组操作
int clink[10]; //定义为一个变量不好,有可能函数线程还没执行操作就被主线程获取新的连接描述符覆盖,成为新的一个客户端链接
void InitClink()  //初始化存放链接描述符的数组
{
	memset(clink,-1,sizeof(clink));
/*	int i=0;
	for(;i<10;i++)
		clink[i]=-1;*/
}
int Insert(int c)  //找到第一个可以插入的位置将c插入
{
	pthread_mutex_lock(&mutex);  //避免插入与获取同时执行,线程不安全
	int i=0;
	for(;i<10;i++)
	{
		if(clink[i]==-1)  
		{
			clink[i]=c;
			break;
		}
	}
	pthread_mutex_unlock(&mutex);
	if(i>=10)   //插入失败
		return -1;
	return 0;   
}
int GetCli()  //获取链接描述符
{
	pthread_mutex_lock(&mutex);  //也避免了多个线程同时获取c
	int i=0;
	int c=clink[0];  
	for(;i<9&&clink[i+1]!=-1;i++)
	{
		clink[i]=clink[i+1]; //按照队列顺序获取链接描述符,将后面的元素前移
	}
	clink[i]=-1;
	pthread_mutex_unlock(&mutex);
	return c;
}
void* pthread_fun(void *arg)
{
	while(1)  //保证函数线程不退出,处理多个客户端
	{
		//连接描述符数组中没有新链接,函数线程就阻塞在这里等待获取新链接
		sem_wait(&sem);
		int c=GetCli();  
		while(1)  //与特定的客户端交互
		{
			char buff[128]={0};
			int n=recv(c,buff,127,0);
			if(n<=0)
			{
				printf("recv over");
				close(c);   //与多线程一样,直接就关闭了链接
				break;
			}
			printf("%d:%s\n",c,buff);
			send(c,"ok",2,0);
		}
	}
}
int main()
{
	int listenfd=socket(AF_INET,SOCK_STREAM,0);
	assert(listenfd!=-1);

	struct sockaddr_in ser,cli;
	ser.sin_family=AF_INET;
	ser.sin_port=htons(8000);
	ser.sin_addr.s_addr=inet_addr("127.0.0.1");

	int res=bind(listenfd,(struct sockaddr*)&ser,sizeof(ser));
	assert(res!=-1);

	listen(listenfd,5);
	sem_init(&sem,0,0); //确保有链接插入到数组中,才能在函数线程中获取
	pthread_mutex_init(&mutex,NULL);
	//创建线程池
	int i=0;
	for(;i<3;i++)  
	{
		pthread_t id;
		pthread_create(&id,NULL,pthread_fun,NULL);
	}
	InitClink();
	while(1)   //主线程依旧是为了接受客户端连接
	{
		int len=sizeof(cli);
		int c=accept(listenfd,(struct sockaddr*)&cli,&len);
		if(c<0)
			continue;
		if(Insert(c)==-1) //只有三个线程处理,所以即便插不进去也没必要扩容
		{
			close(c);  //直接关闭获取到的链接描述符
			continue;  //避免v操作
		}
		sem_post(&sem);//v操作代表插入成功,使得函数线程可以获取到c进行操作
	}
}

实际上我们定义的链接描述符的数组大小是大于等于子线程个数即可的,但是也没必要开的很大,因为我只定义了三个线程,最多同时也就处理三个线程,你将链接数组开的很大,其他的客户端还是在等待这,没有什么用。另外,线程也没必要开的特别多,这个是取决于你的CPU的核数,否则切换效率还是不高。

对于线程池实现,我只实现了一种子线程只要有空就可以获取链接进行交互;我们对于主线程如何分配描述符给子线程也是有相关的分配算法,实现分布式的负载均衡的一种分配策略。待实现。。。。

进程池实现

1:子进程获取文件描述符是从管道中获取的,管道中没有描述符的话获取就会阻塞,不用进行同步控制。

2:因为进程池间通讯是很麻烦的,所以也就不存在需要同步控制,防止进程间数据混乱。但是主进程对于链接描述符如何传递给子进程,有人就会说就像线程池那样直接传递那个文件描述符的值不就行了,但线程池是同一个进程的,文件描述符是针对的进程,所以各个线程之间是共享的,所以我们传递值过去是没有什么毛病的,但是进程不一样啊。又有人说哪不是fork之间的文件描述符共享的吗,对呀,但是我们子进程的创建是在服务器之初就创建了,正如我上面说的,这几个子进程都是“白板”,啥都没有,这个时候你就不能只给她传递一个值,因为这个值对于子进程来说就是一个下标,但是它的这个下标的内存中存放的是NULL,而不是像父进程那样指向了一个strut file的结构,所以我们需要通过管道方式在进程间传递文件描述符(网络通讯的管道,不是进程间通讯的管道,网络的管道socketpair是全双工滴。。。)

代码实现:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<fcntl.h>
static const int CONTROL_LEN=CMSG_LEN(sizeof(int));
void send_fd(int fd,int fd_to_send) //将父进程打开的链接描述符发给子进程
{
	struct iovec mem;
	struct msghdr msg;
	char buff[0];
	mem.iov_base=buff;  //设置内存的起始地址
	mem.iov_len=1;    //设置内存的长度
	msg.msg_name=NULL;
	msg.msg_namelen=0;
	msg.msg_iov=&mem;
	msg.msg_iovlen=1;  //内存块个数

	struct cmsghdr cm;  
	cm.cmsg_len=CONTROL_LEN;
	cm.cmsg_level=SOL_SOCKET;
	cm.cmsg_type=SCM_RIGHTS;
	*(int *)CMSG_DATA(&cm)=fd_to_send;  //通过辅助数据发送的链接描述符
	msg.msg_control=&cm;
	msg.msg_controllen=CONTROL_LEN;

	sendmsg(fd,&msg,0);
}
int recv_fd(int fd)
{
	struct iovec mem;
	struct msghdr msg;
	char buff[0];

	mem.iov_base=buff;
	mem.iov_len=1;
	msg.msg_name=NULL;
	msg.msg_namelen=0;
	msg.msg_iov=&mem;
	msg.msg_iovlen=1;

	struct cmsghdr cm;
	msg.msg_control=&cm;
	msg.msg_controllen=CONTROL_LEN;

	recvmsg(fd,&msg,0);

	int fd_to_read=*(int *)CMSG_DATA(&cm);
	return fd_to_read;

}

int main()
{
	int pipefd[2];   //创建管道描述符
	int fd_to_pass=0;
	int ret=socketpair(PF_UNIX,SOCK_DGRAM,0,pipefd);  
	assert(ret!=-1);   //创建父子进程间的网络通讯管道

	int sockfd=socket(AF_INET,SOCK_STREAM,0);
	assert(sockfd!=-1);

	struct sockaddr_in ser,cli;
	ser.sin_family=AF_INET;
	ser.sin_port=htons(7000);
	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,status;
	for(i=0;i<3;i++)
	{
		status=fork();
		if(status==0)break;  //避免子进程生成孙子进程
	}

	if(0==status)  //子进程执行模块
	{
		while(1)
		{
			close(pipefd[1]);  //关闭写端
			fd_to_pass=recv_fd(pipefd[0]); 
			printf("hello\n");
			while(1)
			{
				char buff[128]={0};
				int n=recv(fd_to_pass,buff,127,0);
				if(n<=0)
				{
					printf("recv over\n");
					close(fd_to_pass);
					break;
				}
				printf("%d:%s\n",fd_to_pass,buff);
				send(fd_to_pass,"ok",2,0);
			}
		}
	}
	else  //父进程执行模块
	{
		close(pipefd[0]);  //在父进程中将读通道关闭,虽然是全双工,但是父进程写的只能子进程读
		while(1)
		{
			int len=sizeof(cli);
			int c=accept(sockfd,(struct sockaddr*)&cli,&len);
			if(c<0)
				continue;
			send_fd(pipefd[1],c);
		}
	}
}

这里我并没有开辟一个数组用来存储链接描述符,而是直接往管道中写就行了,等到子进程有空闲的,直接从管道中获取就行

进程池/线程池选择那个?

线程切换比较快,并且线程通讯比较方便,但是这也是问题,因为通讯比较方便有可能会发生线程不安全,需要进行同步控制。这倒不是比较进程与线程的区别,主要选择那个需要看我们的程序需要怎样的功能及需求。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/eunice_fan1207/article/details/84571596

智能推荐

【c++】输入一个n×n的矩阵,求出两条对角线元素值之和【原创技术】_Mr_just的博客-程序员宝宝

题目:输入一个n×n的矩阵,求出两条对角线元素值之和以及输出矩阵中最大值和最小值的下标。源代码://科目:C++实验2//题目:输入一个n×n的矩阵,求出两条对角线元素值之和以及输出矩阵中最大值和最小值的下标。//语言:C++//作者:武叶//创作时间:2012年3月8日 #include using namespace std; void main()

Boost Asio(一)初探_weixin_30824599的博客-程序员宝宝

一、简介Boost Asio ( asynchronous input and output)关注数据的异步输入输出。Boost Asio 库提供了平台无关性的异步数据处理能力(当然它也支持同步数据处理)。一般的数据传输过程需要通过函数的返回值来判断数据传输是否成功,而Boost Asio将数据传输分为两个独立的步骤:采用异步任务的方式开始数据传输。将传输结果通知调用端与传...

HDU 2897 邂逅明下(巴什博弈变形)_luxxxxxxx_的博客-程序员宝宝

转自kaungbin博客链接点击打开链接巴什博弈变形/*HDU 2897 邂逅明下大意:一堆石子共有n个,A,B两人轮流从中取,每次取的石子数必须在[p,q]区间内,若剩下的石子数少于p个,当前取者必须全部取完。最后取石子的人输。给出n,p,q,问先取者是否有必胜策略?Bash博弈的变形假设先取者为A,后取者为B,初始状态下有石子n个,除最后一次每次取的石子个数必须在[p,q]区间...

位运算_用于消去x的最后一位的1_Czayzx的博客-程序员宝宝

位运算需要注意的地方:1.注意打括号2. 1 &amp;amp;amp;amp; (state&amp;amp;amp;gt;&amp;amp;amp;gt;i) 和 state &amp;amp;amp;amp; ( 1&amp;amp;amp;lt;&amp;amp;amp;lt; i ) 还是有差别的。 前者的答案只有0和1,而后者的答案有0和可能的正数。判断的时候还是要注意下写法3. 当前行是否相邻的判断条件是: state &amp;amp;amp;amp; (state&amp;am

java 字符转整数_在Java中将字符转换为整数_LifeCcreator的博客-程序员宝宝

public class IntergerParser {public static void main(String[] args){String number = "+123123";System.out.println(parseInt(number));}private static int parseInt(String number){char[] numChar = number.t...

随便推点

java之输入输出(ACM,OJ相关)_frcoder的博客-程序员宝宝

1. java输入输出的基础部分2. 浮点数输出3. 多进制输出(8、16进制)

Android studio layout布局2_ZX99977的博客-程序员宝宝

相对布局(重点)1.1 相对布局窗口内子组件的位置总是相对兄弟组件、父容器来决定的,因此叫相对布局1.2 如果A组件位置是由B组件的位置决定的,Android要求先定B组件,再定义A组件如果A组件位置是由B组件的位置决定的,Android要求先定B组件,再定义A组件如果A组件位置是由B组件的位置决定的,Android要求先定B组件,再定义A组件注1:注意XML中组件的顺序,不然会报错...

迷宫求解(非递归)_非递归求解迷宫问题_qq_41027326的博客-程序员宝宝

    上篇文章写出了利用函数形成栈桢的特性完成迷宫求解问题, 本篇文章我们自己手动维护一个栈, 其进行出栈, 入栈, 取栈顶元素, 来完成迷宫求解寻路的过程     思路和以前一样, 首先, 我们先定义一个栈, 对其初始化, 同时, 定义一个迷宫地图, 对该地图进行初始化, 先判断当前位置是否可以落脚, 如果不能落脚就直接 return, 如果能够落脚, 就将入栈同时将其标记, 标记完之后就循...

方法重写与方法重载的区别_方法重写与方法重载的区别 csdn_java程序员劝退师的博客-程序员宝宝

方法重载是实现的是编译时的多态性(也称前绑定),方法重写实现的是运行时的多态性(也称后绑定)。一、方法重写(0veriding)在Java程序中,类的继承关系可以产生一个子类,子类继承父类,它具备了父类所有的特征,继承了父类所有的方法和变量。子类可以定义新的特征,当子类需要修改父类的一些方法进行扩展,增大功能,程序设计者常常把这样的一种操作方法称为重写,也叫称为覆写或覆盖。重写体现了J...

LINUX下的SD卡分区_Decisiveness的博客-程序员宝宝

LINUX下的SD卡分区    首先在windows下面使用HP格式化工具格式化SD卡,然后将SD卡接入Linux操作系统。识别SD卡后,打开终端。查看SD卡是否已经挂载,如果已经挂载需要先卸载再操作。可以通过mount来进行查看。输入mount,我们可以看到我们的盘符为sdb。在终端输入umount /media/FAT32将其卸载。   下面我们对SD卡进行分区。首先我们看一下fd

DOM中的动态NodeList与静态NodeList(为何getElementsByTagName()比querySelectorAll()快100倍)_BetaCat1的博客-程序员宝宝

GitHub版本:&nbsp;https://github.com/cncounter/translation/blob/master/tiemao_2014/NodeList/NodeList.md副标题: 为何getElementsByTagName()比querySelector...

推荐文章

热门文章

相关标签