多进程编程¶
相比于多线程编程,多进程编程每个进程之间拥有更好的隔离性。一个进程崩溃基本不会导致其他进程也跟着崩溃。但是,独立性也带来了进程间共享数据困难的问题。使用多进程编程,可以像多线程编程一样,使用共享内存来完成进程间通信,也可以使用网络来进行通信。一般来说,如果需要程序运行在不同的机器上,那么就只能采用基于网络通信的多进程编程模型了,这也是常见的分布式系统的编程模型。多进程程序中,每个程序自己仍然可以使用多线程模型来提高 CPU 的利用率。
多线程和多进程
多进程可以理解为互不共享资源的线程,反过来讲,线程也可以理解为共享资源的进程,它们的界限有时候并不是那么清楚。在不同操作系统上,有的会分开区别,创建线程和进程的开销也有区别。例如,Linux 中,clone
系统调用既可以用来创建进程,也可以用来创建线程,无论是 fork
还是 pthread_create
,最终都会使用 clone
来创建进程/线程。线程在 Linux 中也叫做轻量进程(Light-weight Process, LWP)。
经典的使用多进程的程序有 nginx、PostgreSQL 等,其中 nginx 使用多进程的主要目的是负载均衡,不同的客户端连接将会分配到不同工作进程进行处理,一个工作进程会处理多个连接,而 PostgreSQL 则是为每一个客户端连接都开启新的进程,一个进程只处理一个连接。
在多线程编程中,由于线程之间共享资源,所以我们将注意力放到了如何避免共享资源出错,也就是避免竞态条件上。而在多进程编程里,我们首先需要解决如何让多个进程进行通信的问题。
System V IPC¶
大多数操作系统,例如 Linux、Solaris 等都实现了 System V 中引入的 IPC API。而在 Windows 操作系统中,通常也能找到功能和它们相似的函数。
这套经典的 API 包含以下组成部分。其中前三种是支持最广泛的。
- 信号量(Semaphore)
- 消息队列(Message queue)
- 共享内存(Shared memory)
- 管道(Pipe)
信号量¶
和我们在多线程编程中介绍的 POSIX 信号量不同,System V 信号量更加复杂。
消息队列¶
消息队列和多线程编程中的共享队列类似,多个生产者进程可以向队列中放入消息,消费者进程则可以从队列中获取消息。POSIX 消息队列还支持事件通知,配合 poll
和 epoll
等事件驱动机制,可以不需要轮询队列状态。
共享内存¶
在多进程编程中,可以使用共享内存的方法来解除一片内存区域的隔离性,从而使多个进程能够同时读写同一片内存空间,就和多线程编程一样。共享内存的原理是,操作系统将同一片物理内存映射到不同进程中的虚拟地址空间中,并设置页表项。然而,不同进程被映射到的虚拟地址可能不同,因此不能像在多线程编程中一样,在不同进程之间直接传递裸指针。
通常来说,我们需要传递一个间接指针,或者偏移量。例如我们可以将一片内存空间作为一个整形数组使用,那么此时可以使用数组索引的方法,来访问这片内存的单个值。
管道¶
管道顾名思义,就是将两个进程连接起来,它类似于没有缓冲空间的消息队列,当一个进程向管道写入数据时,将阻塞等待,直到有另一个进程从管道中读取数据。管道分为匿名管道和命名管道,匿名管道通常用于父子进程之间的通信,而命名管道则方便多个进程之间通信。