读书笔记
🛬《Operating System:Three Easy Pieces》第二十六章 并发:介绍
00 分钟
2023-1-5
2023-4-15
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)存储的地方,即相关线程的栈。
notion image

实例:线程创建

notion image
但请注意,这种排序不是唯一可能的顺序。实际上,给定一系列指令,有很多可能的顺序,这取决于调度程序决定在给定时刻运行哪个线程。例如,创建一个线程后,它可能会立即运行
notion image
 
notion image
如你所见,线程创建有点像进行函数调用。然而,并不是首先执行函数然后返回给调用者,而是为被调用的例程创建一个新的执行线程,它可以独立于调用者运行,可能在从创建者返回之前运行,但也许会晚得多。(不确定性) 从这个例子中也可以看到,线程让生活变得复杂:已经很难说出什么时候会运行了! 没有并发,计算机也很难理解。遗憾的是,有了并发,情况变得更糟,而且糟糕得多。

为什么更糟糕:共享数据

我们现在可以看看每个工作线程正在尝试做什么:向共享变量计数器添加一个数字,并在循环中执行 1000 万(107)次。因此,预期的最终结果是:20000000。 我们现在编译并运行该程序,观察它的行为。有时候,一切如我们预期的那样:
遗憾的是,即使是在单处理器上运行这段代码,也不一定能获得预期结果。有时会这样:
让我们再试一次,看看我们是否疯了。毕竟,计算机不是应该产生确定的(deterministic) 结果,像教授讲的那样?!也许教授一直在骗你?(大口地吸气)
每次运行不但会产生错误,而且得到不同的结果!有一个大问题:为什么会发生这种情况?

原子性愿望

我们要做的是要求硬件提供一些有用的指令,可以在这些指令上构建一个通用的集合,即所谓的同步原语(synchronization primitive)。通过使用这些硬件同步原语,加上操作系统的一些帮助,将能够构建多线程代码,以同步受控的方式访问临界区,从而可靠地产生正确的结果—— 尽管有并发执行的挑战。
  • 临界区(critical section)是访问共享资源的一段代码,资源通常是一个变量或数据结构。
  • 竞态条件(race condition)出现在多个执行线程大致同时进入临界区时,它们都试图更新共享 的数据结构,导致了令人惊讶的(也许是不希望的)结果。
  • 不确定性(indeterminate)程序由一个或多个竞态条件组成,程序的输出因运行而异,具体取 决于哪些线程在何时运行。这导致结果不是确定的(deterministic),而我们通常期望计算机系统给出确定的结果。
  • 为了避免这些问题,线程应该使用某种互斥(mutual exclusion)原语。这样做可以保证只有一 个线程进入临界区,从而避免出现竞态,并产生确定的程序输出。

参考


评论
Loading...