所以这些 CS 导论就该明确方向的系列问题怎么还那么经……
还是得从基础概念入手。
https://stackoverflow.com/a/51759235/2307646里面引用的 Robert Harper 的博客文章看来又能点进去了,那就不重复为什么需要这样明确之类的细节问题了。
就说重点,补课,再解释顶楼的问题:
1.并行(parallelism) 和并发(concurrency) 都是计算(或者说,表达计算的程序片段)的性质,但两者本质上是两回事。
更进一步地,两者可以是正交的:程序中并行和并发程度的多少之间也没有必然联系。
注意,计算是抽象的。计算的实现,包括“同时”之类的物理属性,和这些性质没有直接关系。
2.并行是关于管理程序中确定性的(deterministic) 计算之间的依赖(dependency) 使它们整体具有更好的渐进效率(asymptotic efficiency) 的性质;并发是关于处理程序的各种非确定性的组合(non-deterministic composition) 使之能响应各种不保证可准确预知的时机输入的性质。
一定程度上,不考虑依赖之间的动态变化,并行描述的是程序中的计算之间的静态关系,而并发可以描述不确定的动态关系。
3.并行的对立面是串行(seriality) ,指一系列确定的计算明确具有链式的依赖;并发的对立面是序列(sequentiality) ,指不确定的计算的某个子集之间,其顺序被约束了。
约束计算的操作称为调度(scheduling) ,其中可能包含对计算资源的分派(dispatch) 。
4.进程(process) 或者线程(thread) 为了实现是占用特定的计算资源完成的带目的计算(或者说,任务(task) )引入的抽象,是程序的动态映像和为了实现计算分配的其它资源的集合。
两者的区别传统上和资源的具体集合有关,这里不是重点,以下都以可调度的线程代表。
作为组成其它程序的组件,它们可具有并行和并发的性质,分别通过竞争性调度机制的具有非确定性和调度中分派计算资源的目标不同的事实而体现。一般地,决定如何调度的逻辑是在进程或者线程之外的。程序通过这样的抽象的表达,只是表明计算已被拆分成为不同的待调度的组件,并不能保证已经具有并行或者并发的性质。
5.使用进程或者线程这样的抽象只是为了便于实现这种特定的可调度的计算表达。
这不排除其它手段实现并发和并行的手段,因为一般情形下,调度自身的效果被不确定性涵盖而不是计算的作用(effect) ,不被要求对用户可见。最简单地,一个表达式在计算时若不要求子表达式计算之间的相对顺序,也没有影响计算结果的依赖限制它们之间的顺序(以保证不违反计算的因果性语义),那么它们的计算原则上就是并发的——用户没法预知确定的计算如何发生,也没法保证确定这里是不是需要插入一个线程或者其它可调度的实体来实现并发;而若使用确定的最优化规则(例如某个指令集允许的指令级并行规则),它们之间也可以是并行的,用户可以查看目标代码等方式来确认这里的计算被分派到不同的设备上同时实现以取得比串行计算更好的效率。
6.为了解决共享资源的竞争引起破坏计算的目的非预期的非确定性,可能需要引入同步。
同步操作和调度一样约束计算的顺序,因此可能有相互作用,例如增加不必要(程序中不要求表达)的隐含依赖而减小程序的可并行部分,并最终实际减小并行程度。
就 LZ 的例子,GIL 是一种粗粒度的内部调度机制,在极端情况下把并行语义的程序串行化。这个意义上它是不并发的,但这是偶然情况,不是实现系统总是对用户可见的整体的性质,更不是语言要求的性质( PyPy 就不用 GIL )。
7.多线程中的线程在不同的上下文中不严格地是一回事。
一般地,高级语言使用表达式的求值引入计算的作用。对大多数高级语言来讲,默认情形表达的计算遵循同样的顺序规则,最常见的如指令式语言按程序的字面顺序(literal order) 。这个顺序整体上约束了确定性计算(尽管按上面的讨论,像子表达式这样的计算仍然可以是非确定性的)。
为了更明确地表达可被程序操作的非确定性计算,同时不修改默认情形的计算顺序的语义,按原有规则的计算任务整体被抽象为一个执行线程(thread of execution) 。多线程环境即指语言允许程序蕴含多个执行线程,每个线程内都适用默认计算规则;而线程间是没有类似的计算顺序规则限制,需要再另行约定共享计算资源和同步操作以明确线程之间计算的互操作。
这个意义下,“同时”或者竞争调度不是多线程的关键。尽管语言中的执行线程允许并发,但不可见的调度实现不需要提供计算资源是否在物理上(同时)被重叠利用的保证。极端地,调度可以完全放弃对线程的资源分派,无限等待线程自发完成计算,此时线程不再抢占(preemptive) ,调度退化为协作式多任务(cooperative multitasking) 的实现。
但大多数情形在语言引入执行线程的目的不止是为了允许在程序中表达非确定性,还同时作为提供显式复用计算资源的特性(乃至显式地并行),所以不太会有这种调度内部直接放弃非确定性退化成平凡情况,执行线程在这里也就和一般意义上被实现调度的线程不加区分,统称为线程。
存在 GIL 虽然更接近退化的情况,只要不改变被实现的语言具有多个执行线程,且语言不能绕过执行线程可见地调度线程,那么整个实现就是一个多线程环境。
8.在硬件实现中,因为在外部看来计算资源整体的占用是很大程度上能预期上限,习惯上把这部分实现可用的资源(特别地,一个 CPU 核)作为一个物理线程。相对地,一个能被软件分辨并调度的线程是逻辑线程。
同时多线程(SMT) 专指这里的复用物理处理器核的硬件资源,以使一个物理处理器支持超过一个逻辑线程的手段。在软件看来,一个线程逻辑上对应一个处理器,因此一个物理核心对应多个逻辑处理器。
不支持 SMT 且只有一个物理核心的实现仍然可以通过分时复用实现多线程。这和软件的语言实现的情况类似,因为物理核心原则上对软件不可见,所以软件的意义上这就是“真正的”多线程。
9.硬件相比软件的特殊性是可提供物理同时的多任务支持:一个处理器 package 可以有多个物理核,如同一个抽象机进程中支持多个执行线程,这似乎符合最朴素的抽象。
但这和并行或者多任务根本上都没有实现以上的关系,因为如最开始提到的,“物理同时”从来就不是考虑这些抽象时必备的要素,仅仅是方便理解而提到罢了。
而且,实际的处理器内部也不可能完全重叠地利用计算时间,最显然地,要求时钟同步来避免非预期的不确定性。
此外,单一核仍然能通过异步中断能实现物理上同时的多任务,这和多线程也没有直接的关系。