type
status
date
slug
summary
tags
category
icon
password
Property
Apr 15, 2023 03:23 PM
本章将介绍为单个运行进程提供的新抽象:线程(thread)。
经典观点是一个程序只有一个执行点(一个程序计数器,用来存放要执行的指令),但多线程(multi-threaded)程序会有多个执行点(多个程序计数器,每个都用于取指令和执行)。换一个角度来看,每个线程类似于
独立的进程
,只有一点区别:它们共享地址空间,从而能够访问相同的数据。单个线程的状态与进程状态非常类似。
- 线程有一个
程序计数器
(PC),记录程序从哪里获取指令。
- 每个线程有自己的一组
用于计算的寄存器
。所以,如果有两个线程运行在一个处理器上,从运行一个线程(T1)切换到另一个线程(T2)时,必定发生上下文切换
(context switch)。线程之间的上下文切换类似于进程间的上下文切换。
- 我们需要一个或多个
线程控制块
(Thread Control Block,TCB),保存每个线程的状态。
- 与进程相比,线程之间的上下文切换有一点主要区别:地址空间保持不变(即不需要切换当前使用的页表)。
线程和进程之间的另一个主要区别在于栈。在简单的传统进程地址空间模型 [我们现在可以称之为单线程(single-threaded)进程] 中,只有一个栈,通常位于地址空间的底部。
在多线程的进程中,
- 每个线程
独立运行
,当然可以调用各种例程来完成正在执行的任何工作。
每个线程
都有一个栈。在图 中,可以看到两个栈跨越了进程的地址空间。因此,所有位于栈上的变量、参数、返回值和其他放在栈上的东西,将被放置在有时称为线程本地(thread-local)存储的地方,即相关线程的栈。
实例:线程创建
但请注意,这种排序不是唯一可能的顺序。实际上,给定一系列指令,有很多可能的顺序,这取决于调度程序决定在给定时刻运行哪个线程。例如,创建一个线程后,它可能会立即运行
如你所见,线程创建有点像进行函数调用。然而,并不是首先执行函数然后返回给调用者,而是为被调用的例程创建一个新的执行线程,它可以独立于调用者运行,可能在从创建者返回之前运行,但也许会晚得多。(不确定性)
从这个例子中也可以看到,线程让生活变得复杂:已经很难说出什么时候会运行了!
没有并发,计算机也很难理解。遗憾的是,有了并发,情况变得更糟,而且糟糕得多。
为什么更糟糕:共享数据
我们现在可以看看每个工作线程正在尝试做什么:向共享变量计数器添加一个数字,并在循环中执行 1000 万(107)次。因此,预期的最终结果是:20000000。
我们现在编译并运行该程序,观察它的行为。有时候,一切如我们预期的那样:
遗憾的是,即使是在单处理器上运行这段代码,也不一定能获得预期结果。有时会这样:
让我们再试一次,看看我们是否疯了。毕竟,计算机不是应该产生确定的(deterministic)
结果,像教授讲的那样?!也许教授一直在骗你?(大口地吸气)
每次运行不但会产生错误,而且得到不同的结果!有一个大问题:为什么会发生这种情况?
原子性愿望
我们要做的是要求硬件提供一些有用的指令,可以在这些指令上构建一个通用的集合,即所谓的
同步原语
(synchronization primitive)。通过使用这些硬件同步原语,加上操作系统的一些帮助,将能够构建多线程代码,以同步
和受控
的方式访问临界区,从而可靠地产生正确的结果—— 尽管有并发执行的挑战。临界区
(critical section)是访问共享资源的一段代码,资源通常是一个变量或数据结构。
竞态条件
(race condition)出现在多个执行线程大致同时进入临界区时,它们都试图更新共享 的数据结构,导致了令人惊讶的(也许是不希望的)结果。
不确定性
(indeterminate)程序由一个或多个竞态条件组成,程序的输出因运行而异,具体取 决于哪些线程在何时运行。这导致结果不是确定的(deterministic),而我们通常期望计算机系统给出确定的结果。
- 为了避免这些问题,线程应该使用某种
互斥
(mutual exclusion)原语。这样做可以保证只有一 个线程进入临界区,从而避免出现竞态,并产生确定的程序输出。
参考
- 作者:GJJ
- 链接:https://blog.gaojj.cn/article/blog-44
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。