PHP是单线程,还是多线程的呢?
PHP是多进程,还是多线程的呢?
...
解决这些问题,首先必须先了解线程和进程。
备注:进程和线程的都是比较抽象的计算机概念,可参阅《漫画进程与线程》建立一个初步的认知。
计算机资源
经典的冯洛伊曼结构中把计算机系统抽象成“CPU+存储器+IO”三部分,计算机资源无外乎两种:
- 计算单元
CPU是计算单元,单纯从CPU角度来看它是一个黑盒,CPU只对输入的指令和数据进行计算,然后输出结果。CPU不负责管理计算那些“指令和数据”。换句话来说,CPU只提供计算能力,并不负责分配计算资源。那么计算资源是由谁来分配的呢?计算资源是操作系统来分配的,也就是操作系统的调度模块,由操作系统按照一定的规则来分配什么时候由谁来获得CPU的计算资源,比如分时间片。 - 存储资源
存储资源是内存、磁盘等存储设备的资源,操作系统使用虚拟内存机制来管理存储器。从缓存的角度来说,把内存作为磁盘的缓存。进程是面向磁盘的,为什么这么说呢?进程表示的是一个运行的程序,程序的代码段和数据段都是存放在磁盘中的,只是在运行时加载到内存中。所以虚拟内存面向的是磁盘,虚拟页是磁盘的分配,然后被缓存到物理内存的物理页中。因此,存储资源是操作系统由虚拟内存机制来管理和分配的,进程则是操作系统分配存储资源的最小单元。
什么是虚拟内存机制,有什么作用呢?
多任务
现代的操作系统如Windows、Linux、UNIX、MacOS等都是支持“多任务”的操作系统,那么什么是多任务呢?
简单来说,就是操作系统可以同时运行多个任务。现在多核CPU已经普及,但在早期单核时代也是可以执行多任务的。由于CPU执行代码都是顺序执行的,那么单核CPU是如何执行多任务的呢?
操作系统轮流让各个让任务交替执行,表面上看每个任务都是交替执行的,但是由于CPU的执行速度很快,在人类的感知中就好像是所有任务都在同时执行一样。
真正的并行执行多任务只能在多核CPU上实现,但是由于任务数据远远超过CPU的核心数量。所以,操作系统也会自动把多个任务轮流调度到每个核心上去执行。
对于操作系统来说,一个任务就是一个进程。由于每个进程至少要做一件事儿,但有些进程不止同时只做一件事儿。当在一个进程中需要同时做多件事情时就需要同时运行多个“子任务”,我们把进程内这些“子任务”称为线程。所以,一个进程至少有一个线程。
在实际编程中实现多任务的方式主要有三种:
- 多进程模式
- 多线程模式
- 多进程 + 多线程模式
同时执行多个任务通常各个任务之间并不是没有关系的,而是需要相互通信和协调。因此,多进程和多线程的程序的复杂度要远远高于单进程单线程的程序。
线程是最小的执行单元,而进程由至少一个线程组成。如何调度进程和线程,完全由操作系统决定,程序自己不能决定什么时候执行,执行多长时间。多进程和多线程的程序涉及到同步、数据共享等问题,编写起来比单线程更为复杂。
任务调度
大部分操作系统如Windows、Linux的任务调度是采用时间轮转的抢占式调度方式,也就是说,一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。任务执行的一小段时间叫做时间片,任务正在执行的状态叫做运行状态,任务执行一段时间后强制暂停去执行下一个任务,被暂停的任务就处于就绪状态并等待下一个属于它的时间片的到来。这样往复循环每个任务都得以执行,由于CPU的执行效率很高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在同时运行(并发)。
进程Process
什么是进程?
进程是操作系统结构的基础,Multics的设计者在20世纪60年代首次使用了“进程”这个技术词语,它比“作业”更加通用一些。
进程是对计算机的一种抽象,进程表示一个逻辑控制流,也就是一种计算过程,它造成了一个假象,好像这个进程一直是独占CPU资源的。另外,进程拥有一个独立的虚拟内存地址空间,它也造成了一个假象,好像这个进程一直在独占存储资源。
- 进程是正在执行的程序
- 进程是正在计算机上执行的程序的实例
- 进程是能分配到CPU并由CPU执行的实体
- 进程由单一顺序的执行线程、一个当前状态、一组相关的系统资源三者所描述的活动单元。
进程包含指令集和系统资源集
- 指令集:指的是程序代码
- 系统资源集:是指I/O、CPU、内存等。
简单来说,进程是具有一定独立功能的程序,在关于某个数据集合上的一次运行活动。换言之,进程是一个程序在一个数据集上的一次动态执行过程,是系统进行资源分配和调度的一个独立单位。
进程一般由程序、数据集、进程控制块三部分组成:
- 程序:编写的程序是用来描述进程要完成那些功能以及如何完成
- 数据集:是程序在执行过程中所需要使用的资源
- 进程控制块:是用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,是系统感知进程存在的唯一标志。
也可以把进程当作由一组元素组成的实体,进程的两个基本元素是:
- 程序代码:可能被执行相同程序的其它进程共享
- 代码相关联的数据集
假如CPU开始执行这个程序代码,把这个执行实体称为进程。在进程执行时,任意给定时间,进程都可以唯一的被表征为以下元素:
- 进程描述符
进程的唯一标识符,用来和其它进程区分。在Linux中叫做进程ID(pid
),它在系统调用fork
创建子进程时生成,注意若使用getpid
返回的其实并不是进程ID,而是线程组号tgid
。 - 进程状态
进程的状态包括就绪、挂起、运行... - 优先级
与进程之间执行调度相关,是相对于其它进程而言的。 - 程序计数器
程序中即将被执行的下一条指令的地址,是内核或用户内存空间中的内存地址。 - 内存指针
包含程序代码和进程相关数据的指针,以及与其它进程共享内存块的指针。 - 上下文数据
进程执行时CPU处理器上寄存器中的数据。 - IO状态
包含显式的I/O请求、分配给进程的I/O设备等。 - 记账信息
可能包含CPU处理时间总和、使用的时钟总和、时间限制等。
进程控制块PCB
进程的构成元素会被存放在一个叫做“进程控制块”(PCB,Processing Control Block)的数据结构中,进程控制块是操作系统能够支持多进程和多任务的结构。
当操作系统执行进程切换时,会执行两步操作:
- 中断:中断当前CPU处理中的进程
当进程中断时,操作系统会把程序计数器、CPU处理器寄存器(对应进程控制块中的上下文数据)保存到进程控制块中的相应位置,进程状态也会发生变化,可能进入阻塞状态,也可能进入就绪状态。 - 执行:执行下一个进程
当执行下一个进程时,操作系统会按规则将下一个进程设置为运行状态,并加载即将要执行进程的程序上下文数据和程序计数器等。
不管是中断还是执行,进程控制块中的程序计数器、上下文数据、进程状态都会发生变化。
线程Thread
为什么会存在线程这样的概念?它解决了什么样的问题呢?
在早期的操作系统中并没有线程的概念,进程是拥有资源和独立运行的最小单位,也就是程序执行的最小单位。任务调度采用时间片轮转的抢占式调度方式,由于进程是任务调度的最小单位,每个进程由各自独立的一块内存,使得各个进程之间内存地址相互隔离。
随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销越来越大,对多个任务之间上下问切换的效率要求越来越高,已经无法满足越来越复杂的程序的要求了。于是就出现了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。
一个进程可以由一个或多个线程,各个线程之间共享程序的内存空间,也就是所在进程的内存空间。一个标准的线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间和多个线程组成。
理论上来说,在Linux内核中是没有线程这个概念的,只有内核调度实体(Kernal Scheduling Entry,KSE)这个概念。Linux的线程本质上是一种轻量级的进程(Light Weight Process, LWP),是通过clone
克隆系统调用来创建的。由于进程是一种KSE,线程也是一种KSE。所以线程是操作系统调度的最小单元。
进程是程序执行时的一个实例,是程序执行到某种程度的数据结构的汇集。从内核的角度来看,进程的就是分配系统资源的基本单位。
线程是进程的一个执行流,是CPU调度和分派的基本单位。线程是比进程更小的能独立运行的基本单位。简单来说,进程是资源分配的最小单位,线程是程序执行的最小单位。
一个进程会由多个线程组成,线程与同属一个进程的其它线程共享进程所拥有的全部资源。
进程有两个特性部分:资源所有权和调度执行
- 资源所有权:是指进程包含了进程运行所需的内存空间、I/O等资源。
- 调度执行:是指进程执行过程中间的执行路径,或者说程序的指令执行流。
进程的这两个特性部分是可以分开的,分开后拥有资源所有权的通常称为进程,拥有执行代码的可分派部分的被称之为线程或轻量级进程。
线程Thread
有“执行的线索”的意思在其中,而进程Process
在多线程环境中被定义为资源所有者,还会存储进程的进程控制块PCB
。
线程的结构
线程的结构与进程不同,每个线程包含四部分:
- 线程状态:线程的当前状态
- 一个执行栈
- 私有的数据区:用于存放每个线程局部变量的静态存储空间
- 寄存器集:用于存储CPU处理器的一些状态
每个进程都有一个进程控制块和用户地址空间,每个线程都有一个独立的栈和独立的控制块,以及一个独立执行上下文。
线程的执行过程
线程的执行过程与进程不同,每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。线程不能够独立执行,必须依存于进程之中,由于进程提供多个线程执行控制。从逻辑上看,多线程的意义在于一个进程中,有多个执行部分可以同时执行。此时,进程本身不是基本运行单位,而实线程的容器。
线程于进程对比而言,优势在于快,不管是线程的创建还是终止,不管是线程间的切换还是线程间共享数据或通信,它的速度于进程相比都有较大的优势。
单线程与多线程
进程与线程的关系
- 一个线程只能属于一个进程,而一个进程可以拥有多个线程,但至少有一个线程(主线程)。
多线程处理就是允许一个进程中在同一时刻执行多个任务 - 线程是一种轻量级的进程,与进程相比,线程给操作系统带来的维护和管理的负担要小,以就是说线程的开销代价更小。
- 系统资源分配给进程,同一进程中的所有线程共享这个进程的所有资源。
- CPU会分给线程,换句话说,真正在CPU上运行的是线程。
- 线程没有地址空间,线程包含在进程的地址空间中。线程上下文只包含一个堆栈、一个寄存器、一个优先权。线程文本包含在它的进程文本段中,进程拥有的所有资源都属于线程。所有的线程共享进程的内存和资源。同一进程中的多个线程共享代码段(代码和常量)、数据段(全局变量和静态变量)、扩展段(堆存储)。但是每个线程拥有自己的堆段(运行时段,用来存放所有局部变量和临时变量)、寄存器内容。
- 父子进程使用进程间通信机制,同一进程的线程通过读取和写入数据到进程变量来进行通信。
并发与并行
并发
并发又称为共行,是指能够处理多个同时性活动的能力。并发事件之间不一定要同一时刻发生。现代计算机系统可以在同一段时间内以进程的形式,将多个程序加载到存储器中,并借由CPU处理的时分复用,制造出在一个CPU处理器上展现同时运行的错觉。并行
并行是指同时发生的两个并发事件,并行具有并发的含义,并发则不一定会并行。
简单来说,并发和并行的区别在于一个CPU处理器同时处理多个任务和多个CPU处理器或者是多核处理器同时处理多个不同的任务,并发是逻辑上同时发生,而并行则是物理上的同时发生。
高并发
可参见《高并发原理》,后续待补。
多进程与多线程的关系
使用多进程的优势
- 子进程结束后,系统内核会负责回收资源。
- 子进程异常退出时不会导致整个进程退出,父进程还有机会重建流程。
- 一个常驻主进程,只负责任务分发,逻辑会更加清晰。
- 使用多进程更加稳定,另外利用进程间通信IPC也可以实现数据共享。
- 使用多进程共享内存和线程间读写变量是一样的,同样需要加锁,会有同步、死锁问题。
- 使用多进程消息队列,可以采用多个子进程抢夺队列模式,性能很好。
使用多线程得优势
- 线程是在同一个进程内的,可以共享内存变量实现线程间通信。
- 线程比进程更轻量级,开大量进程比线程消耗更多系统资源。
使用多线程存在的问题
- 线程读写变量存在同步问题需要加锁
- 锁得粒度过大时会存在性能问题,可能会导致只有一个线程在运行,其它线程都在等待锁。
- 同时使用多个锁,逻辑会很复杂,一旦某个锁没有被正确释放,可能会发生线程死锁。
- 某个线程发生指明错误会导致整个进程崩溃
并发模型
C10K(Client 10000)问题 - 如何单服同时服务1w个客户端?
由于早期服务器是基于进程/线程模型,每新来一个连接就分配一个进程/线程去处理这个连接,而进程/线程在操作系统中会占用一定的资源的。由于硬件的限制,进程/线程的创建是有瓶颈的。另外,进程/线程的上下文切换也是有成本的,每次调度器调度线程,操作系统都要把线程的各种必要信息如程序计数器、堆栈、寄存器、状态等保存起来。
由于CPU的运算速度远快于I/O操作,互联网应用如Web都是I/O密集型而非计算密集型的。I/O密集型是指计算机CPU大量的时间耗费在等待数据的输入和输出上,而不是计算上。当CPU大部分时间都在等待I/O的时候,大部分计算资源是被浪费掉了的。显然,简单粗暴地开一个进程/线程去处理一个连接时不够的,为了达到高并发,需要重点思考的是I/O策略(模型),在同样的硬件条件下不同的设计会产生很大的差异。
PHP并发模型
PHP并发模型可分为多进程模型和多线程模型,那么PHP使用的哪一种呢?答案是都支持,也就是说PHP支持多线程的模型,在多线程情况下,通常要解决的问题是资源共享和隔离,而PHP自身就是线程安全的。
那么到底是哪一种呢?具体来说就需要看PHP所使用的是那个SAPI,例如在Apache中就可能使用多线程模型也可能使用多进程模型,在PHP-FPM中使用的就是多进程模型。
目前比较推荐的方式是使用PHP-FPM的模型,因为这个模型对PHP来说有诸多优势:
- 内存释放简单
使用多进程模型时,进程可以很容易通过退出的方式来释放内存,由于PHP有很多扩展,稍有不慎就可能导致内存泄漏,PHP-FPM通过进程退出的方式,简单粗暴的解决了问题。 - 容灾能力强
由于PHP扩展或PHP自身可能会出现错误,如果是单进程多线程模型,那么整个PHP就挂掉了,这直接就影响到服务。多进程的话,即使某个进程死掉了也不会影响整个服务。
多进程与多线程各具优势,例如HHVM选择的就是多线程模型,多线程模型最大的好处是数据共享和通信方便,因为在同一个进程空间内,可以直接使用指针。在PHP的opcode cache工具中apc和opcache等使用的是共享内存来共享opcode,而在HHVM中则不需要走共享内存。共享内存有个问题是存储复杂的数据结构不方便,因为指针的问题,多线程情况下C/C++中的数据结构是可以共享的。这对效率提升也是有帮助的。
多进程和多项还有一个明显的模型区别是在处理请求时的逻辑上
- 多进程
由于跨进程时不容易传递fd
客户端唯一连接标识的,在多进程中,通常采用的是在父进程中listen()
,然后给子进程accept()
的方式来实现负载均衡,这样的模型下可能会有惊群的问题。 - 多线程
多线程模型下可以采用一个独立线程接收请求然后派发到各个worker
工作线程中去。
多线程的PHP
PHP从代码级别来讲是不支持多线程操作的,不能像Java、C#等语言一样编写多线程代码。多线程只是代码运行时在同一时刻同时执行多个线程任务,来提高服务器CPU的利用率。PHP是可以以多进程方式执行,例如PHP的进程管理工具PHP-FPM的进程管理机制就是采用了多进程单线程的方式,有效提高了并发访问的响应效率。
PHP从设计之初到流行起来都没有出现明显需要多线程才能解决问题的需求,某些需要多线程的地方也有相应的解决方案和替代方案。而且多线程并不总是比单线程具有更多优势,另外多线程可能会引入其它问题,例如多个线程同时调用一个类中的同一个方法时,可能出现死锁的情况。
简单来说,对于一个客户端的一个页面请求处理的PHP是单线程的,这样做的好处是可以自上而下的编写和理解代码中的业务逻辑。但在PHP是可以同时开启多个线程来处理多个客户端请求的同一个PHP脚本,所以PHP也可以看成是多线程的。
虽然每个PHP脚本的执行是单线程的,但对于Web服务器组件如Apache/Nginx/PHP-FPM是多线程的,因为客户端每次对某个PHP脚本的发起请求时,Web服务器都会创建一个新的进程/线程,用来执行对应的PHP脚本。也就是说,对于一个客户端请求来说,PHP是单线程的,但多个请求间是并发的。
简单来说,PHP本身是不支持多线程,但是Web服务器是支持多线程的,利用Web服务器本身的多线程来处理,从Web服务器多次调用实现多线程的程序。也就是说,可以多人同时访问,这也就是在PHP中实现多线程的基础。
PHP线程安全
如何选择PHP的版本,TS or NTS?
由于Linux/UNIX系统采用多进程的工作方式,而Windows系统采用多线程的工作方式。如果在Windows的IIS下以CGI方式运行PHP会非常慢,因为CGI模式是建立在多进程的基础之上的,而非多线程。所以,在Windows IIS中会把PHP配置成以ISAPI的方式来运行,ISAPI是多线程方式。但存在要给问题,很多PHP扩展时以Linux/UNIX的多进程思想开发的,这些扩展在ISAPI方式运行时出错并搞垮IIS。因此,在IIS下CGI模式才是PHP运行的最安全方式,但CGI对于每个HTTP请求都需要重新记载和卸载整个PHP环境,其消耗是巨大的。
为了兼顾IIS下PHP的效率和安全,Microsoft提出了FastCGI的解决方案,FastCGI可以让PHP的进程重复利用,而不是为每个新请求就重开一个进程。同时FastCGI也可以运行多个进程同时执行,这样即解决了CGI进程模式消耗太大的问题,又利用上了CGI进程模式不存在线程安全问题的优点。
因此,如果PHP使用ISAPI的方式运行就必修使用线程安全(Thread Safe, TS)的版本,如果使用FastCGI模式就没有必要使用线程安全检查,可采用非线程安全(None Thread Safe, NTS)版本以提高效率。
ZTS是什么?
PHP的SAPI多数是单线程环境,比如CGI、CLI、FPM,每个进程只启动一个主线程,这种模式下是不存在线程安全问题的。但也又多线程的环境,如Apache,在这种情况下就需要考虑线程安全问题。因为PHP中又很多全局变量,如EG、CG。如果多个线程共享同一个变量将会发生冲突,所以PHP为多线程的应用模型提供了一个安全机制 - Zend线程安全(Zend Thread Safe,ZTS)。
PHP专门为解决线程安全问题抽象出一个线程安全资源管理器(Thread Safe Resource Manager, TSRM),实现原理:既然共用资源这么困难那么就不共用,各线程不再共享同一份全局变量,而是各自复制一份,使用数据时各个线程各自取自己的副本,互不干扰。