写这篇文章的动机是源于知乎上的一个问题: 指令级并行,线程级并行,数据级并行区别?线程的概念是什么? 这篇博文也是我对这个问题的解读。那个答案是我写的,也可以直接跳转到知乎阅读。

1. 进程与线程

我们先不要关注这个概念,我们先说个例子:小明的一天

  • 上午
  • 下午
  • 晚上

那么对于小明来说,他可以把一天的宏观被分为:上午,下午和晚上三个部分了。

同理, 对一个程序来说,它分为若干个进程。我假设有下面这个程序,把它用来参照小明的这个例子:

  • 进程A –用来处理用户登录
  • 进程B –用来提供服务,比如播放视频什么
  • 进程C –用来和用户交互

打个不恰当的比方,就是上面ABC这三个进程。

进程是操作系统处理一个程序时的抽象。程序是什么?我们没法描述它,而引入进程的概念可以很好的描述一个运行中的程序。

我们再回答刚才的例子,小明的一天是一个宏观的角度。上午下午晚上,这个还是太粗略了。我们根本无法越切知道小一天做了什么。进程(Process) A, B, C,这对程序和操作系统来说,也是太过于粗略了。接下来,我们分的再细。如果我们分的再细致一点,小明的上午继续划分一下,分成是实实在在的小事。 小明的上午:

上午: 洗脸刷牙,吃早饭,听音乐,坐公交
下午:打篮球

那么对一个程序来说,它的线程就像同样发生在小明这些实实在在的小事。线程(对进程而言)= 实实在在的小事。比如一个图片解码的线程,再比如一个单线程的脚本。这些线程大多逻辑简单清晰,做着每一个实实在在的小事,就像一个工人。

题外话: 如果你了解过基于Python开发的异步消息队列任务的Celery框架,这个框架里面确实把执行每个任务的线程–叫做术语工人(worker)。 实际上也正是如此,每个线程就真的像工人一样,勤勤恳恳执行着任务,直到线程的终止。

那么对小明而言:

线程1 = 刷牙洗脸
线程2 = 吃饭
线程3 = 听音乐
线程4 = 坐公交

在这个层面上:

一个程序包含着若干个进程,
一个进程包含着若干个线程。

即:

程序>进程>线程,这样的包含关系

我门来看一下Windows的任务管理器,欣赏一下其中的进程。每个进程都有自己唯一的编号,叫做PID(process id的缩写). 如果让Windows去描述程序,这很难做到,但是如果让它去描述进程,它就可以像图片这样间接的去描述程序了。这就是我前面说的线程是进程一个抽象,其实进程又何不是程序的抽象呢? window任务管理器

再比如:下图我用于编辑文档的某个程序,打开任务管理器看到了5个进程。实际上,这些进程又是由许许多多的线程组成。(只不过普通用户能看得见进程,Windows就像上面的图片这么直观,Linux系统中Top -H命令就可以列出全部的进程。但普通用户和应用程序员看不见线程的,但操作系统看得见线程。) 云笔记的线程

下附更生动形象的图,我当时写这篇文章并没有好好解释这张图,这次更新时解释了下。

Prosess and Thread

这张图来自维基百科,从图片看出的结论就是:进程是线程的在一定时间内有序集合。当然,你还有其它发现吗? 我的发现: 这个进程就只有两个线程,#1#2。但是#1没执行完,就被#2强势抢占了,然后#1只好让#2爽了会。最后#2匆匆完事之后,#1又恢复活力,开始执行了。这里隐含了个术语:上下文切换。 其实还有更晦涩的东西,如果你想了解更多,你可以搜索:线程的五大状态关于这个图片的圆: 空余的部分其实就是系统分配给这个进程的资源,就像朋友聚会时酒店上的饭菜,你只能吃你自己桌子上这么多,不要想别人桌子上的。

线程与进程的总结:

每个线程对操作系统来说,是一个程序的细小部分,就是CPU当前要处理的一件事。线程是基于进程的抽象,一个进程在系统层面上分析,进程它是由大量的可执行的单元组成,叫做线程(Threads) 。我门应记住:程序>进程>线程,这样的包含关系。一个进程不仅仅可能包含这个多个线程,进程还包含着:被操作系统分配的资源空间,比如操作系统在内存中分配给它的栈,数据区域等。这些资源空间被多个线程所共享。比如你和朋友们一次聚餐,这就很像一个进程。桌子上那些饭菜就是资源,你们每个人都是一个线程,每个人每次伸手去夹菜就是在使用这个共享的资源。

下面开始说第二部分。


2. 并发(Concurreny)与并行(Parallelism)的基本概念

接着继续说并发与并行,这个问题还是回到小明的这个例子。

并发 = 小明可以一边玩手机一边看电视。
但是在事实上,他的眼睛在看电视的时候不能看手机,他在看手机没法盯着电视屏幕,他的眼睛飞快在两个屏幕上切换。

这不是真正意义上的同时进行,但又是客观存在同时进行两件事,这叫并发。 所以我们说:并发 = 小明在 “一心两用” (但不是真正意义上同时进行)

并行 = 小明可以一天坐公交一边听音乐。
这两件事同时进行互不干扰,做到真正意义的同步同时进行,这叫并行。

并行 = 小明在 “一心两用”(真正的同时进行) 理解并发和并行的概念后面的就好懂了,务必要理解这个两个不同之处!


3.1线程级并发(Thread-Level Concurrency)

在计算机发展的早期,计算机每次只能运行一个进程,在操作系统(OS)中能拥有资源和独立运行基本单位。因此用户想运行下一个进程必须等上一个进程运行完。比如:你想浏览这个网页就必须关闭上个网页,你想运行这个线程就必须关闭别的。那也太头疼了,是不是?

但这一切在1967年迎来了变化,首次在蓝色巨人IBM公司开发的OS /360系统,首次实现了多线程(Multiprogamming with a Variable Number of Tasks) ,那时候还没有建立线程(Thread)的概念,他们称之为任务(Tasks)。

并发与不并发这有什么不同之处呢? 这是第一次实现了并发(Concurrency),这次并发实现了用户可以运行不同的任务,比如网络服务商可以同时给大量用户提供网页,用户也可以使多个任务并发,比如可以一边上网一边听歌。

对于进程级并发:它不需要CPU的硬件支持,只需要操作系统支持就可以了。对于1967年,那个时候操作系统都是单核的。用小明的话说,这叫“一心(CPU核心)两用”。

d

这里又提到了关于前面提到的甜点上下文切换:操作系统切换进程通过上下文切换(process context switch)实现的,必须下图中的P1和P2的之间的进程切换,左边是cpu的时间线。

对cpu而言,这两个进程其实不是同时进行的;
对用户而言,由于P1和P2切换的速度非常快,所以用户觉得是“是同时进行的”。

IBM只是开发了一个系统就改变这么多吗?当然真的。注意我前面用的词是并发,因为对真正的计算机而言这些任务并不是真正意义上的同步,而是像小明的那个例子。计算机是如何让用户感觉任务同时进行的的呢? 操作系统通过高速的切换不同的线程,仿佛你在同时好多任务一样。借用一个非常好的例子:

就像一个耍杂技的同时把小球扔在空中。

实际上, 玩杂耍的每次手只碰到了一个球,只是看起来就像能同时处理好多球一样的样子

抛球杂技

这样的操作系统我们称之为 单处理系统。(A uniprocessor system is defined as a computer system that has a single central processing unit that is used to execute computer tasks)


3.2 线程级并行(Thread-Level Parallelism)

虽然IBM实现了并发,但这对CPU而言并不是真正意义上的同时进行,只是让它的效率更高而已。因此人们期望能同时处理多个线程,实现线程的并行。自从1980s开始,系统开发人员也就已经发出了多处理系统Multiprpcessor system,由单一的操作系统核心控制的多进程的系统。

与此同时,CPU的晶体管也遵循摩尔定律高速发展。但问题是:依旧不能同时处理多个线程,仅仅提高单核芯片的速度会产生过多热量且无法带来相应的性能改善,也永远不能实现真正意义上的同时处理多任务。还只不过像是是玩杂耍的小球的那个例子。

就比如2000年的英特尔公司的奔腾四,使用了英特尔发布最新的NetBurst架构。按最初的预测,奔腾四在该架构下,最终可以把主频提高到10GHz。但是问题是更高的主频却带来了功耗增加,使其性能上反而还不如早些时推出的产品。

英特尔和AMD也都意识到,当主频接近4GHz时,速度也会遇到自己的极限:那就是单靠主频提升,已经无法明显提升系统整体性能。因此迫切需要一个能支持同时处理2个线程以上的处理器,来提升CPU的瓶颈。

Herb Sutter, 一位著名的C ++专家, 他写过一篇十分经典的文章, 推荐给读者: The Free Lunch Is Over: A Fundamental Turn Toward Concurrency in Software(免费午餐结束了:软件的根本转向并发), 该文章指出微处理器的串行处理速度已达到物理极限, 分析并预测到了CPU并发的到来.

由于2004年左右这种迫切的需求, 由此推动了技术发展. ,线程级并行应运而生。主要由下面两种技术的支撑:

1. 超线程技术(Hyper-Threading,简称HT)

2004年,奔腾4实现了Hyper-Threading.(单核心双线程)

超线程技术实现了单个物理核心同时两个线程,也就是别人常说的虚拟内核数。比如单物理核心实现的双线程,它同时可以处理两个线程,它的物理核心数其实是是1个,通过Hyperthreading技术实现的线程级并行(Thread Lever Parallelism)。至于技术细节的实现,这涉及到高速缓存的知识。

2. 多核技术–物理核心

1. 2005年,英特尔宣布他的第一个双核心 EM64T 处理器,和 Pentium D840(次年发布,双核心双线程,蹩脚双核)
2. 2006年,Core 2(双核心双线程,但不支持HT技术)这大概才算真正意义上单芯片多核心处理器的诞生。(物理双核)
3. 而2006后迎来了Multi-Core Processor多内核处理器时代,而且也伴随着多线程技术,也就常说的几核几线程。核一般指的是物理核心的数目,线程是计算机能同时进行的线程。

简单的画了下Multprocessors的示意图: Multprocessors

回顾一下:在1960s 通过操作系统已经实现了线程级并发,虽然那个时候还没建立线程的概念。

这种方式依赖操作系统的上下文切换,频繁的上下文切换意味损失了CPU的处理效率。但直到2000年随着CPU的技术突破以后,从1960s到2000s年前后花了50多年的时间,才实现和更高级的线程级并行。

比如拥有Core i7的每个核心有带着超线程(HP)技术,每个核可以同时处理2个线程。而i7有4个物理核心,因此能4x2=8线程并行。非常流弊啊!

附上core i7的架构示意图: 4个核

[总结]线程级并行的好处:

  1. 当运行多任务时,它减少了之前的模拟出来的并发,那么用户进行多任务处理时可以运行更多的程序进行并发了。
  2. 它可以使单个程序运行更快。(仅当该程序有大量线程可以并行处理时)

4. 指令集并行 Instruction-Level Parallelism

指令集的是更低层次的概念。计算机处理问题是通过指令实现的,每个指令都是交给CPU执行。

在1978年的 Intel 8086 处理器都只能一次执行单指令。 Intel首次在486芯片中开始使用,原理是: 当指令之间不存在相关时,它们在流水线中是可以重叠起来并行执行

指令集并行基于流水线(Pipeline)技术。这个流水线技术只言片语很难解释清楚,我举个例子可能会好点。

顾客A(表示指令1),顾客B(表示指令2)这A、B两个顾客准备去洗车。而洗车的标准流程如下:

  1. 检查工作
  2. 撤掉脚垫清洗干净
  3. 预洗
  4. 用水枪冲洗车身
  5. 上洗车液
  6. 擦抹。。
  7. 后面略

那么洗车店可以有两个方案:

方案一:
    洗车店让A先洗完,A走完整个洗车流程。
    再让顾客B去洗,B走完整个流程。            

这个效率很低吧!!!!

方案二:

洗车点让A先进流程①,
等A到了流程②,立马让顾客B开始流程①
等A到了流程③,立马让顾客B开始流程②
………依次往复                                      

这个效率很高吧!!!

等等,方案二有点像什么?像车间的流水线,所以叫流水线技术。

指令1和指令2,实际上不同指令,但是正式因为他们可以互不干扰,因此可以这么执行。


5.数据级并行 Multiple-Data parallelism

这是最低的层次了,我地方我也了解不深。

大型机多用于进行科学计算,为了更快的处理数据,它们使用了更多的寄存器,这样可以同时可以处理更多的操作数。单一指令运行多个操作数并行计算。这里涉及到操作数的概念,如果你有汇编的基础应该会很好理解。

我们考虑下面这个计算式子:(a+b)*(c+d)

该计算过程被分解为三步:

1.   e = a +b 
2.   f  = c +d
3.   m =  e * f

要知道:早期的计算机一次只能处理一条指令,它要先算步骤1(加法操作),再算步骤2(加法操作),最后算3(乘法操作)。需要三步(花费三个指令)得到答案。

但是我们观察到: 3的结果依赖于1和2,而1和2都单纯的加法操作,所以开始想办法让1和2同时计算,那么CPU只要两步得到答案,步骤1和2一次算出来的结果,直接进行乘法运算。

它运用了SIMD(Single -Instruction ,Multple -Data)单指令多数据流技术。一个指令执行了(a,b,c,d) 4个操作数。SIMD指令集可以提供更快的图像,声音,视频数据等运行速度。