本文由作者:auxten 授权发布
链接:http://zhuanlan.zhihu.com/auxten/20167077
著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。
多进程,多线程编程
系统程序员、运维开发程序员在面试的时候经常会被问及一个常见问题:
进程是什么,线程是什么,进程和线程有什么区别?
不得不承认,这么多年了。这个问题依旧是个很难以招架的问题,简单地说:
进程和线程有很多类似的性质,他们都可以被 CPU 作为一个单元进行调度,他们都拥有自己独立的栈(stack)等等。因此线程也被称作 LWP(Lightweight Process,轻量级进程);对应的进程也可以被称作为 HWP(Heavyweight Process,重量级进程),从线程的角度看,进程就是只有一个线程的进程。如果一个进程有多个线程,那么它就能同时(只有在SMT系统中才有可能真正的“同时”)执行多个任务。他们的异同可以从以下几个角度来论述:
调度
在传统的计算机操作系统中,CPU 调度的基本单位是进程。后来操作系统普遍引入了线程的概念,线程成为了 CPU 调度的基本单位,进程只能作为资源拥有的基本单位。
并行
由于线程的引入,原先一个进程只能有一个并发,现在一个进程可以有多个线程并行执行。早期的很多 HTTP server 都是通过线程来解决服务器的并发,比起之前用 fork 子进程来处理并发效率有了数倍的提升。这一切都得益于线程可以用进程更低的代价实现并发。
共享
一般来说 Linux 线程会继承或共享如下资源:
进程代码段,如下图所示
- 进程的公有数据段内存(利用这些共享的数据,线程很容易的实现相互之间的通讯)
- 进程打开的 FD(File Descriptor,文件描述符)
- 信号的处理器
- 进程用户 ID(UID)与进程组 ID(PGID)
隔离
Linux 的线程会独立拥有如下资源(非共享):
- 线程 ID,在 Linux 中线程和进程共享 ID 空间,在 UNIX 系统中线程的 ID 是和进程 ID 不同层面的概念
- 寄存器的值,这其实就是线程能作为独立调度单元的最必要的保证
- 线程的栈,这是线程能并行运行的保证
- 优先级,Linux 的系统设计使得线程和进程除了在某些资源的共享&隔离有差异之外, 几乎是一视同仁的,所以他们可以有不同的 priority。
多进程多线程的产生,在Linux系统中的地位
Linux 由于从一开始的定位就是一个多任务操作系统,从 Linus Torvalds 写出第一个版本的时候就有了进程的概念。比如我们耳熟能详的 init 进程的 pid 就是1。
线程的产生是为了解决并发问题,线程的定位也是轻量级的进程。
Linux 内核在 2.6 版本之前都是没有线程的概念的,任务的最小调度单元都是进程。但 Linux 在设计的时候就为线程的引入创造了良好的条件,Linux 中著名的启动新进程系统调用 fork 就是通过内核调用 clone 实现的拷贝地址空间等资源。Linux 通过改变内核调用 clone 的参数就很简单的创造出了线程。所以,从现代操作系统内核的调度的角度来说,进程和线程的差异微乎其微。
但不幸的是 Linux 早期的内核版本通过细微修改增加的线程机制和 POSIX 标准并不完全兼容,特别是信号处理、调度、跨进程同步的行为上。
为了推进 Linux Threads 和 POSIX 标准的统一,两拨人做了很多的努力:IBM 牵头的 NGPT (Next Generation POSIX Threads)和红帽(Red Hat)主推的 NPTL(Native POSIX Thread Library)。这场竞争以 NPTL 的胜利告终,NPTL 的用户态 API 就是我们现在常用的 Pthread 系列 API。这场 Red Hat 战胜 IBM 的战争也基本确立了前者在 Linux 界扛把子的地位。
在 NPTL 成为 Linux 的 POSIX 事实标准之前,以 FreeBSD 为首的 UNIX 系统保持了对 Linux 的性能优势。这也就导致了很多历史比较老的公司当年系统都用的 FreeBSD 而不是 Linux。
为什么不能一味的开线程解决并发问题
上面说到,线程的出现是为了解决 Linux 系统面临的日益增多的,并发编程的需求。
但就像我们这一小节的标题讲的一样:“不能一味的开线程解决并发问题”。
这是由于上下文切换(Context Switch)的代价:当计算机还处于单核时代的时候 ,就已经有了多任务操作系统。但单核的 CPU在同一时刻只能运行一个进程的一个指令。 为了达到用户想要的“多任务”同时运行(比如,我在敲这段文字的时候,后台还在运行着 iTunes 播放音乐,还有一个迅雷在我的虚拟机里运行)。Linux 通过把 CPU 的时间切成 大小不等的时间片,通过内核的调度算法,让他们轮流来占用宝贵的 CPU 资源。由于切换的 时间片的大小一般都是微秒,所以在我们人类看来,计算机就在运行“多任务”。
上下文切换(Context Switch)
一个程序如果运行到了他的时间片结束还没有完成他的工作,那么,对不起,请把你需要 保存的数据(通常是一些 CPU 寄存器的数值)存储在内存中,然后排队去吧。
什么,稍等?NO,NO,NO 这不是一个用户态的进程能够和内核讨价还价的。
保存这个现场是需要一定的代价的,更严重的是,这将极大的影响 CPU 的分支预测, 影响系统性能。所以上下文切换(Context Switch)是我们要极力避免的。
进程或者线程开的多了,就会导致上下文切换(Context Switch)增多,严重影响 系统性能。
所以:“不能一味的开线程解决并发问题”。
协程(coroutine)简介
精辟的说,协程就是用户自己在进程中控制多任务的栈,尽可能的不让进程由于外部中断或者 IO 的等待丧失 CPU 调度的时间片,从而在进程内部实现并发。
为了缓解,处理高并发的连接,Linux 在很早的时候就引入了协程,来缓解上下文切换造成 的性能损失,在某种程度上实现异步编程。但由于协程的编程太过于晦涩难懂,所以即便是 协程在线程之前更早的被引入 Linux 内核,也始终没有流行起来。
下面是 wikipedia 对于协程的一段描述,大家可以参考一下:
到2003年,很多最流行的编程语言,包括 C 和他的后继,都未在语言内或其标准库中直接支持协程。(这在很大程度上是受基于堆栈的子例程实现的限制)。
有些情况下,使用协程的实现策略显得很自然,但是此环境下却不能使用协程。典型的解决方法是创建一个子例程,它用布尔标志的集合以及其他状态变量在调用之间维护内部状态。代码中基于这些状态变量的值的条件语句产生出不同的执行路径及后继的函数调用。另一种典型的解决方案是用一个庞大而复杂的switch语句实现一个显式状态机。这种实现理解和维护起来都很困难。
在当今的主流编程环境里,线程是协程的合适的替代者,线程提供了用来管理“同时”执行的代码段实时交互的功能。因为要解决大量困难的问题,线程包括了许多强大和复杂的功能并导致了困难的学习曲线。当需要的只是一个协程时,使用线程就过于技巧了。然而——不像其他的替代者——在支持 C 的环境中,线程也是广泛有效的,对很多程序员也比较熟悉,并被很好地实现,文档化和支持。在 POSIX 里有一个标准的良定义的线程实现 pthread.
但近些年来,golang 的努力,似乎又让这个古老的机制有了复苏的迹象。
程序运行时的内存布局
首先我们需要了解一个基础知识:程序运行时的内存,也就是我们在用户态能看到的内存地址,都不是物理内存中的地址。现代操作系统都会在物理内存上做一层内存映射(memory mapping)。所以如下图,每个进程的内存空间都是独立的,0x8000 这种地址在物理地址中其实是不一样的地址。
如上图,每个线程,都有自己独立的“栈”、“寄存器”、“线程计数器”。每个进程可以有多个线程。 同一个进程里的线程都可以共享内存空间。
多进程和多线程的选用场景
在 Linux 系统编程中,多进程和多线程都有自己的用武之地。
多数情况他们的选用是按照他们的特性,其中最重要的特性就是上面提过的“共享”、“隔离”。
我们举个例子来说吧:
我们所熟知的 memcached,是个典型多线程编程。之所以他是多线程,而不是多进程 主要的一个原因在于,memcached 的多个线程需要共享内存中的 Key-Value 数据。所以多线程 是一个必然的选择。
然后就是大名鼎鼎的 Nginx,是个典型的多进程编程。由于 Nginx 所要处理的 HTTP 请求都是 比较独立的,没有太多需要共享的数据。更重要的是 Nginx 需要支持“不停服务重启 server ”这一特性 这个功能也是这能在多进程框架下才能实现的。
所以,一个结论就是:到底是多进程好,还是多线程需要根据业务场景来分析选择。
Python 的 GIL
GIL是Global Interpreter Lock 的缩写。顾名思义,就是 Python 解释器的一个全局锁。 它的产生是由于 Python 解释器在实现的时候作者为了“糙快猛”地实现出一个原型引入了很多 全局变量,由于全局变量的存在就要加锁,为了加锁那干脆一不做二不休,加个全局锁吧…… 嗯,当时情况应该就是这样的。
后来 Python 逐渐流行起来,很多模块的作者一方面也是为了简化问题,另一方面也是由于 Python 解释器 本身就有 GIL,很多模块自己也肆无忌惮地引入了很多全局变量。
从此 Python 的 GIL 就走上了一条不归路,对 Python 程序员的影响就是,Python 的多线程在同一时刻 只能有一个线程在运行。多线程情况下就是线程不停地在抢锁,抢得头破血流。
关于 Python 的 GIL 及其造成的性能影响,这篇 David Beazley 的这篇文章做了非常深刻的论述:
这部分的内容我们将在课上做更加深入的论述。
我们可以得到的结论:
- Python 的多线程对 CPU 密集型是反作用,对 IO 密集型可以采用
- Python 多进程能充分利用多核 CPU
51Reboot Python 零基础课程6折优惠,有需要的朋友咨询wechat:17812796384
具体的课程的基础部分内容跟多的请详细了解