C++,python,热爱算法和机器学习
全部博文(1214)
分类: 架构设计与优化
2016-07-02 20:31:02
系统程序员、运维开发程序员在面试的时候经常会被问及一个常见问题:
进程是什么,线程是什么,进程和线程有什么区别?
不得不承认,这么多年了。这个问题依旧是个很难以招架的问题,简单地说:
进程和线程有很多类似的性质,他们都可以被CPU作为一个单元进行调度,他们都拥有自己独立的栈(stack)等等。因此线程也被称作LWP(Lightweight Process,轻量级进程);对应的进程也可以被称作为HWP(Heavyweight Process,重量级进程),从线程的角度看,进程就是只有一个线程的进程。如果一个进程有多个线程,那么它就能同时(只有在SMT系统中才有可能真正的“同时”)执行多个任务。他们的异同可以从以下几个角度来论述:
在传统的计算机操作系统中,CPU调度的基本单位是进程。后来操作系统普遍引入了线程的概念,线程成为了CPU调度的基本单位,进程只能作为资源拥有的基本单位。
由于线程的引入,原先一个进程只能有一个并发,现在一个进程可以有多个线程并行执行。早期的很多HTTP server都是通过线程来解决服务器的并发,比起之前用fork子进程来处理并发效率有了数倍的提升。这一切都得益于线程可以用进程更低的代价实现并发。
一般来说Linux线程会继承或共享如下资源:
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资源。由于切换的 时间片的大小一般都是微秒,所以在我们人类看来,计算机就在运行“多任务”。
一个程序如果运行到了他的时间片结束还没有完成他的工作,那么,对不起,请把你需要 保存的数据(通常是一些CPU寄存器的数值)存储在内存中,然后排队去吧。
什么,稍等?NO,NO,NO 这不是一个用户态的进程能够和内核讨价还价的。
保存这个现场是需要一定的代价的,更严重的是,这将极大的影响CPU的分支预测, 影响系统性能。所以上下文切换(Context Switch)是我们要极力避免的。
进程或者线程开的多了,就会导致上下文切换(Context Switch)增多,严重影响 系统性能。
所以:“不能一味的开线程解决并发问题”。
精辟的说,协程就是用户自己在进程中控制多任务的栈,尽可能的不让进程由于外部中断或者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”这一特性 这个功能也是这能在多进程框架下才能实现的。
所以,一个结论就是:到底是多进程好,还是多线程需要根据业务场景来分析选择。
GIL是Global Interpreter Lock的缩写。顾名思义,就是Python解释器的一个全局锁。 它的产生是由于Python解释器在实现的时候作者为了“糙快猛”地实现出一个原型引入了很多 全局变量,由于全局变量的存在就要加锁,为了加锁那干脆一不做二不休,加个全局锁吧…… 嗯,当时情况应该就是这样的。
后来Python逐渐流行起来,很多模块的作者一方面也是为了简化问题,另一方面也是由于Python解释器 本身就有GIL,很多模块自己也肆无忌惮地引入了很多全局变量。
从此Python的GIL就走上了一条不归路,对Python程序员的影响就是,Python的多线程在同一时刻 只能有一个线程在运行。多线程情况下就是线程不停地在抢锁,抢得头破血流。
关于Python的GIL及其造成的性能影响,这篇David Beazley的这篇文章做了非常深刻的论述:
这部分的内容我们将在课上做更加深入的论述。
我们可以得到的结论: