用Java的人通常写的是“单进程多线程”的程序;而用C++的人,可能写的是“单进程多线程”“多进程单线程”“多进程多线程”的程序(这里主要指Linux系统上的服务器程序)。之所以会有这样的差异,是因为Java程序并不直接运行在Linux系统上,而是运行在JVM之上。而一个JVM实例是一个Linux进程,每一个JVM都是一个独立的“沙盒”,JVM之间相互独立,互不通信。所以Java程序只能在这一个进程里面,开多个线程实现并发。而C++直接运行在Linux系统上,可以直接利用Linux系统提供的强大的进程间通信机制(IPC),很容易创建多个进程,并实现进程间的通信。
“多进程多线程”是“单进程多线程”和“多进程单线程”的组合体,其原理并没有差异,所以接下来只讨论“单进程多线程”和“多进程单线程”两种编程模型,对比“多进程”和“多线程”的关键差异。
1.为什么要多线程
对于客户端程序,有UI交互界面,多线程不可避免,这类程序不在讨论之列。本节主要讨论的是服务器端的程序。
这里所说的“多”线程,是指运行几百个业务线程的服务器程序。如果是4核CPU,运行4个线程,本质上仍是单线程。之所以要开多线程,是因为服务器端的程序往往是I/O密集型的应用。举个极端的例子,假设程序没有任何I/O(磁盘I/O或网络I/O),纯粹的CPU计算,如同一个最简单的、空的死循环,只需要一个线程就可以把一个CPU的核占满。
所以,多线程主要是为了应对I/O密集型的应用。多线程能带来两方面的好处:
(1)提高CPU利用率。通俗地讲,不能让CPU空闲着。当一个线程发生I/O时,会把该线程从CPU上调度下来,并把其他的线程调度上去,继续计算。
(2)提高I/O吞吐。典型的场景是,应用程序连接的Redis或者MySQL,它们提供的都是同步接口,一次只能处理一个请求。要想并发,办法是通过连接池和多线程,实现每个线程使用一个连接。好比在客户端和服务器之间开了多条通道,并行传输数据。
除了多线程,线程间的同步机制也非常复杂,在此只列举线程间常用的同步机制:
· 锁(悲观锁、乐观锁、互斥锁、读写锁、自旋锁、公平锁、非公平锁等)。
· Wait与Signal。
· Condition.
无论C++开发者在Linux系统中使用的pthread,还是Java开发者使用的JUC库,都有这些基本机制。基于这些基本机制,又可以封装出各式各样的、便于应用层使用的同步机制,比如信号量、Future、线程池,还可以封装出各式各样的线程安全的数据结构,比如阻塞队列、并发HashMap等。
2.多进程
既然多线程可以实现并发,那为什么要设计多进程呢?因为多线程存在两个问题,一是线程间内存共享,要加线程锁;而加锁后会导致并发效率下降,同时复杂的加锁机制也将增加编码的难度;二是过多的线程造成线程间的上下文切换,导致效率低下。
在并发编程领域,一直有一个很重要的设计原则:“不要通过共享内存来实现通信,而应通过通信实现共享内存。”这句话不太好理解,换成通俗一点的说法就是:“尽可能通过消息通信,而不是共享内存来实现进程或者线程之间的同步。”
进程是资源分配的基本单位,进程间不共享资源,通过管道或者Socket方式通信(当然也可以共享内存),这种通信方式天生符合上面的并发设计原则。而对于多线程,大家习惯于共享内存,然后通过加各种锁来实现同步。虽然在多线程领域也有这种思想的实现,比如 Akka 框架,但流行程度仍然不够。
除锁的问题之外,多进程还带来另外两个好处:一是减少了多线程在不同的CPU核间切换的开销;另外,多进程相互独立,意味着其中一个崩溃后,其他进程可以继续运行,这对程序的可靠性很有帮助。
多进程模型的典型例子是Nginx。Nginx有一个Master进程,N个Worker进程,每个Worker进程对应一个CPU核,每个进程都是单线程的。Master进程不接收请求,负责管理功能;各个Worker 进程间相互独立,并行地接收客户端的请求,也不需要像多线程那样在不同的 CPU 核间切换。
有了多进程之后,在每个进程内部,可能是单线程,也可能是多线程,这往往取决于I/O。比如Redis就是单进程单线程的模型(这里说的单线程模型,不是指整个Redis服务器只有一个线程,而是指接收并处理客户端请求的线程只有一个)。之所以单线程可以支持,是因为在请求接收的地方用的是epoll的I/O多路复用,在请求处理的地方又完全是内存操作,没有磁盘或者网络I/O,所以只需单线程就足够了。要利用多核也很简单,开多个Redis实例就可以了。
但对于I/O密集型的应用,要提高I/O效率,则需要下面几种办法:
(1)异步 I/O。如果客户端、服务器都是自己写的,比如 RPC 调用,则可以把所有的 I/O都异步化(利用epoll或者真正的异步I/O)。异步化之后,请求可以Pipeline处理,就不需要多线程了。但像MySQL的JDBC提供的都是同步接口,不支持I/O异步。
(2)多线程。I/O不支持异步,就只能开多个线程,每个线程都是同步地调用I/O,实际上是用多线程模拟了异步I/O。典型例子是Web应用服务器调用Redis或MySQL。
(3)多协程。
3.多协程
多线程除锁的问题之外,还有一个问题是线程太多,切换的开销很大。虽然线程切换的开销比进程切换的开销小很多,但还是不够。以常用的Tomcat服务器为例,在通常配置的机器上最多也只能开几百个线程。如果再多,则线程切换的开销太大,并发效率反而会下降,这意味着Tomcat最多只能并发地处理几百个请求。但如果是协程的话,可以开几万个!协程相比线程,有两个关键特点:
· 更好地利用CPU:线程的调度是由操作系统完成的,应用程序干预不了,协程可以由应用程序自己调度。
· 更好地利用内存:协程的堆栈大小不是固定的,用多少申请多少,内存利用率更高。
现代的编程语言像Go、Rust,原生就有协程的支持,但偏传统的Java、C++等语言没有原生支持。因此,产生一些第三方的方案,比如Java的Quasar Fiber、微信团队为C++研发的libco等,但普及程度还比较低,开发者还是习惯多线程的开发模型。
最后,表4-3总结了多线程、多进程和多协程编程模型的对此。
表4-3 多线程、多进程和多协程编程模型的对比
多年建站经验,上千个成功案例,
为您提供一站式服务
大厂经验工程师对现有网站进行
改版,修复,维护。
微信小程序,支付宝小程序,
百度小程序
响应式网页设计可以与多种设备兼容,
如智能手机,平板电脑和PC