关于并发和 I/O 模型
并发和并行
一提到并发,通常就会提起并行,那么什么是并发,什么又是并行?
并行意味着一个任务同时有多个可执行单元在运行。
从操作系统的角度来看,并行意味着一个任务被多个CPU 核心执行。 单独一个 CPU 核心不能执行并行计算,它可以是在多个任务中来回切换地进行运算,但是不能被称为并行。并行的必要条件是多CPU 核心 或多 CPU 的计算环境。
并发意味着在同一时期内执行多个任务,可以在一个 CPU 核心上,也可以在多个 CPU 核心上。但是在同一时期,这些任务不一定都处于运行状态,这取决于 CPU 核心或者 CPU的数量。
阻塞 IO 、 非阻塞 IO和多路复用 IO
阻塞 IO 最传统的一种 IO 模型,即在读写数据的过程中会发生阻塞现象。
非阻塞 IO 用户发起 IO 请求后, 并不需要等待,而是马上就得到一个结果。用户线程需要不断地询问内核数据是否就绪,会一直占用 CPU
多路复用 IO 会有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真正调用实际的 IO读写操作,因为只需要用一个线程就可以管理多个 socket, 系统不需要建立新的进程或者线程,也不必维护这些线程和进程,所以大大减少了资源占用。多路复用有多种方案,比如 select poll epoll 之后另开一篇再表。
另外IO 模型还有 信号驱动 IO, 异步IO, 也在之后的文章中再讲。
并发 IO 模型
1. 单进程阻塞IO
最原始的一种 IO 模型,任务只能一个接着一个按序执行,哪怕 CPU 空闲着。只有一个 PHP脚本在执行。
2. 多进程阻塞IO
这是目前 PHP-FPM 和大部分 CGI 脚本(Python、Ruby)所用的模型
这个模型的缺点是:如果并发量很大的情况下,服务器会来不及处理,会引发 502
3. 多线程阻塞 IO
这是 Java Web Application所用的模型
线程比进程轻量,并且能在应用间共享内存和状态。
每个请求都单独的占用一个线程,线程由线程池管理来复用,减少重新创建线程的系统开销。
Java 语言支持非阻塞 IO 模型, 但是大多数 Web 服务器都没在使用。
4. 单线程非阻塞 IO
这是 Node.js 使用的模型
采用的是 Event Loop 的模式,每次发出请求后,会给出回调函数,一旦完成 IO 操作后,就会被执行。底层是将任务放入队列,处理好任务后,会将结果返回给回调函数。
这个模型的缺点是:每一个请求都在同一个周期内执行,一个请求可能会在同一周期内阻塞其他请求,处理的链接越多,每个请求的响应时间就越慢。如果请求很大,无法发挥多核 CPU 应有的性能。
5. Reactor 和单线程 IO 多路复用
这是 Redis 6.0 之前采用的模型, 6.0 之后采用的多线程IO,只是用来处理网络数据的读写和协议的解析,而执行命令依旧是单线程。
单线程的优点:单线程操作,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。
所以 Redis 的单线程处理速度仍然很快。
6. Reactor 和多线程
这是 Java Netty 采用的模型
Reactor 模式下主程序只负责监听文件描述符上是否有事件发生,并不处理文件描述符的读写。读写由其他的工作程序做。主程序用来抗并发,不阻塞,且非常的轻便,事件可以通过队列的方式等待被工作程序执行。
7. 多线程、协程 非阻塞IO
这是 Go 采用的模型
Go 原生支持并发,采用轻量级的线程,协程(大小默认 2KB)来处理,并且在程序端来管理这些协程,可以充分利用多核 CPU的性能。是一种非常好的并发解决方案。
以上就是目前流行的或者历史存在过的并发模型,当然还有其他的变体,但都是大同小异。通过这些模型方法最终需要解决的依然是 IO 速度和 CPU 速度之间的差。