再谈:线程和进程

date
Jul 22, 2023
slug
re-thread-and-process
status
Published
tags
Program
type
Post
pin
0
summary
这是个老生常谈的问题,但我希望能讲得更明白一些。
大家好,本文我想来谈谈进程线程的区别,即 process thread 的区别。
这是一个老生常谈的问题,可能每个程序员都会在面试或考试中被问到。但是在我刚学习这些概念时,在网上查找资料时,我发现大部分讲解并不够详细,或者没有说到关键点。所以我想用我的理解来谈谈这个问题,并希望能清晰地表达。

一、线程

1.1 线程是什么

首先,我们从线程开始说起,线程是什么呢?顾名思义,线程像一条线一样,指的是一个按顺序运行的程序逻辑。比如a+b。它是线性的逻辑,先输入 a,输入 b,然后 a 加 b,然后输出,顺序连接在一起。在汇编语言中也清晰地表现出这种顺序执行,这就是逻辑上说的一个”线”程。

1.2 操作系统中的线程切换

线程 (thread) 指的是一段线性执行的程序,这是逻辑意义上的线程。
从 CPU 的角度或操作系统的角度来看,操作系统是一个运行在 CPU 上的程序。我们假设 CPU 是一个朴素的 CPU,按照取指令、译码、运行、读写内存、写回寄存器的顺序工作,一次执行一条指令,按程序员给的指令序列顺序运行。
当操作系统启动时,它有一个主线程,即内核线程,经过初始化后进入调度器循环。需要注意的是,内核线程启动、初始化到进入调度器的过程是线性的,就像我们编写的普通程序一样,一行接一行,我们可以称之为内核线程。进入调度器循环之后,这时就会出现第二个线程,即用户进程的线程。这里说“进程”可能会引起一些歧义,我的意思是指用户编写的应用程序的线程。
用户编写的应用程序,比如a+b,编译为一系列指令,这些指令是一段连续的逻辑,与内核的逻辑无关。所以用户线程和内核线程是两个不同的逻辑,这是两个线程。
调度器循环中,内核线程会切换到用户线程。在线程切换时,系统会保存内核线程的寄存器状态,并切换到用户线程的逻辑。通过这个线程切换,调度器可以运行用户编写的线程,在内核的线性逻辑和用户程序的线性逻辑之间切换。这是操作系统内核的功能。
在 CPU 的角度,实际上它是按顺序执行的,但它先执行内核线程,然后内核会将自己的线程切换到用户线程,实现了线程切换。所以,从CPU和操作系统的角度来看,线程是这样一个东西。

1.3 多线程并行

而从顶层,从用户编写多线程程序的角度来看,多线程的目的一般是为了并行计算,操作系统可以将这些线程分配给多个CPU同时运行。如果我现在有两个单线程的CPU,操作系统就可以将这两个线程分别放到两个CPU上运行。
虽然它们使用不同的寄存器,但可以共享同一个内存空间,例如用户编写的变量存储在内存的某个位置,现在有两个线程都要读取这个内存空间,所以它们使用的是同一套地址空间。它们可以在两个不同的 CPU 上运行(假设这两个 CPU 都是单线程的原始 CPU。我们知道现代的 CPU 有许多的技术,一个 CPU 其实可以同时运行多个线程)。

二、进程

2.1 进程是什么

以上就是线程的概念。那么进程 (process) 是什么呢?进程是操作系统提供给每个应用程序的一个隔离环境,类似于一个虚拟机,是抽象的一台计算机。为什么说是一个虚拟机?因为它给每个应用程序创建了一种独立使用一台计算机的感觉,而且使用的是一台很朴素的计算机,只有顺序执行的 CPU 和一块独立使用的内存。
进程的本质是为每个应用程序提供一个隔离环境,类似独占一台计算机。一个进程至少有一个线程,对吧?至少有一段连续的逻辑,可能可以写多线程,但至少有一个线程。
一段代码,按照图灵机的说法,被存储在内存的某个位置。我们暂且不考虑物理内存是怎样的,我们要讨论的是进程为应用程序提供的虚拟机的内存
这个内存比较朴素,从一个固定的地址开始是一段连续的代码,然后从另一个固定地址开始向上是堆,又一个地址开始向下是栈,也就是堆栈的概念。进程拥有这样一段内存。作为一个程序员,无论是写高级语言还是汇编语言,我们都可以将自己视为在这台虚拟机上编写代码,并在这台虚拟机上运行我们的代码。所以当我们讨论内存中的堆栈时,我们说的是这个虚拟空间上的堆栈。

2.2 操作系统如何实现进程

这就是进程,进程如何模拟独占的CPU和内存呢?这是操作系统的基本功能之一。
一是分配 CPU 的调度机制。调度机制是通过(简单来说)尽量均匀地分配 CPU,使每个应用程序都能运行,就像它拥有独占的 CPU 一样。等到一个进程的使用时间片到期了,硬件就触发一个定时器中断,先保存寄存器的数据,然后把所有计算用的寄存器切换到调度器上,调度器载入下一个进程的寄存器数据,最后把这些寄存器切换到下一个进程。这个过程是程序员不可见的,一个应用程序被“打晕”然后又被唤醒,它看到的寄存器是一样的,是内核给它维护了“这段时间无事发生”的“幻觉”,因此说这个进程看到的是一颗它独占使用、连续使用的 CPU。
二是分配内存的虚拟内存机制。对于虚拟内存来说,它主要通过页表映射的方式来操作。通过一定的规则,例如把物理内存切成片段,然后从中提取几个片段,并将这些片段组合成连续的内存,形成虚拟内存,供进程使用。这个物理内存和虚拟内存之间有一个地址映射的关系,由内核维护的。
因此,进程就像拥有独立处理器和独立内存的计算机,使得应用程序或者程序员可以认为自己是在一台独立计算机上编写和运行程序。这就是进程,它本质上是对计算机的一种模拟或者抽象。

三、线程和进程的对比

3.1 多线程、多进程和并行的关系

多进程指的是多个应用程序同时运行。无论是多线程还是多进程,在现代计算机上都可以并行运行。但并不是说它们都一定是并行的。
例如,如果我只有一颗单线程的CPU,那无论我写多少个线程,它们都无法同时使用这个CPU,必须轮流切换 CPU 的使用权。这个情况其实就是协程,下文再述。
或者,即使我有多颗物理上的优秀CPU,但我的操作系统调度器决定一次只给一个进程分配所有的 CPU 资源,那在这种情况下,多进程也不能并行运行。
虽然这听起来很奇怪,但理论上是可能的。并行和多进程、多线程并不是必然的联系。
通常来说,多线程的目的是实现并行计算,但多进程的目的不一定是。多进程的本质是为了给多个应用程序提供隔离的环境。因此,在一个进程里使用多线程使用的是同一套上下文,而多进程使用的是不同的上下文和内存空间。它们其实是本质不同的:含意不同、目的不同、实现方式也不同。

3.2 线程不是小型的进程

我们还常常听到这样的说法,一个进程中有多个线程。这种说法会让人误以为,线程是一个低级的进程,线程是一个微型的进程。但这种感觉是错误的,因为线程和进程是两个不同的概念。线程并不是从进程派生出来的,正如上文所述,它们是两个不同维度的概念。

四、协程

最后,还有一种称为协程的东西。协程在概念上也就是文初所述的、一个线性逻辑。
但它不像多线程一样支持并行。为什么不并行?因为并行可能导致数据竞争的问题。
所以多协程的含意就是说,在一个线程上进行多条逻辑线的运行,就像上文说的,在一颗单线程的 CPU 上做线程切换一样。它物理上是顺序执行的,不存在并行的情况,但逻辑上是有多条逻辑线。

以上就是我所理解的,关于多进程、多线程和协程的区别。谢谢阅读。

补充

5.1 多进程其实是一种并发需求

多进程其实是一种并发而非并行。例如一个单线程 CPU 上要跑一个支持多进程的操作系统,这其实是一个并发而不并行的需求。

5.2 和 ChatGPT 整理一遍

来自 handongxue

5.3 从功能角度看待线程和协程的区别

来自 Ekstasis
“协程与线程的概念是很紧密的,它就是不能并行的多线程。”这句话似乎不是很准确。
通常情况下协程“coroutine”应该是指 cooperative multitasking (协作式多任务)的实现,而所谓线程就一般指的是 preemptive multitasking (抢占式多任务)的实现了。两种并发模型都为我们提供了独立执行流的抽象,但他们所提供抽象的不同之处在于“如何进行任务间的切换”。
单个CPU时,抢占式多任务中任务切换是由相应硬件机制强行终止一个任务再换上另一个任务来实现的;而协作式多任务中是任务自己通过yield,await等特殊操作来主动交出CPU控制权的。
抢占式多任务很容易扩展到多CPU上,因此也能很好得利用并行能力了;但协作式多任务由于只提供了主动交出控制权这样的并发模型,所以也就难以利用多CPU的并行能力了;
那么由上面所说的 “线程->抢占式->并行,“协程->协作式->无法并行” 两个关系来看,难道“多协程是不能并行的多线程”也对?但是!线程的定义中并没有强调“并行”,因此也有不能并行的线程模型!比如由于CPython臭名昭著的GIL(Global Interpreter Lock),每个解释器线程进入指令分派循环时都需要获取这把锁,因此CPython提供的并发模型就是“线程->抢占式->无法并行,“协程->协作式->无法并行”了,,
当然这些话有些咬文嚼字之嫌,,,毕竟这些概念本来也就不是well-defined的。不过“协作”“抢占”两个概念以及“CPython”的实现这些东西我感觉还是挺有意思的。
 

© Beautyyu言醴 2022 - 2024