基于进程的并发
基本模型
在TCP服务器编程中,多进程并发服务器通常由主进程负责连接的建立,然后fork出子进程,负责该连接剩下的行为,直到关闭。
关于多进程并发服务器有几点重要的内容:
通常服务器会运行很长时间,因此必须要包括一个SIGCHLD处理程序,来回收僵死子进程的资源。因为当SIGCHLD处理程序执行时,SIGCHLD信号是阻塞的,而Unix信号是不排队的,所以SIGCHLD处理程序必须准备好回收多个僵死子进程的资源。
父进程在fork调用后,将连接交给子进程处理,父子进程必须关闭他们各自不用的fd拷贝,对父进程尤为重要。
因为套接字的文件表表项中的引用计数,直到父子进程的fd都关闭了,到客户端的连接才会终止。
优劣
进程有独立的地址空间,既是优点也是缺点,这样一来一个进程不可能不小心覆盖另一个进程的虚拟存储器,另一方面,独立的地址空间使得进程共享状态信息变得更加困难。
基于进程的并发能力毕竟有限,想想系统下成百上千个进程的情况下,系统还能运转多快,更别说几万了。
基于I/O多路复用的并发
何为多路复用
虽然了解I/O多路复用,但一直以来,对这个名字很好奇,什么叫多路复用?为什么称为IO多路复用?
通过查看维基上的定义算是有了一个初步的认识,以下是在维基中摘录的部分介绍:
多路复用(Multiplexing,又称“多工”)是一个通信和计算机网络领域的专业术语,通常表示在一个信道上传输多路信号或数据流的过程和技术。因为多路复用能够将多个低速信道整合到一个高速信道进行传输,从而有效地利用了高速信道。通过使用多路复用,通信运营商可以避免维护多条线路,从而有效地节约运营成本。
多路复用的抽象模型
首先,各个低速信道的信号通过多路复用器(MUX,多工器)组合成一路可以在高速信道传输的信号。在这个信号通过高速信道到达接收端之后,再由分路器(DEMUX,解多工器)将高速信道传输的信号转换成多个低速信道的信号,并且转发给对应的低速信道。查看原文请点击。
反过来看I/O多路复用,就是在一个线程中,处理多路I/O。对应于多路复用的原则,线程处理能力必须快,I/O需要尽可能的拆分成小的单元,每个单元尽量短的占用线程的时间,如果某个I/O处理单元长时间占用线程的处理时间,就会导致其他I/O得不到及时的处理。
I/O多路复用的基本思想
比如我的程序需要从多个I/O上等待数据到来并读取
pipe fd1
tcp socket1 fd2
tcp socket2 fd3
udp socket fd4
那么定义一个数组{fd1,fd2,fd3,fd4}存储好,并通知内核,我对这几个fd的输入事件感兴趣,此时内核会说你等着吧,等他们谁有数据了我会通知你,然后你就睡眠了。
当某个fd有数据到达可读时,内核会立即通知你,嘿,有数据来了,快醒了。于是你赶快醒来,找到哪个fd有数据,然后接收处理,处理完了,一轮结束继续告知内核感兴趣的事件,然后睡眠。
linux支持I/O多路复用的系统调用有select、poll、epoll。
I/O多路复用优缺点
I/O多路复用可以用于事件驱动的编程,事件驱动设计,比基于进程的设计给了程序员给多的多程序行为的控制。例如一个并发服务器,需要为某些客户端提供特殊化的服务,基于事件驱动的设计,可以在流程里面有选择的处理。而基于进程的设计,需要在fork进程之前,选择子进程的行为,如果特殊化的服务比较多,基于进程的设计处理起来是很困难的。
基于I/O多路复用的服务器是运行在单一进程上下文中,因此每个逻辑流都能访问该进程的全部地址空间,使得流之间共享数据变得很容易。
基于I/O多路复用的服务器在GDB调试时要比多进程方便。
基于驱动设计常常比基于进程的设计要高效的多,因为他们并不需要进程上下文切换。
事件驱动设计的一个明显的缺点是编码复杂,随着并发颗粒度(每个逻辑流在每个时间片上执行的指令数量)减小,复杂性还会上升。并发粒度控制不好,很容易导致忙于处理某个逻辑流,其他流得不到处理。
基于线程的并发
基于线程的逻辑流结合了基于进程和基于I/O多路复用的的流的特性。同进程一样,线程由内核自动调度,并且内核通过一个整数ID来识别线程。同基于I/O多路复用的流一样,多个线程运行在单一进程的上下文中,因此共享这个进程虚拟地址空间的整个内容,包括他的代码、数据、堆、共享库和打开的文件。
基本多线程模型
整个模型类似与基于多进程的设计,主线程不断的等待连接请求,然后创建一个线程处理该请求。
这个模型中有个微妙的问题,主线程建立某一新连接的fd,如果直接传递给子线程,那么很有可能在子线程处理前,主线程建立了另一个连接,并更新了fd,那么不幸的结果就是,现在两个线程在同一个描述符上执行输入输出。以下是示例代码:
1 connfd = accept(listenfd, &clientaddr, &clientlen); 2 pthread_create(&tid, NULL, thread, &connfd);
另一个问题是在线程例程中避免存储器泄露,要么显示回收内存,要么必须分离内存。
基于预线程化的生产者-消费者模型
服务器由一个主线程和一组工作者线程构成,主线程不断的接受来自客户端的连接请求,并将得到的连接描述符放在一个有限缓冲区中。每一个工作者线程反复的从共享缓冲区中取出描述符,为客户端服务,然后等待下一个描述符。
I/O多路复用不时编写事件驱动程序的唯一方法,上述模型实际上也是一个事件驱动服务器,带有主线程和工作线程的简单状态机。
多线程并发编程中需要考虑的一些问题参考《深入理解计算机系统》12.7节,如果你有更好的书,记得推荐给我哟。
高级并发编程
参考NGINX并发处理模型的设计
基本的并发编程模型