线程(计算)

一个具有两个执行线的过程,在一个处理器上运行
程序过程与线程
调度抢占上下文切换

计算机科学中,执行线程是最小的编程指令序列,可以通过调度程序独立管理,该指令通常是操作系统的一部分。在许多情况下,线程是过程的组成部分。

给定过程的多个线程可以同时执行(通过多线程功能),共享内存等资源,而不同的过程不共享这些资源。特别是,一个过程的线程共享其可执行的代码以及其在任何给定时间的动态分配变量和非线程 - 本地变量的值。

线程和过程的实现在操作系统之间有所不同。在现代操作系统中, Tanenbaum表明,流程组织的许多不同模型是可能的。

历史

线程在OS/360多编程中以“任务”的名称出现,并在1967年使用可变数量的任务(MVT)出现。Saltzer(1966)将Victor A. Vyssotsky称为“线程”一词。

随着CPU开始利用多个核心,在2000年代初期,在软件应用程序中使用线程变得越来越普遍。希望利用多个内核来获得性能优势的申请必须使用并发来利用多个核心。

相关概念

可以在内核级别或用户级别上进行调度,并且可以进行多任务处理。这产生了各种相关概念。

过程

在内核级别,一个过程包含一个或多个内核线程,该线程共享该过程的资源,例如内存和文件处理 - 一个过程是资源单元,而线程是调度和执行的单位。内核调度通常是统一的,或者更常见的是合作。在用户级别,像运行时系统这样的过程本身可以安排多个执行线程。如果这些不共享数据,例如在Erlang中,它们通常被类似地称为流程,而如果它们共享通常称为(用户)线程的数据,尤其是在预先安排的情况下。合作计划的用户线程称为纤维;不同的过程可能会以不同的方式安排用户线程。用户线程可以通过内核线程以各种方式执行(一对一,多对一,多对多)。术语“轻量级过程”是指用户线程或内核机制,以将用户线程调整到内核线程。

一个过程是内核调度的“重量级”单元,因为创建,破坏和切换过程相对昂贵。流程拥有操作系统分配的资源。资源包括内存(对于代码和数据),文件把手,插座,设备手柄,窗口和过程控制块。过程是通过过程隔离隔离的,并且不共享地址空间或文件资源,除非通过诸如继承文件句柄或共享内存片段之类的明确方法,或以共享方式映射相同的文件 - 请参阅“概要通信” 。创建或破坏过程相对昂贵,因为必须获取或释放资源。流程通常是预先任务的多任务,并且由于诸如高速缓存冲洗等问题(特别是,过程切换更改虚拟内存地址,导致无效,从而潮红,从而使未被删除的翻译lookaside lookaside luffer (尤其是),过程切换相对昂贵,并且过程切换相对昂贵,而不是上下文切换的基本成本。 TLB),特别是在X86上)。

内核线

内核线程是内核计划的“轻量级”单元。每个过程中至少存在一个内核线程。如果一个过程中存在多个内核线程,则它们共享相同的内存和文件资源。如果操作系统的流程调度程序是先发制人的,则内核线程将进行多任务。内核线程除了堆栈,包括程序计数器在内的寄存器的副本和线程 - 本地存储(如果有),则没有资源,因此创建和销毁相对便宜。线程开关也相对便宜:它需要上下文开关(保存和还原寄存器和堆栈指针),但不会更改虚拟内存,因此可以缓存友好(使TLB有效)。内核可以为系统中的每个逻辑核心分配一个线程(因为每个处理器在支持多线程的情况下将自己分为多个逻辑内核,或者仅支持一个逻辑核心,如果不支持一个逻辑核心),并且可以交换线程被阻止。但是,内核线程的时间比要交换的用户线程更长。

用户线程

线程有时会在用户空间库中实现,因此称为用户线程。内核不知道它们,因此它们在用户空间中进行管理和安排。某些实现将其用户线程基于几个内核线程,以便从多处理器机器( M:N模型)中受益。虚拟机实现的用户线程也称为绿色线程

由于用户线程实现通常完全在用户空间中,因此在同一过程中的用户线程之间的上下文切换非常有效,因为它根本不需要与内核进行任何交互:可以通过在本地保存由CPU寄存器来执行上下文开关。当前执行用户线程或光纤,然后加载要执行的用户线程或光纤所需的寄存器。由于调度在用户空间中发生,因此可以更轻松地根据程序的工作负载来量身定制调度策略。

但是,在用户线程中使用阻止系统调用(与内核线程相对)可能是有问题的。如果用户线程或光纤执行阻止的系统调用,则该过程中的其他用户线程和纤维在系统调用返回之前将无法运行。此问题的一个典型示例是执行I/O时:大多数程序都会同步执行I/O。启动I/O操作时,将进行系统调用,并且直到完成I/O操作完成后才返回。在此期间,整个过程被内核“阻止”并且无法运行,在执行中,在同一过程中饿了其他用户线程和纤维。

解决此问题的一个常见解决方案(尤其是在许多绿色线程实现中使用)是提供I/O API,该I/O API通过内部使用非块I/O来实现阻止调用线程而不是整个过程的接口,并在进行I/O操作时安排另一个用户线程或光纤。可以为其他阻止系统调用提供类似的解决方案。另外,可以编写该程序以避免使用同步I/O或其他阻止系统调用(尤其是使用非阻止I/O,包括lambda连续性和/或异步/等待原始词)。

纤维

纤维是一个更轻松的调度单位,合作进行了安排:运行的光纤必须明确“产量”才能允许另一个光纤运行,这使得它们的实现比内核或用户线程更容易。可以在同一过程中安排光纤在任何线程中运行。这允许应用程序通过管理自己的计划来提高绩效,而不是依靠内核调度程序(可能不会为应用程序调整)。诸如OpenMP之类的并行编程环境有时通过纤维实现其任务。与纤维密切相关的是旋律,其区别是,旋ou是一种语言级别的结构,而纤维是系统级结构。

线程与进程

线程与传统的多任务操作系统流程不同:

  • 过程通常是独立的,而线程作为过程的子集存在
  • 流程比线程更具状态信息,而过程共享过程状态以及内存和其他资源中的多个线程
  • 流程具有单独的地址空间,而线程共享其地址空间
  • 流程仅通过系统提供的过程间通信机制进行交互
  • 在同一过程中线程之间的上下文切换通常比过程之间的上下文切换更快

据说Windows NTOS/2等系统具有便宜的线程和昂贵的过程。在其他操作系统中,除了地址空间开关的成本外,在某些架构(尤其是X86 )的成本外,没有太大的差异。

线程的优点和缺点与过程包括:

  • 线程的资源消耗较低:使用线程,应用程序可以使用比使用多个进程所需的资源少的资源运行。
  • 简化线程的共享和通信:与过程不同,这些过程需要消息传递或共享内存机制才能执行过程间通信(IPC),线程可以通过他们已经共享的数据,代码和文件进行通信。
  • 线程崩溃了一个过程:由于线程共享相同的地址空间,因此线程执行的非法操作可能会使整个过程崩溃;因此,一个不当行为的线程可能会破坏应用程序中所有其他线程的处理。

调度

先发制人与合作计划

操作系统的预先合作安排线程。多用户操作系统通常利用先发制的多线程,因为其通过上下文切换而不是执行时间的细粒度控制。但是,先发制人的调度可能会在程序员意外的时刻上下文切换线程,从而导致锁车队优先倒置或其他副作用。相比之下,合作的多线程依靠线程来放弃对执行的控制,从而确保线程运行到完成。如果通过等待资源进行合作多任务螺纹,或者如果在密集计算过程中不产生对执行的控制,这可能会导致问题。

单与多处理器系统

直到2000年代初,大多数台式计算机只有一个单核CPU,但不支持硬件线程,尽管在此类计算机上仍然使用了线程,因为在线程之间的切换通常比完整的程序上下文开关要快。在2002年,英特尔(Intel)“超线程”的名称增加了对Pentium 4处理器同时多线程的支持; 2005年,他们引入了双核D处理器, AMD引入了双核Athlon 64 X2处理器。

具有单个处理器的系统通常按时间切片实现多线程:中央处理单元(CPU)在不同的软件线程之间切换。这种上下文切换通常频繁地发生,以至于用户将线程或任务视为并行运行(对于流行的服务器/桌面操作系统,当其他线程在等待时,线程的最大时间切片通常仅限于100-200ms)。在多处理器多核系统上,多个线程可以并行执行,每个处理器或核心同时执行单独的线程;在带有硬件线程的处理器或核心上,也可以通过单独的硬件线程同时执行单独的软件线程。

线程模型

1:1(内核级线程)

用户与内核中可计划实体的1:1对应关系中创建的线程是最简单的线程实现。 OS/2Win32从一开始就使用了这种方法,而在Linux,GNU C库实现了此方法(通过NPTL或旧的LinuxThreads )。 SolarisNetBSDFreeBSDMacOSiOS也使用了这种方法。

M :1(用户级线程)

一个M :1模型意味着所有应用程序级线程映射到一个内核级计划的实体;内核不了解应用程序线程。通过这种方法,可以非常快速地进行上下文切换,此外,即使在不支持线程的简单内核上也可以实现。但是,主要缺点之一是,它不能从多线程处理器或多处理器计算机上的硬件加速度中受益:同一时间永远不会安排一个线程。例如:如果其中一个线程需要执行I/O请求,则整个过程被阻止,并且无法使用线程优势。 GNU便携式线程以及状态线程也使用用户级线程。

MN (混合线程)

Mn将一些M数量的应用程序线程映射到一些n数量的内核实体或“虚拟处理器”上。这是内核级别(“ 1:1”)和用户级(“ n :1”)线程之间的折衷。通常,“ MN ”线程系统比内核或用户线程更为复杂,因为需要更改内核和用户空间代码。在M:n实施中,线程库负责在可用的可计划实体上安排用户线程;这使线程的上下文切换非常快,因为它避免了系统调用。但是,这增加了复杂性和优先倒置的可能性,以及在Userland调度程序和内核调度程序之间的广泛(且昂贵)的协调方面的次优计划。

混合实施示例

UNIX系统中线程模型的历史记录

Sunos 4.X实施了轻重量工艺或LWP。 NetBSD 2.x+和Dragonfly BSD实施LWP作为内核线程(1:1型号)。 Sunos 5.2至Sunos 5.8以及Netbsd 2到Netbsd 4实现了两个级别的型号,在每个内核线程(M:N型号)上多样地在每个内核线程上多发性型号。 Sunos 5.9及更高版本以及NetBSD 5消除了用户线程的支持,返回1:1型号。 FreeBSD 5实施M:N模型。 FreeBSD 6支持1:1和M:N,用户可以选择使用 /etc/libmap.conf的给定程序。从FreeBSD 7开始,1:1成为默认值。 FreeBSD 8不再支持M:N模型。

单线程与多线程程序

计算机编程中,单线程是一次处理一个命令。在对变量的语义和过程状态的正式分析中,单个线程一词可以不同地使用,表示“在单个线程中进行回溯”,这在功能编程社区中很常见。

多线程主要在多任务操作系统中找到。多线程是一种广泛的编程和执行模型,允许在一个过程的上下文中存在多个线程。这些线程共享该过程的资源,但能够独立执行。螺纹编程模型为开发人员提供了同时执行的有用抽象。多线程也可以应用于一个过程,以在多处理系统上启用并行执行

多线程库倾向于提供一个函数调用来创建一个新线程,该线程将函数作为参数。然后创建一个并发线程,该线程开始运行传递功能并在函数返回时结束。线程库还提供数据同步功能。

线程和数据同步

同一过程中的线程共享相同的地址空间。这允许同时运行代码可以在没有IPC的开销或复杂性的情况下紧密而方便地交换数据。但是,当线程之间共享时,即使简单的数据结构需要一个以上的CPU指令才能更新,即使是简单的数据结构,它们也会容易出现:两个线程最终可能会同时尝试更新数据结构并发现它出乎意料地更改了脚下的脚下。种族条件引起的错误可能很难繁殖和分离。

为了防止这种情况,线程应用程序编程接口(API)提供同步原始图,例如静音词,锁定数据结构,以防止并发访问。在UniProcessor系统上,插入锁定静音的线程必须睡觉,因此触发上下文开关。在多处理器系统上,该线程可能会用自旋锁对互斥X进行轮询。这两种都可能会在对称的多处理(SMP)系统中削弱性能和强制处理器,以竞争内存总线,尤其是如果锁定的粒度太好了。

其他同步API包括条件变量临界部分信号量显示器

线程池

涉及线程的流行编程模式是线程池的编程模式,在启动时创建了一定的线程数,然后等待分配任务。当新任务到达时,它会醒来,完成任务并返回等待。这避免了针对执行的每个任务的相对昂贵的线程创建和破坏功能,并将线程管理从应用程序开发人员的手中带出,并将其放在库或更适合优化线程管理的库或操作系统中。

多线程程序与单线程程序优点和缺点

多线程应用程序具有以下优点与单线程:

  • 响应能力:多线程可以使应用程序对输入保持响应。在一个线程程序中,如果长期运行任务上的主要执行线程块,则整个应用程序可能会冻结。通过将这样的长期运行任务移至与主执行线程同时运行的工作线程,该应用程序可以在后台执行任务时对用户输入保持响应。另一方面,在大多数情况下,多线程并不是保持程序响应迅速的唯一方法,而无障碍I/O和/或UNIX信号可用于获得相似的结果。
  • 并行化:寻求使用多核或多CPU系统的应用程序可以使用多线程将数据和任务拆分为并行子任务,并让基础体系结构管理线程的运行方式,即同时在一个核心上或在多个内核上并行。诸如CUDAOPENCL之类的GPU计算环境使用多线程模型,其中数百个线程与数百个线程在大量内核上并行运行。反过来,这可以实现更好的系统利用,并且(前提是同步成本不会吞噬收益),可以提供更快的程序执行。

多线程应用程序具有以下缺点:

  • 同步复杂性和相关错误:使用典型的线程程序的共享资源时,程序员必须小心避免种族条件和其他非直觉行为。为了正确操纵数据,线程通常需要及时集合以按正确的顺序处理数据。线程还可能需要相互排斥的操作(通常是使用静音的操作),以防止在一个线程中读取或覆盖一个线程,同时被另一个线程读取。粗心地使用此类原语会导致僵局,生病或种族在资源上进行。正如爱德华·李(Edward A.作为一种计算模型,是非确定性的,程序员的工作成为修剪这种非确定性的一种。”
  • 无法测试。通常,多线程程序是非确定性的,因此无法测试。换句话说,多线程程序可以轻松地具有从未在测试系统上表现出来的错误,仅在生产中表现出来。这可以通过将线程间的通信限制为某些定义明确的模式(例如消息通信)来缓解这一点。
  • 同步成本。由于现代CPU上的线程上下文切换可能会花费高达100万个CPU周期,因此它使编写有效的多线程程序变得困难。特别是,必须特别注意以避免线际上的同步过于频繁。

编程语言支持

许多编程语言以某种能力支持线程。

  • IBM PL/I (f)早在1960年代后期就包括了对多线程的支持(称为多任务),并且在优化编译器和更高版本中继续进行了支持。 IBM Enterprise PL/I编译器引入了一个新的模型“线程” API。这两个版本都不是PL/I标准的一部分。
  • CC ++的许多实现支持线程,并提供对操作系统本机线程API的访问。线程实现的标准化接口是POSIX线程(PTHREADS),它是一组C函数库调用。 OS供应商可以根据需要自由实现接口,但是应用程序开发人员应该能够在多个平台上使用相同的接口。包括Linux在内的大多数UNIX平台支持Pthreads。 Microsoft Windows在进程中具有自己的一组线程函数。H接口用于多线程,例如BeginThread
  • 一些更高的级别(通常是跨平台)编程语言,例如JavaPython.NET Framework语言,在运行时在平台中抽象了平台特定的线程实现方面的特定差异,使线程暴露于开发人员。其他几种编程语言和语言扩展也试图使开发人员的并发和线程的概念完全( CILKOpenMP消息传递接口(MPI))抽象。某些语言是为顺序并行性设计的(尤其是使用GPU),而无需并发或线程( ateji pxcuda )。
  • 由于全局解释器锁(GIL),一些解释的编程语言具有实现(例如Ruby的Ruby MRI ,python的Cpython ),但由于全局解释器锁(GIL)而支持线程执行。 GIL是解释器持有的相互排除锁,可以防止解释器同时在两个或多个线程上同时解释应用程序代码,从而有效地限制了多个核心系统上的并行性。这主要限制了需要处理器的线程的性能,这需要处理器,而对于I/O-BOND或网络结合的线程并不多。解释的编程语言的其他实现,例如使用线程扩展名的TCL ,通过使用公寓模型避免使用GIL限制,在该模型中必须在线程之间明确“共享”数据和代码。在TCL中,每个线程都有一个或多个解释器。
  • 在用于数据并行计算编程模型中,一系列线程仅使用其ID并行运行相同的代码,以在内存中找到其数据。本质上,必须设计应用程序,以便每个线程在内存的不同段上执行相同的操作,以便它们可以并行操作并使用GPU体系结构。
  • 硬件说明语言(例如Verilog)具有不同的线程模型,该模型支持大量线程(用于建模硬件)。

也可以看看