Thread

来自osdev
跳到导航 跳到搜索

导言

在操作系统中,线程是执行和并发系统中挂起的各种任务的基本单元。 它们通常是进程的内部组件,共享同一组资源,即文件句柄、共享内存句柄、POSIX信号、消息传递缓冲区等。 但在Windows NT这样的体系结构中,它们可以拥有特定的特殊资源集,内核可以销毁线程终止时对应的一些资源

当调度程序选择线程时,线程可以看到彼此以并行方式执行,然后执行一段特定的时间。 这意味着,加入几个线程的工作是重复打印各自唯一的特定数字,你就会看到它们以随机方式在先后打印数字。 但线程只能在多处理器系统上实时并发执行,因为多个CPU才能同时执行任务,而单处理器系统中一个CPU逐个执行每个任务。

优势

所有现代操作系统都深度支持线程实现。 它们能导出内核调用,以配置内核如何管理各种线程。 与传统进程相比,线程有许多优点,可以在较新的应用程序中使用-

1. 并发性(Concurrency) - 线程允许以并行方式执行多个任务,并加速程序的各种工作。 例如,在加载页面时,两个工作线程可以在浏览器中解析两个不同的CSS样式表。 这提高了程序的输出和响应能力。

2. 更好地利用资源 - 当工作被划分为多个进程时,与工作在一个进程下的同一地址空间中划分为线程之间相比,会产生巨大的开销。 划分线程而不是进程可以减少上下文切换,并为程序提供更好的缓存使用率。

3. 线程的用户级管理 - 线程还可以在用户级别进行管理,这样就不需要将上下文切换到内核。 借助 Scheduler Activations(参见Scheduling)和M:N线程模型(我们将在本文后面讨论),这将极大地提高性能。

4. 简单 - 与创建几个单独的工作进程和相应的程序(使用进程间通信将它们相互链接)相比,创建一个有效利用线程API的单一程序非常简单。

线程的复杂性

程序和操作系统本身中的多线程可能会导致以下不良情况-

1. 竞争条件和同步开销(Race Conditions & Synchronization Overhead) - When many threads are working on the same data structure in memory, then they must somehow serialize their changes with synchronization techniques. This greatly impacts performance when the number of competing threads is huge, especially when concurrency bugs cause deadlocks, livelocks or other race conditions. For example, if threads 'A' and 'B' require both the resources 'R1' and 'R2', and 'A' locks 'R1' first and 'B' locks 'R2' first. Now both are competing for the other resource, both of which are already locked. This results into a complete stoppage of execution and is called a deadlock.

2. 可靠性 - 与单线程模型相比,多线程确实很复杂。 此外,如果一个线程抛出系统无法处理的异常(非C++语言中的异常),那么整个进程可能会终止。

内核线程

内核中的任务调度器处理内核级线程。 这些线程由内核保存在数据结构中,并由内核管理。 内核可以阻止它们进行I/O操作,并且很容易从外部进程中销毁它们。

进入内核后,线程的内核堆栈被加载,前面用户空间的堆栈及其执行状态被保存。 每个线程可能有自己的堆栈或共享一组堆栈。

多个内核堆栈

每个线程都有自己的内核堆栈的主要优点是,系统调用可以在内核内部阻塞,并且很容易在以后从该点恢复。 如果在系统调用期间发生页面错误或中断,则可以切换到另一个上下文并返回,然后完成系统调用。 然而,大量线程可能会占用大量内存,其中大部分在任何给定时间都不会使用。 此外,内核线程的不断切换可能会导致更高的缓存未命中率,从而降低性能。

单内核堆栈

另一种方法是每个处理器有一个内核堆栈,并让线程共享该堆栈。 这大大减少了必须分配的内存量。 线程在进入内核时获得对处理器内核堆栈的控制,并在线程切换时放弃它。 由于堆栈指针会被重置到顶部,堆栈实际上被销毁并重新创建。 然而,这意味着线程不能轻易地在内核内部阻塞或抢占。 例如,微内核可能需要向处理页面错误的服务器发送消息,但一旦切换到服务器线程,出错线程的堆栈将被清除。 内核必须提供一种机制来重新启动中断的系统调用(例如,使用continuations),或者保证系统调用不会阻塞、被抢占或出错(任何这样的情况都是致命的)

可变内核堆栈

多个堆栈和单个堆栈之间的折衷方案是,每个处理器对所有线程使用一个堆栈,但当线程阻塞或在内核内部被抢占时,会分配一个新堆栈。 被阻止的线程拥有旧堆栈的所有权,其他线程共享新堆栈。 当线程被解锁并在内核内完成其操作时,其堆栈被释放。 这允许在内核内部阻塞,同时最小化内存使用。 这种方法的主要问题是,每次切换线程时,都必须为堆栈分配和释放内存,而这时可能是在内核必须及时处理中断的时候。

用户线程

用户级线程由用户级调度器持有,内核看不到。 它们可以在用户空间中创建或销毁,因此比内核线程的开销更低。

线程模型

线程可以使用这三种方法在系统中实现-

1:1模型

进程在用户空间中使用的每一线程都可以在内核中进行调度。 这意味着调度器将实际保存进程正在使用的所有线程的结构。

优点

当所有用户级线程都由一一对应的内核线程支持时,程序将获得以下好处-

1. 所有线程都可以在单独的处理器上并发执行。 调度器可以将每个线程分配给处理器,如果可能的话,所有线程都可以以并行方式执行。

2. 与单独管理用户级线程相比,依靠内核管理线程更简单。

3. 内核线程更容易暂停特定时间,因为内核可以轻松访问APIC计时器。

4. 调度器可以使用简单的模型构建,而不需要用户级调度器的概念。

缺点

当所有用户级线程都由内核线程支持时,由于上下文切换到内核,它们的创建、销毁、暂停、等待、阻塞等操作变得非常昂贵。 它还会导致进程级别的负载不平衡。 运行大量线程的进程可能会损害系统,因为它会占用其他重要任务的大部分重要时间。 这个问题可以通过分组调度来解决

1:n 模型

在该模型中,所有用户级线程直接映射到一个内核线程。 对于内核来说,这个过程看起来像一个单线程程序。 但在用户模式下,内核线程将直接执行用户级调度程序,而用户级调度程序又会轮替选择执行线程。

优点

这种方法减少了创建、销毁、阻塞等用户级线程的开销,因为所有线程都可以在用户空间中管理,而无需任何上下文切换到内核。 它还允许流程为其任务定制一个调度程序,并有助于协作调度

缺点

在不停止其他线程的情况下,无法轻松阻止线程获取外部资源。 由于内核只能暂停一个内核线程,因此如果阻塞用户级线程的时间太长,那么它将导致进程的输出下降。

M:N 模型

这在理论上是一个理想的线程模型。 用户空间中的线程数为“M”,内核线程数为“N”,前提是M>N。 它以前是在FreeBSD上实现的,但由于其复杂性而被弃用。

优点

这种方法允许使用内核线程的功能进行快速的用户级线程管理。 此外,它还减少了内核中线程结构的资源使用。 例如,如果系统中有两个CPU,一个进程使用8个线程。 将由4个线程组成的组映射到2个内核线程会更实际。 这两个线程的优先级可以随着映射的增加而增加,这将允许用户级线程和内核线程之间建立动态关系。 如果使用两个以上的内核线程来运行线程,那么由于两个CPU的限制,它们仍然无法并发执行。

缺点

M:N模型相当复杂。 它涉及用户级和内核级的合作。 在实际测试中,与1:1模型相比,这种复杂性在一般应用中多次未能带来实质性的好处。 这就是为什么它在Solaris和FreeBSD等系统中被弃用的原因。

另见