跳转至

5 Threads

约 1630 个字 8 张图片 预计阅读时间 5 分钟

在进程一节中,我们看到了进程在 fork 时有较大的开销,也看到了 copy-on-write 等技术能够减少这些开销;在调度一节中,我们同样看到了上下文切换时会带来的开销。我们容易意识到,在很多情况下,若干进程可能共享一些内容,而操作系统本身如果知道这些共享,则可以减省新建进程的开销以及进程间切换的时延。因此操作系统引入了 线程 (threads)

​在 Linux 中,线程也被称为 轻量级进程 (LightWeight Process),这实际上是对线程非常恰当的一个描述。每个线程都有它自己的 thread ID, PC, register set 和 runtime stack。线程与同一进程的其他线程共享 code section, data section, heap, open files 以及 signals。

​对于支持线程的操作系统,实际调度的是内核级线程而非进程。也就是说,线程是运行以及 CPU 调度的基本单元。(而进程是分配资源的基本单元。)

5.1 多线程编程的优缺点

采用 多线程编程 (Multi-Threaded Programming) 的优点包括:

  • Economy: 建立线程相比进程是很经济的,因为 code, data & heap 已经在内存中了;另外在同一进程的线程间进行 context switch 时也会更快,因为我们不需要 cache flush。
  • Resource Sharing: 同一进程的线程之间天然共享内存,因此我们无需为它们编写 IPC;这也允许我们对同一块内存做并行的处理。但这也会引入风险。
  • Responsiveness: 多线程的进程会有更好的响应性,即当一个线程 blocked 或者在做一些长时间的操作时,其他线程仍然能完成工作,包括对用户的响应。
    • 例如,在一个 client-server 结构中,我们用一个线程来响应客户端的请求:
  • Scalability: 在多处理器的体系结构中,多线程进程可以更好地发挥作用,因为每个线程都可以在一个处理器上运行;而单线程进程只能在一个处理器上运行。

实际上,后两点对多个单线程进程也是适用的。但多线程进程相较而言更加经济和自然。

多线程也有一些缺点:

  • 如果一个进程出现错误,那么整个进程都会去世(比如浏览器的一个网页挂了,那么可能整个浏览器都会挂)。
  • 由于 OS 对每个进程地址空间的大小限制,多线程可能会使得进程的内存限制更加紧缩(这在 64 位体系结构中不再是问题)。
  • 由于多个线程共享部分内存,因此内存保护会比较困难。

5.2 线程的实现方式

​线程实现的重点是共享一些资源;而具体的实现分为 用户级线程 (User-Level Thread)内核级线程 (Kernel-Level Thread) 两类。

用户级线程的特点是,它在操作系统上只是一个进程,这个进程包含 线程库 (Thread Library) 的部分,​这部分代码负责完成线程的创建、切换等操作;而内核级线程则是由操作系统支持这些操作。

​用户级线程的优点包括:

  • ​它并不实际占用操作系统的 TID (Thread ID) 等资源,因此理论上来说可以支持比内核级线程更多的线程数;
  • ​它的调度等操作代码均在用户态,不需要进入内核态;
  • ​比较容易实现自定义的调度算法。

​用户级线程的缺点包括:

  • 一旦当前正在运行的线程阻塞,那么在操作系统看来就是整个进程被阻塞了,那么同一个进程中的其他线程也同样会被阻塞;而如果是内核级线程的话,一个线程阻塞了,其他线程仍然能正常运行。
  • 同一个进程中的多个用户级线程无法在多核上分别运行。

多线程模型

​在同时支持用户级线程和内核级线程的系统中,用户级线程到内核级线程的映射方式有多种选择。

​​这种方式其实和只支持用户级线程没啥区别:

​这种方式和只支持内核级线程也没啥区别:

​​​前面两种的组合,在这种情况下,\(n\) 个用户级线程可以映射到 \(m (\le n)\) 个内核级线程上:

5.3 ​Linux 线程

​Linux 中没有特别区分进程和线程,它们都被称为 tasks,每一个线程都有一个 task_struct。(虽然从概念上说,线程应该都使用其所属进程的 PCB,但是 Linux 不是这么实现的!)

​Linux 中可以通过 clone() 新建一个线程;这个系统调用包含了一大堆选项,包括新建的是一个线程还是进程,以及继承 / 新建哪些资源等。事实上,在很多实现中,fork() 的内部就是调用 clone() 实现的。

​从用户视角而言,Linux 中的线程通过 TID (Thread ID) 标识,而来自同一个进程的多个线程应当具有同样的 PID (Process ID);系统调用 gettid()getpid() 的实现和这个理解是一样的。

​但是事实上,task_struct 里面并没有 tid 这个字段,而是有 pidtgid (Thread Group ID) 这两个字段。其实,这里的 pid 和我们前述的 TID 含义一致,而这里的 tgid 和前述 PID 含义一致。这主要是历史原因,在还不支持线程的年代,pid 作为调度的依据被使用;在支持多线程后,调度的单元从进程变为了线程,如果引入一个新的字段 tid,则需要修改相关的代码,因此 pid 被保留,但发挥 tid 的作用;引入了一个新的字段 tgid,表示这一组线程的标识符,也就是其所属进程的标识符。

1

​正因如此,从 Linux 2.4 开始,getpid() 返回的就不再是 pid 了,而是 tgid。2

​使用 ps 指令,还能看到一个名叫 LWP (LightWeight Process) 的值,这个值始终和 TID 的值相同;LWP 通常被用作给用户呈现,而 TID 更经常被用作 gettid() 之类的系统调用。还有一个字段叫 NLWP,表示进程中的线程个数。3

颜色主题调整

评论区~

有用的话请给我个赞和 star => GitHub stars
快来跟我聊天~