多线程程序、多进程程序是当前单机应用常用并行化的手段,线程是可以直接被CPU调度的执行单元,虽然多进程程序中每个进程也可以是多线程的,但是本文主要讨论的多进程程序默认是每个进程都有一个单独线程的情况。多线程程序和多进程程序,涉及到的线程间和进程间的通信、同步原语基本都是相同的,所以两者的开发在一定程度上有着高度的相似性,但同时差异化也十分的明显,所以高性能程序使用多线程还是多进程实现常常也是争论的焦点。
虽然自己之前开发的程序基本都是基于pthreads和C++ std::thread的多线程程序,但是多进程程序还是有它相应的用武之地的,比如大名鼎鼎的Nginx中master和worker机制就是采用多进程的方式实现的,所以这里也对多进程和多线程程序的区别联系整理一下,最后顺便看看Nginx中master和worker进程的管理和实现机制,在后续开发多进程程序的时候可以直接借鉴使用。
一、多线程和多进程程序
Linux中有一句耳熟能详的话——线程被认为是轻量级的进程,在现代操作系统的概念中,进程被认为是资源管理的最小单元,而线程被认为是程序执行的最小单元,所以多线程和多进程之间的差异基本体现在执行单元之间对资源耦合度的差异。虽然对于用户空间而言,最为广为使用的pthreads线程库提供了自己一套线程创建和管理、线程间同步接口,其实在Linux下面创线程和创建进程都是使用clone()系统调用实现的,只是在调用参数(flags)上不同,导致创建的执行单元具有不一样的资源共享情况,从而造就了线程和进程实质上的差异。
1.1 多线程的特点 multi-threaded
从上面的图中看出,同一个进程中的多个线程,跟执行状态相关的资源都是独立的,比如:运行栈、优先级、程序计数器、信号掩码等都是独立的,而打开的文件描述符(包含套接字)、地址空间(除了函数中的自动变量属于栈管理,还有新提出来的线程局部变量,其它基本都是共享的)都是共享的。这里还设计到信号处理句柄、信号掩码等,因为在多线程中信号的问题比较的复杂,后面单独列出来解释。
共享相同的地址空间、文件描述符给程序的开发带来了极大的便利,创建多线程的开销要小的多,而且在运行中任务切换损失也很小,很多的缓存都维持有效的,还有比如负责套接字listen的线程和工作线程之间可以方便的传递网络连接创建的套接字,生产线程和消费线程可以方便的用队列进行数据交换,程序设计也可以特化出日志记录、数据落盘等工作线程各司其职。但是天下没有免费的午餐,任何的便利都是需要付出代价的,多个执行单元可以访问资源意味着共享资源必须得到保护和同步,这是多线程程序设计不可回避的问题:
(1). 多个线程可以安全的访问只读的资源,但是哪怕只有一个修改者也是不安全的,额外说一句,我们说的保护是保护的资源,而不是行为;
(2). 传统很多库函数都不是线程安全的,这些函数当初设计的时候没有考虑到多线程的问题,所以使用了大量的全局变量和静态局部变量,这些函数是不可重入的。所以在你调用库函数、链接别人库的时候,一定要看看有没有”_r”后缀的版本;
(3). 还要就是之前不断被提到的内存模型,因为同个进程中的多个线程可能会并行的执行,这时候如果在线程之间有高速度的数据同步需求的时候,必须让资源的更新能够及时地被别的线程感知到;
(4). 多线程程序正因为线程之间共享的资源太多,所以如果一个线程出现严重的问题,其余的线程也会被杀死。遥想当年在TP-LINK的时候,所有的服务功能都以线程的形式被包裹在一个用户进程中,某个模块出现问题都可能导致上不了网需要重启,所以现在看来稳定运行的TP-LINK路由器不得不说是一个奇迹~
1.2 多进程的特点 multi-process
多进程程序之间保证了资源的高度隔离,只在创建出来的父子进程之间有少量的联系,进程组、回话等就不在此讨论了。
这个时候需要共享的资源必须显式共享,虽然操作系统优化机制可以让他们的只读数据(比如执行代码)物理上共享,进程间的资源共享或者通过关联到文件系统的某个路径或者文件,或者通过全局字符串名字方式,通过以某个进程首先创建资源,其他进程打开资源的方式共享。由于历史原因,Linux进程间通信通常包含SYS V和Posix两套接口,其种类和功能大同小异,但是个人的实际感受Posix的操作接口要更加的好用一些。