Thread

Thread

线程概述

Intro

为使程序能并发执行,系统必须进行以下的一系列操作:创建进程 撤消进程 进程切换
由于进程是一个资源的拥有者,因此在创建、撤销和切换中,系统必须为此付出较大的时间和空间的开销。
如何使多个程序更好的并发执行,同时又能减少系统开销?

进程的概念体现出两个特点

  • 资源所有权:一个进程包括一个保存进程映像的虚地址空间,并且随时分配对资源的控制或所有权,包括内存、I/O 通道、I/O 设备、文件等。
  • 调度/执行:进程是被操作系统调度的实体。

调度和分派的部分通常称为线程或轻型进程(lightweight process),而资源所有权的部分通常称为进程

线程具有许多传统进程所具有的特征,所以又称为轻型进程(Light-Weight Process) ,相应地把传统进程称为重型进程(Heavy-Weight Process),传统进程相当于只有一个线程的任务。
在引入了线程的操作系统中,通常一个进程都拥有若干个线程,至少也有一个线程。

Create Thread vs Create Process

History

  • 60 年代:提出进程(Process)概念,在 OS 中一直都是以进程作为能拥有资源和独立运行的基本单位的。
  • 80 年代中期:提出线程(Thread)概念,线程是比进程更小的能独立运行的基本单位,目的是提高系统内程序并发执行的程度,进一步提高系统的吞吐量。
  • 90 年代:多处理机系统得到迅速发展,线程能比进程更好地提高程序的并行执行程度,充分发挥多处理机的优越性。

线程的共享问题

进程内的所有线程共享进程的很多资源,而这种共享又带来了同步问题

1
2
3
4
5
6
7
线程间共享                 线程私有
进程指令			       线程ID
全局变量		        寄存器集合(包括PC和栈指针)
打开的文件	           栈(用于存放局部变量)
信号处理程序                信号掩码
当前工作目录                优先级
用户ID

线程的互斥问题

对全局变量进行访问的基本步骤

  • 将内存单元中的数据读入寄存器
  • 对寄存器中的值进行运算
  • 将寄存器中的值写回内存单元

线程的属性

  • 轻型实体 线程自己基本不拥有系统资源,只拥有少量必不可少的资源:TCB,程序计数器、一组寄存器、栈。
  • 独立调度和分派的基本单位 在多线程 OS 中,线程是独立运行的基本单位,因而也是独立调度和分派的基本单位。
  • 可并发执行 同一进程中的多个线程之间可以并发执行,一个线程可以创建和撤消另一个线程。
  • 共享进程资源 它可与同属一个进程的其它线程共享进程所拥有的全部资源。

线程的控制

Create/Terminate Thread

线程的创建

  • 在多线程 OS 环境下,应用程序在启动时,通常仅有一个“初始化线程”线程在执行。
  • 在创建新线程时,需要利用一个线程创建函数(或系统调用),并提供相应的参数。
  • 如指向线程主程序的入口指针、堆栈的大小,以及用于调度的优先级等。
  • 在线程创建函数执行完后,将返回一个线程标识符供以后使用。

线程的终止

  • 线程完成了自己的工作后自愿退出;
  • 或线程在运行中出现错误或由于某种原因而被其它线程强行终止。

线程的三种终止方式

  • 线程从启动例程函数中返回,函数返回值作为线程的退出码
  • 线程被同一进程中的其他线程取消
  • 线程在任意函数中调用 pthread_exit 函数终止执行

线程间的同步与通信

互斥锁 Mutex

互斥锁是一种比较简单的、用于实现进程间对资源互斥访问的机制。
由于操作互斥锁的时间和空间开销都较低,因而较适合于高频度使用的关键共享数据和程序段。
互斥锁可以有两种状态, 即开锁(unlock)和关锁(lock)状态。

上锁的两种方式

  • 阻塞方式
1
2
3
4
//用来锁住互斥体变量。如果参数 mutex 所指的互斥体已经被锁住了,那么发出调用的线程将被阻塞直到其他线程对 mutex 解锁。
lock(mutex)
访问
unlock(mutex)
  • 非阻塞方式
1
2
3
if(trylock) then
else
//如果互斥体已经被上锁,该调用不会阻塞等待,而会返回一个错误代码。

条件变量 Condition Variable

在多线程编程中,条件变量是一种同步机制,用于在某个特定条件变为真时唤醒一个或多个线程。条件变量通常与互斥锁(mutex)一起使用,以确保线程在检查条件和对条件进行等待时不会发生竞争条件。

以下是条件变量在控制线程同步中的一般用法:

  • 等待条件:当线程需要等待某个条件变为真时,它会首先获取与条件变量相关联的互斥锁。然后,它会检查条件是否已经满足。如果条件未满足,线程会调用条件变量的等待方法(例如在 Java 中的 Condition.await())。这将释放互斥锁,并使线程进入睡眠状态,等待条件变为真。
  • 改变条件:当线程改变可能会影响条件的状态的数据时,它会首先获取互斥锁,然后修改数据。修改完成后,它会调用条件变量的唤醒方法(例如在 Java 中的 Condition.signal()Condition.signalAll()),以唤醒正在等待该条件的所有线程。然后,它会释放互斥锁。
  • 响应唤醒:当线程被唤醒时(即,它从等待方法返回时),它会重新获取互斥锁,并再次检查条件。这是必要的,因为在多线程环境中,条件可能在线程被唤醒和线程实际运行之间的时间内发生变化。

使用条件变量可以使线程在等待某个条件时不必进行忙等待(即,不断地检查条件是否已经满足)。这可以大大提高系统的效率,因为线程在等待时不会消耗 CPU 资源。

信号量机制

  • 私用信号量(private semaphore)。
    当某线程需利用信号量来实现同一进程中各线程之间的同步时,可调用创建信号量的命令来创建一私用信号量,其数据结构存放在应用程序的地址空间中。
    私用信号量属于特定的进程所有,OS 并不知道私用信号量的存在,因此,一旦发生私用信号量的占用者异常结束或正常结束,但并未释放该信号量所占有空间的情况时,系统将无法使它恢复为 0(空),也不能将它传送给下一个请求它的线程。
  • 公用信号量(public semaphore)。
    公用信号量是为实现不同进程间或不同进程中各线程之间的同步而设置的。
    有着一个公开的名字供所有的进程使用,故称为公用信号量。
    其数据结构是存放在受保护的系统存储区中,由 OS 为它分配空间并进行管理,故也称为系统信号量
    如果信号量的占有者在结束时未释放该公用信号量,则 OS 会自动将该信号量空间回收,并通知下一进程。因此公用信号量是一种比较安全的同步机制

线程的实现方式

各类线程的实现细节,其中上下文切换是核心

用户级线程 User-Level Threads

  • 用户级线程仅存在于用户空间中。对于这种线程的创建、撤消、线程之间的同步与通信等功能,都无须内核来实现。
  • 对于用户级线程的切换,通常是发生在一个应用进程的诸多线程之间,这时,也同样无须内核的支持。由于切换的规则远比进程调度和管理的规则简单,因而使线程的切换速度特别快。可见,这种线程是与内核无关的
  • 由应用程序完成所有线程的管理
    线程库(用户空间):通过一组管理线程的函数库来提供一个线程运行管理系统(运行系统)
  • 内核不知道线程的存在
  • 线程切换不需要核心态特权
  • 调度算法可以是进程专用的

Pros And Cons:

  • 线程切换不调用内核
  • 调度是应用程序特定的:可以选择最好的算法
  • 可运行在任何操作系统上(只需要线程库),可以在一个不支持线程的 OS 上实现

  • 当线程执行一个系统调用时,该线程及其所属进程内的所有线程都会被阻塞。
  • 多线程应用不能利用多处理机进行多重处理。

用户级线程实现

用户级线程是在用户空间实现的。所有用户级线程都具有相同的数据结构,它们都运行在一个中间系统上。

当前有两种方式实现的中间系统:

  • 运行时系统(又称为线程库)
    用于管理和控制线程的函数的集合,包括创建、撤消线程函数、线程同步和通信函数、线程调度函数等。
    用户级线程不能直接利用系统调用,必须通过运行时系统间接利用系统调用。
  • 内核控制线程
    这种线程又称为轻型进程 LWP(Light Weight Process)
    每个进程都可拥有多个 LWP,每个 LWP 都有自己的 TCB,其中包括线程标识符、优先级、状态、栈和局部存储区等
    LWP 可通过系统调用来获得内核提供的服务,当一个用户级线程运行时,只要将它连接到一个 LWP 上,它便具有了内核支持线程的所有属性

内核支持线程 Kernel Supported Threads

  • 内核支持线程,是在内核的支持下运行的,即无论是用户进程中的线程,还是系统进程中的线程,他们的创建、撤消和切换等,是依靠内核实现的。
  • 在内核空间中为每一个内核支持线程设置了一个线程控制块 TCB, 内核是根据该控制块而感知某线程的存在的,并对其加以控制。

Pros and Cons

  • 在多处理器系统中,内核能够同时调度同一进程中多个线程并行执行;
  • 如果进程中的一个线程被阻塞了,内核可以调度该进程中的其它线程占有处理器运行,也可以运行其它进程中的线程;
  • 内核支持线程具有很小的数据结构和堆栈,线程的切换比较快,切换开销小;
  • 内核本身也可以采用多线程技术,可以提高系统的执行速度和效率。

对于线程切换而言,其模式切换的开销较大
在同一个进程中,从一个线程切换到另一个线程时,需要从用户态转到内核态再转到用户态进行,这是因为用户进程的线程在用户态运行,而线程调度和管理是在内核实现的,系统开销较大。

和用户级相比,内核支持线程有什么不同?
由内核完成线程的创建、调度等
主要工作仍是保存现场,但保存位置为内核栈
每个执行序列需有两个栈: 用户栈+内核栈
用户栈:普通的函数调用
内核栈:系统调用、中断处理

内核支持线程的实现

在仅设置了内核支持线程的 OS 中,一种可能的线程控制方法是,系统在创建一个新进程时,便为它分配一个任务数据区 PTDA(Per Task Data Area),其中包括若干个线程控制块 TCB 空间。TCB 包含了线程的各种信息,如线程的状态(运行、就绪、阻塞等)、寄存器的值、栈指针、优先级、线程 ID 等。每个线程都有一个与之对应的 TCB。

在创建新进程时,系统会为该进程的 PTDA 预分配若干个 TCB 空间,用来存放该进程将要创建的线程的 TCB。当进程创建新线程时,一个空闲的 TCB 会被取出并填充该线程的信息。当线程结束时,其 TCB 会被回收并放回到空闲的 TCB 池中。

通过这种方式,系统可以有效地管理和调度线程。每次线程切换时,系统只需要保存当前线程的状态到其 TCB 中,然后从下一个线程的 TCB 中恢复其状态,就可以实现线程的切换。

Exercises

EX1
进程 A 包含了 1 个用户级线程$t_{1}$,进程 B 包含了 100 个用户级线程$t_{i},i \in (1,100)$,$t_1$运行的时间和$t_i$的运行时间是否一样?
不一样,$t_1$运行的时间是$t_{i}$的 100 倍,进程 B 获得 CPU 的时间与进程 A 的相等
EX2
若进程 A 和进程 B 中的线程都是内核支持线程,两者的运行时间是否一样?
$t_1=t_i$,进程 B 获得 CPU 的时间是进程 A 的 100 倍
Explanation
当一个系统设置了用户级线程时,虽然每个进程可能有多个线程,但对于操作系统内核来说,它只看到进程,而看不到进程内部的线程。因此,内核的调度仍然是以进程为单位进行的。在采用时间片轮转调度算法时,操作系统会公平地将处理器时间分配给每个进程。也就是说,每个进程都会轮流获得一定的处理器时间(时间片)来执行。这确保了各个进程之间的公平性。
而运行内核支持线程时,操作系统内核是可以看到进程内部的线程的。因此,内核的调度是以线程为单位进行的。也就是说,每个线程都会轮流获得一定的处理器时间(时间片)来执行。这确保了各个线程之间的公平性。

作者

Jiaxing Gao

发布于

2024-03-14

更新于

2024-11-16

许可协议

评论

}