4、多进程与多线程模型
4.1、多进程模型
使⽤多进程模型,也就是为每个客户端分配⼀个进程来处理请求。服务器的主进程负责监听客户的连接,⼀旦与客户端连接完成,accept() 函数就会返回⼀个「已连接Socket」,这时就通过 fork() 函数创建⼀个⼦进程,实际上就把⽗进程所有相关的东⻄都复制⼀份,包括⽂件描述符、内存地址空间、程序计数器、执⾏的代码等。
这两个进程刚复制完的时候,⼏乎一模一样。不过,会根据返回值来区分是⽗进程还是⼦进程,如果返回值是 0,则是⼦进程;如果返回值是其他的整数,就是⽗进程。
正因为⼦进程会复制⽗进程的⽂件描述符,于是就可以直接使⽤「已连接 Socket 」和客户端通信了,可以发现,⼦进程不需要关⼼「监听 Socket」,只需要关⼼「已连接 Socket」;⽗进程则相反,将客户服务交给⼦进程来处理,因此⽗进程不需要关⼼「已连接 Socket」,只需要关⼼「监听 Socket」。
存在问题:进程间上下文切换开销大
4.2、多线程模型
对于多线程模式,也就说来了client,服务器就会新建一个线程来处理该client的读写事件。 这种模式虽然处理起来简单方便,但是由于服务器为每个client的连接都采用一个线程去处理,使得资源占用非常大。因此,当连接数量达到上限时,再有用户请求连接,直接会导致资源瓶颈,严重的可能会直接导致服务器崩溃。因此,为了解决这种一个线程对应一个客户端模式带来的问题,提出了采用线程池的方式。
为什么引入多线程模型?
主要是因为进程间上下文切换系统开销大。
线程是运⾏在进程中的⼀个“逻辑流”,单进程中可以运⾏多个线程,同进程⾥的线程可以共享进程的部分资源的,⽐如⽂件描述符列表、进程空间、代码、全局数据、堆、共享库等,这些共享些资源在上下⽂切换时是不需要切换,⽽只需要切换线程的私有数据、寄存器等不共享的数据,因此同⼀个进程下的线程上下⽂切换的开销要⽐进程⼩得多。
当服务器与客户端 TCP 完成连接后,通过 pthread_create() 函数创建线程,然后将「已连接 Socket」的⽂件描述符传递给线程函数,接着在线程⾥和客户端进⾏通信,从⽽达到并发处理的⽬的。
线程池
可以使⽤线程池的⽅式来避免线程的频繁创建和销毁,所谓的线程池,就是提前创建一个固定大小的线程池,来一个客户端,就从线程池取一个空闲线程来处理,当客户端处理完读写操作之后,就交出对线程的占用。也就是说当由新连接建⽴时,将这个已连接的Socket 放⼊到⼀个队列里,然后线程池⾥的线程负责从队列中取出已连接 Socket 进程处理。这样就避免为每一个客户端都要创建线程带来的资源浪费,使得线程可以重用。
但是线程池也有它的弊端,如果连接大多是长连接,因此可能会导致在一段时间内,线程池中的线程都被占用,那么当再有用户请求连接时,由于没有可用的空闲线程来处理,就会导致客户端连接失败,从而影响用户体验。因此,线程池比较适合大量的短连接应用。