如何实现同一时刻能与多个客户端同时交互?我们希望是服务器与多个客户端交互以并发进行处理。在我们高性能服务器编程下有这样几种方法,一是多进程/多线程,二是进程池/线程池,三是I/O复用。
目录
(二)多线程:启动多个线程,每个线程执行和一个客户端交互的程序;
文将介绍前两种多进程/多线程,进程池/线程池。
一、多进程/多线程
(一)多进程
启动多个进程,每个进程执行和一个客户端交互的程序;父进程完成与客户端的连接工作,完成后,创建子进程,子进程与客户端具体交互。
多进程实现服务器和客户端的并发交互具体流程:父进程接收客户端的连接,在父进程的PCB结构中记录所连接的文件描述符c(有限的个数,一般够用),当父进程接收到客户端的连接之后,创建子进程,在子进程创建的时候,会复制父进程的PCB结构,它们会指向同一个地址空间,然后让子进程与客户端进行具体的交互。当子进程和客户端通讯结束后,关闭c,并没有实际关闭,父进程必须显示关闭连接,这样才真正的关闭。
在Linux下实现的简单多进程交互的结果:
通过命令:netstat -natp 显示网络相关信息,可以看到当前的连接状态:
注意:
(1)子进程是否能访问c,——》能访问并且是浅拷贝父进程
(2)子进程关闭c,父进程并没有关闭 ——》父子进程都必须显式关闭c。
(二)多线程:启动多个线程,每个线程执行和一个客户端交互的程序;
主线程只负责接受客户连接,创建函数线程,函数线程负责通讯。
函数线程如何获取到与客户的连接文件描述符?创建线程时,以值传递的方式将连接的文件描述符传递给函数线程。不能使用地址传递。
主线程是否需要关闭文件描述符?主线程不需要关闭文件描述符,同一个进程中所有线程共享进程资源,仅有独立的栈区。
僵死进程——不需要,本身不会产生子进程,只有函数线程;
注意:
(1)函数线程是否能够访问c,--》能访问 打开的文件描述符资源时属于进程的。
(2)函数线程关闭c,链接已经断开。
(三)两者的选择和比较
1、多进程和多线程的选择
根据不同业务,不同的场景需求进行选择。
多进程和多线程的比较:
(1)从编程角度:多线程代码实现相对于进程而言简单,控制也简单;
(2)从占据资源:多进程比多线程大
(3)切换:线程的切换比进程的切换快;
(4)资源共享:线程比进程间共享资源多,线程共享全局,堆,文件等资源。所以还需要考虑线程安全性问题;进程间是独立的,不需要考虑安全性问题。
(5)从能够创建的数量:多进程比多线程要多很多;一个进程最多能够创建的线程是有限的;差不多300多。
2、多进程/多线程实现并发的缺陷:
(1)每个客户端连接上后,服务器都需要创建进程或线程;
(2)创建的进程或线程只为一个客户端服务于,服务完成后就会被释放,造成服务器资源浪费;
(3)一台机器上能创建的进程或线程是有限的;
二、进程池/线程池
(一)概念明确
池:提前申请的大量资源存放的一个单位;程序启动时,将需要的子进程或函数线程创建好,服务器程序结束时,这些进程或线程才会被释放。用池的概念将其维护;
内存池:使用内存之前先申请大量的内存,当后序需要使用时,直接从内存池中分配,而不需要通过系统分配。
进程池:进程池是由服务器预先创建的一组子进程;
线程池:线程池是由主线程预先创建的一组线程;
(二)进程池/线程池的设计思想
在服务器程序启动时,创建出多个进程或多个线程,将其维护在池中。当有客户连接时,就从池中分配进程或线程为客户端服务。一次创建的个数于操作系统的处理能力有关。
主线程或父进程主要负责与客户端连接;而函数线程或子进程与特定客户端进行交互。
1、进程池创建:
(1)在程序启动时,创建多个进程,将子进程维护在进程池中;
(2)进程池中的进程必须阻塞在获取客户端文件描述符之前
(3)主进程负责接收客户连接,并将获取到的客户连接文件描述符传递给进程池中的进程;必须借助于进程间通讯,不能仅仅传递c值,传递的是文件描述符。
(4)主进程通过某种机制唤醒进程池中的某个子进程来为特定的一个客户端进行服务;
2、线程池创建:
(1)程序启动 网络基础
(2)创建线程 线程是循环执行的,线程必须是阻塞在一条件上
(3)主线程通过线程下放选择一个子线程来执行任务;
(3)主线程接收客户连接并且在线程池中唤醒一个线程来为此客户服务;
注意:
线程池中线程的数量是有有限的,如何维护一个等待队列;
一个进程或者一个线程经常阻塞在i/o操作;
(三)进程池/线程池的工作流程
进程池的工作流程完全可以用到线程池中,因此我们只从一个出发来进行探讨:
进程池是由服务器预先创建的一组子进程,这些子进程的数目在3~10个之间。进城池中的所有子进程都运行着相同的代码,并且具有相同的属性,比如优先级,PGID等。
当有新的任务到来时,主进程将通过某种方式选择进程池中的某一个子进程来为之服务。相比与动态创建子进程,选择一个已经存在的子进程的代价显然要小的多。
主进程选择哪个子进程来为新的任务服务,有两种方式:
主进程使用某种算法 来主动选择子进程。最简单最常用的是随机算法和Round Robin轮流选取算法,但更优秀的算法将会使得任务在各个工作进程中更加均匀的分配,从而减轻服务器的整体压力。
主进程和所有子进程通过一个共享的 工作队列来同步,子进程都睡眠在该工作队列上。当有新的任务到来时,主进程将任务添加到工作队列中。这将唤醒正在等待任务的子进程,不过只有一个子进程将获得新任务的“接管权”,他可以从工作队列中选取出任务并执行之,而其他子进程将继续睡眠在工作队列中。
当选择好子进程后,主进程还需要使用某种通知机制来告诉目标子进程有新的任务需要处理,并传递必要的数据。最简单的方法就是在父进程和子进程之间预先建立好一条管道,然后通过管道来实现所有的进程间通信。在主线程个子线程之间传递数据就要简
(四)线程池的实现结果展示:
通过线程池实现多并发操作,服务器可以同时接收多个客户端发来的消息并且进行处理。
(五)两者比较
1、两者选择
(1)从占据资源:多进程比多线程大
(2)切换:线程的切换比进程的切换快;
(3)资源共享:线程比进程间共享资源多,线程共享全局,堆,文件等资源。所以还需要考虑线程安全性问题;进程间是独立的,不需要考虑安全性问题。
2、缺陷:
(1)启动时给某一进程池创建进程,创建进程的个数于主机环境有关。
(2)启动时在进程中创建线程,线程的创建个数也是有限的,大约300多;
3、线程池实现难点
(1)主线程需要将文件描述符传递给函数线程;
(2)函数线程启动起来后必须阻塞在获取文件描述符前;
(3)信号量来控制主线程向函数线程通知获取文件描述符事件;
(4)主线程在数组中插入数据以及函数线程获取数据都必须是互斥的。
4、进程池实现难点
进程池的实现难点是在主进程将获取到的客户连接文件描述符传递给进程池中的进程。这里用到进程间通讯的一些方式;