维基百科 多线程 翻译

线程:https://en.wikipedia.org/wiki/Thread_(computing)

在计算机科学中,一个线程的执行是能够被操作系统中时间排程管理的最小的程序指令序列。在不同的操作系统中,线程和进程的定义是不同的,但是在大多数时候,一个线程是一个进程的内容。多个线程可以存在于一个进程中,同时执行并且分享资源如内存等,不同的进程不分享这些资源。特别是,进程的线程在任何给定时间共享其可执行代码及其动态分配变量和非线程局部全局变量的值。 

单进程和多进程系统的比较

单进程的系统通常通过时间分片来实现多线程,CPU在不同的软件线程中选择。这种转换通常发生的非常普遍和足够的迅速,使得用户以为线程任务是并行的,由每个进程和内核同时的执行一个独立的线程;在一个进程或者有硬件线程的内核,独立的软件线程仍然能够被独立的硬件线程执行。

历史

1967年线程以任务的名字出现在了任务数量可变的OD/360多程序系统中。Saltzer(1966)预言了线程这个概念。许多线代操作系统的进程调度程序支持时间分片也支持多进程的线程,操作系统核允许程序员通过公开所需功能的系统回调接口来操作线程。一些线程实现称为内核线程,而轻量级进程(LWP)是共享相同状态和信息的特定类型的内核线程。此外,当使用计时器、信号或其他方法进行线程处理以中断自己的执行时,程序可以具有用户空间线程,从而执行某种特殊的时间切片。 

线程和进程的比较

线程在这些方面区别于传统的多任务操作系统进程:

进程是典型的不相关的,而线程是进程的子集。

进程比线程携带更多的状态信息,而一个进程中的多个线程共享进程状态以及内存和其他资源。 

进程有独立的地址空间,而线程共享地址空间。

进程仅仅通过系统提供的进程间通信方式来通信

同一进程中的线程切换比进程之间的切换快

像windows NT和OS/2被称作便宜的线程和贵的进程;在其他操作系统中,除了地址空间开关的成本(在某些体系结构(尤其是x86)上)导致翻译后备缓冲区(TLB)刷新之外,没有太大的区别。 

单线程

在计算机编程中,单线程是一次处理一个指令。与单线程相对的是多线程。在对变量语义和进程状态的正式分析中,术语单线程可以不同地用于表示单线程内的回溯,这在编程界很常用。

多线程

多线程主要被发现在多任务的操作系统中。多线程普及的编程和执行模块允许多个线程在一个进程中存在。这些线程共享进程的资源但是能够独立的执行。线程编程模块给开发者提供了一个有用的并行执行的抽象。多线程也能被用在一个进程中使在多进程系统上并行执行。

多线程有以下优点:

响应:多线程可以使应用程序对输入保持响应。在一个单线程程序中,如果主执行线程阻塞了一个长时间运行的任务,那么整个应用程序似乎都会冻结。通过将这些长时间运行的任务移动到与主执行线程同时运行的工作线程,应用程序可以在后台执行任务时保持对用户输入的响应。另一方面,在大多数情况下,多线程并不是保持程序响应的唯一方法,无阻塞I/O和/或Unix信号可用于获得类似的结果。 

更快的执行速度:多线程程序的这一优点使其能够在具有多个中央处理器(CPU)或一个或多个多核处理器的计算机系统上或在多个计算机集群之间更快地运行,因为程序的线程自然地适合并行执行,假定有足够的独立性(t他们不需要等待对方)。 

降低资源消耗:使用线程,应用程序可以同时为多个客户机提供服务,使用的资源比使用多个进程副本本身所需的资源要少。例如,ApacheHTTP服务器使用线程池:侦听传入请求的侦听器线程池,以及处理这些请求的服务器线程池。 

更好的系统利用率:例如,使用多个线程的文件系统可以实现更高的吞吐量和更低的延迟,因为一个线程可以检索速度更快的介质(如缓存内存)中的数据,而另一个线程可以检索速度较慢的介质(如外部存储)中的数据,而两个线程都不等待另一个线程完成。 

简化的共享和通信:与需要消息传递或共享内存机制来执行进程间通信(IPC)的进程不同,线程可以通过它们已经共享的数据、代码和文件进行通信。

并行化:希望使用多核或多CPU系统的应用程序可以使用多线程将数据和任务拆分为并行子任务,并让底层架构管理线程如何在一个核心上并行运行或在多个核心上并行运行。像CUDA和OpenCL这样的GPU计算环境使用多线程模型,其中几十到数百个线程并行运行在大量核心上的数据上。

多线程有以下缺点:              

同步:由于线程共享相同的地址空间,程序员必须小心避免竞争条件和其他不直观的行为。为了正确地操作数据,线程通常需要及时会合,以便以正确的顺序处理数据。线程还可能需要互斥操作(通常使用互斥锁实现),以防止一个线程在被另一个线程修改时读取或覆盖公共数据。不小心使用这些原语会导致死锁、LiveLocks或资源争用。              

线程崩溃进程:线程执行的非法操作会崩溃整个进程;因此,一个行为不正常的线程可能会中断应用程序中所有其他线程的处理。 

时间安排

操作系统预先或协同调度线程。在多用户操作系统中,抢占式多线程是一种更广泛使用的方法,通过上下文切换实现对执行时间的细粒度控制。然而,抢占式调度可能会在程序员意想不到的时刻上下文切换线程,从而导致锁保护、优先级反转或其他副作用。相反,协作多线程依赖于线程放弃对执行的控制,从而确保线程运行到完成。如果一个协同工作的多任务线程通过等待一个资源而阻塞,或者如果它在密集的计算过程中不提供执行控制而使其他线程挨饿,那么这可能会产生问题。            

 直到21世纪初,大多数桌面计算机只有一个单核CPU,不支持硬件线程,尽管线程仍然在这类计算机上使用,因为线程之间的切换通常仍然比全进程上下文切换快。2002年,英特尔以“超线程”的名义向奔腾4处理器添加了对同步多线程的支持;2005年,他们推出了双核奔腾D处理器,AMD推出了双核Athlon 64 x2处理器。            

 嵌入式系统中对实时行为有更高要求的处理器可能通过减少线程切换时间来支持多线程处理,也可能通过为每个线程分配一个专用的寄存器文件,而不是保存/恢复一个通用的寄存器文件。 

进程,内核线程,用户线程和光纤

调度可以在内核级或用户级完成,多任务可以先发制人或协同完成。这产生了各种相关的概念。 

在内核级别,进程包含一个或多个内核线程,这些线程共享进程的资源,例如内存和文件句柄——进程是资源的一个单元,而线程是调度和执行的一个单元。内核调度通常都是以抢先方式或协作方式完成的。在用户级,运行时系统等进程本身可以调度多个执行线程。如果它们不共享数据,如在Erlang中,它们通常被类似地称为进程,而如果它们共享数据,则通常被称为(用户)线程,特别是在预先调度的情况下。协同调度的用户线程称为光纤;不同的进程可能以不同的方式调度用户线程。用户线程可以通过各种方式(一对一、多对一、多对多)由内核线程执行。术语“轻量级进程”不同地指用户线程或用于将用户线程调度到内核线程的内核机制。

进程是内核调度的“重量级”单元,因为创建、销毁和切换进程的成本相对较高。进程拥有由操作系统分配的资源。资源包括内存(用于代码和数据)、文件句柄、套接字、设备句柄、窗口和进程控制块。进程通过进程隔离进行隔离,并且不共享地址空间或文件资源,除非通过显式方法(如继承文件句柄或共享内存段)或以共享方式映射同一个文件—请参阅进程间通信。创建或破坏一个过程是相对昂贵的,因为必须获得或释放资源。进程通常是先发制人的多任务处理,由于缓存刷新等问题,进程切换相对昂贵,超出了上下文切换的基本成本。 

内核线程是内核调度的“轻量级”单元。每个进程中至少存在一个内核线程。如果一个进程中存在多个内核线程,那么它们共享相同的内存和文件资源。如果操作系统的进程调度程序是抢占的,那么内核线程将被抢占多任务处理。内核线程除了一个堆栈、一个寄存器副本(包括程序计数器)和线程本地存储(如果有的话)之外,不拥有其他资源,因此创建和销毁线程相对便宜。线程切换也相对便宜:它需要上下文切换(保存和恢复寄存器和堆栈指针),但不改变虚拟内存,因此缓存友好(使TLB有效)。内核可以为系统中的每个逻辑核心分配一个线程(因为如果每个处理器支持多线程,则将自己拆分为多个逻辑核心;如果不支持多线程,则每个物理核心只支持一个逻辑核心),并且可以交换被阻塞的线程。然而,内核线程交换的时间比用户线程要长得多。

线程有时在用户空间库中实现,因此称为用户线程。内核不知道它们,所以它们是在用户空间中管理和调度的。一些实现将用户线程建立在几个内核线程之上,以从多处理器机器(M:N模型)中获益。在本文中,术语“线程”(不带内核或用户限定符)默认为指内核线程。由虚拟机实现的用户线程也称为绿色线程。用户线程通常创建和管理速度很快,但不能利用多线程或多处理,即使有一些用户线程可以运行,如果所有相关的内核线程都被阻塞,则会被阻塞。 

光纤是一个更轻的协同调度调度单元:正在运行的光纤必须显式“屈服”以允许另一个光纤运行,这使得它们的实现比内核或用户线程容易得多。可以计划在同一进程中的任何线程中运行光纤。这允许应用程序通过管理自己的调度来获得性能改进,而不是依赖于内核调度程序(可能不会针对应用程序进行调优)。像OpenMP这样的并行编程环境通常通过光纤来实现它们的任务。与纤维密切相关的是协程,区别在于协程是语言级构造,而纤维是系统级构造。 

并发性和数据结构              

同一进程中的线程共享相同的地址空间。这使得并发运行的代码能够紧密地、方便地耦合交换数据,而无需工控机的开销或复杂性。然而,当在线程之间共享时,即使是简单的数据结构,如果它们需要多个CPU指令来更新,也会容易出现争用情况:两个线程最终可能会试图同时更新数据结构,并发现它在运行中发生了意外的变化。由竞争条件引起的错误很难复制和分离。             为了防止这种情况发生,线程应用程序编程接口(API)提供了同步原语,如互斥锁,以锁定数据结构,防止并发访问。在单处理器系统上,进入锁定互斥体的线程必须休眠,从而触发上下文切换。在多处理器系统上,线程可以轮询spinlock中的互斥体。这两种方法都可能会影响对称多处理(SMP)系统中的性能和强制处理器争夺内存总线,特别是在锁定的粒度很好的情况下。     虽然线程看起来是顺序计算的一小步,但实际上,它们代表了一大步。它们抛弃了顺序计算最基本和最吸引人的特性:可理解性、可预测性和确定性。线程作为一种计算模型,是非常不确定的,程序员的工作变成了修剪不确定的工作之一。 

I/O和调度

用户线程或光纤实现通常完全在用户空间中。因此,同一进程中的用户线程或光纤之间的上下文切换非常有效,因为它根本不需要与内核进行任何交互:可以通过本地保存当前正在执行的用户线程或光纤使用的CPU寄存器,然后加载美国所需的寄存器来执行上下文切换。执行的ER线或光纤。由于调度发生在用户空间中,调度策略可以更容易地根据程序工作负载的需求进行调整。    但是,在用户线程(而不是内核线程)或纤程中使用阻塞系统调用可能有问题。如果用户线程或光纤执行阻塞的系统调用,则在系统调用返回之前,进程中的其他用户线程和光纤将无法运行。这个问题的一个典型例子是在执行I/O时:大多数程序都是为了同步执行I/O而编写的。当启动I/O操作时,会进行系统调用,直到I/O操作完成后才会返回。在中间期间,整个进程被内核“阻塞”并且无法运行,这会使同一进程中的其他用户线程和纤程无法执行。              

此问题的一个常见解决方案是提供一个I/O API,通过在内部使用非阻塞I/O实现同步接口,并在I/O操作进行期间调度另一个用户线程或光纤。可以为其他阻塞系统调用提供类似的解决方案。或者,可以编写程序以避免使用同步I/O或其他阻塞系统调用。        Sunos4.x实现了轻量级流程或LWP。netbsd 2.x+和dragonfly bsd将LWPS实现为内核线程(1:1模型)。SUNOS 5.2到SUNOS 5.8以及NetBSD 2到NetBSD 4实现了一个两级模型,在每个内核线程(M:N模型)上复用一个或多个用户级线程。Sunos5.9及更高版本,以及NetBSd5取消了用户线程支持,返回到1:1模型。[9]FreeBsd5实现了m:n模型。freebsd 6同时支持1:1和m:n,用户可以使用/etc/libmap.conf选择一个应用于给定程序的选项。从freebsd 7开始,1:1成为默认值。FreeBsd8不再支持m:n模型。       内核线程的使用通过将线程的一些最复杂的方面移入内核来简化用户代码。程序不需要调度线程或显式生成处理器。用户代码可以用一种熟悉的过程风格编写,包括调用阻塞API,而不会让其他线程感到饥饿。然而,内核线程可能会在任何时候强制在线程之间进行上下文切换,从而暴露出竞争危险和并发性错误,否则这些错误可能是潜在的。在SMP系统上,这进一步加剧,因为内核线程实际上可以并行地在单独的处理器上执行。 

模型:

1:1(内核级线程)

用户在与内核[10]中可调度实体对应的1:1中创建的线程是最简单的线程实现。OS/2和Win32从一开始就使用这种方法,而在Linux上,通常的C库实现这种方法(通过nptl或旧的linuxthreads)。这种方法也被Solaris、NetBSD、FreeBSD、MacOS和iOS使用。 

N:1(用户级线程)

n:1模型意味着所有应用程序级线程都映射到一个内核级计划实体;内核不了解应用程序线程。使用这种方法,上下文切换可以非常迅速地完成,而且,它甚至可以在不支持线程的简单内核上实现。但是,它的一个主要缺点是,它不能从多线程处理器或多处理器计算机上的硬件加速中获益:在同一时间调度的线程永远不会超过一个。例如:如果其中一个线程需要执行I/O请求,则整个进程将被阻塞,线程的advanttagE不能使用。GNU可移植线程和状态线程一样使用用户级线程。 

M:N(混合线程)

m:n将一些m个应用程序线程映射到n个内核实体、或“虚拟处理器”上。这是内核级别(“1:1”)和用户级别(“n:1”)线程之间的折衷。一般来说,“m:n”线程系统比内核或用户线程更复杂,因为需要同时更改内核和用户空间代码。在m:n实现中,线程库负责在可用的可调度实体上调度用户线程;这使得线程的上下文切换非常快,因为它避免了系统调用。然而,这增加了复杂性和优先级反转的可能性,以及在用户和内核调度程序之间没有广泛(昂贵)协调的次优调度。 

混合实现案例:

netbsd原生posix线程库实现使用的调度程序激活(M:N模型,而不是1:1内核或用户空间实现模型)              

旧版本的Solaris操作系统使用的轻量级进程              

来自PM2项目的Marcel。              

Tera-Cray MTA-2的操作系统            

 Microsoft Windows 7用户模式计划[11][12]            

 语言haskell的glasgow haskell编译器(ghc)使用轻量级线程,这些线程是在操作系统线程上调度的。 

光纤实现示例:

光纤可以在不支持操作系统的情况下实现,尽管某些操作系统或库为光纤提供了明确的支持。              

Win32提供光纤API[13](Windows NT 3.51 SP3及更高版本)            

 红宝石绿线              

Netscape便携式运行时(包括用户空间光纤实现)              

RiBS2 

编程语言支持:

IBMPL/I(F)在20世纪60年代后期包括了对多线程(称为多任务)的支持,并且在优化编译器和更高版本中继续这样做。IBMEnterprisePL/I编译器引入了一个新的模型“线程”API。这两个版本都不是PL/I标准的一部分。              

许多编程语言在某种程度上支持线程。C和C++的许多实现支持线程,并提供对操作系统的本地线程API的访问。一些高级(通常是跨平台)的编程语言,如Java、Python和.NETFramework语言,将线程暴露给开发人员,同时抽象出运行时线程实现的平台特定差异。其他几种编程语言和语言扩展也试图从开发人员那里完全抽象并发和线程的概念(CILK、OpenMP、消息传递接口(MPI))。有些语言是为顺序并行而设计的(特别是使用GPU),不需要并发或线程(ATEJI PX、CUDA)。              

由于全局解释器锁(gil),一些解释的编程语言具有支持线程和并发性但不支持线程并行执行的实现(例如Ruby MRI for Ruby、CPython for Python)。gil是解释器持有的互斥锁,它可以防止解释器同时在两个或多个线程上解释应用程序代码,从而有效地限制多核系统上的并行性。这限制了性能,主要是针对需要处理器的处理器绑定线程,而对于I/O绑定线程或网络绑定线程则没有太多限制。              

解释编程语言的其他实现,如使用线程扩展的tcl,通过使用单元模型避免了gil限制,在单元模型中,线程之间必须显式地“共享”数据和代码。在TCL中,每个线程都有一个或多个解释程序。              

事件驱动的编程硬件描述语言(如verilog)有一个不同的线程模型,它支持大量线程(用于建模硬件)。 

实际多线程:

线程实现的标准化接口是POSIX线程(pthreads),它是一组C函数库调用。操作系统供应商可以根据需要自由实现接口,但是应用程序开发人员应该能够在多个平台上使用相同的接口。包括Linux在内的大多数Unix平台都支持pthreads。Microsoft Windows在process.h接口中有自己的线程函数集,用于多线程处理,如beginthread。Java使用Java并发库JavaUTIL并发在主机操作系统上提供了另一个标准化接口。              

多线程库提供一个函数调用来创建一个新线程,该线程以函数为参数。然后创建一个并发线程,该线程开始运行传递的函数,并在函数返回时结束。线程库还提供同步功能,使使用互斥锁、条件变量、关键部分、信号量、监视器和其他同步原语实现无争用条件无错误多线程功能成为可能。              

线程使用的另一个范例是线程池,其中在启动时创建一组线程,然后等待分配任务。当新任务到达时,它会唤醒,完成任务并返回等待状态。这就避免了为执行的每项任务执行相对昂贵的线程创建和销毁功能,并将线程管理从应用程序开发人员的手中带走,并将其留给更适合优化线程管理的库或操作系统。例如,像GrandCentralDispatch和线程构建块这样的框架。              

在为数据并行计算而设计的CUDA等编程模型中,一组线程并行运行相同的代码,只使用其ID在内存中查找数据。本质上,应用程序的设计必须使每个线程在不同的内存段上执行相同的操作,以便它们可以并行操作并使用GPU体系结构。 

 

 

多线程 https://en.wikipedia.org/wiki/Multithreading_(computer_architecture)

在计算机体系结构中,多线程是中央处理单元(CPU)或多核处理器中的单个内核同时执行多个进程或线程的能力,由操作系统支持。这种方法不同于多处理。在多线程应用程序中,进程和线程共享单个或多个核心的资源,这些核心包括计算单元、CPU缓存和翻译查询缓冲区(TLB)。              

当多处理系统在一个或多个核心中包含多个完整的处理单元时,多线程旨在通过使用线程级并行和指令级并行来提高单个核心的利用率。由于这两种技术是互补的,因此有时它们在具有多个多线程CPU和具有多个多线程核心的CPU的系统中结合在一起。 

概览

随着进一步利用指令级并行性的努力在20世纪90年代末停滞,多线程模式变得越来越流行。这使得吞吐量计算的概念从更专业的事务处理领域重新出现。尽管进一步加速单个线程或单个程序非常困难,但大多数计算机系统实际上是多线程或多程序之间的多任务处理。因此,提高所有任务吞吐量的技术会导致总体性能提高。              

吞吐量计算的两种主要技术是多线程和多处理。 

优点

如果一个线程出现大量缓存未命中,其他线程可以继续利用未使用的计算资源,这可能会导致更快的总体执行,因为如果只执行一个线程,这些资源就会处于空闲状态。此外,如果一个线程不能使用CPU的所有计算资源(因为指令依赖于彼此的结果),运行另一个线程可能会阻止这些资源变为空闲。

缺点

当共享硬件资源(如缓存或转换后备缓冲区(TLB))时,多个线程可以相互干扰。因此,单线程的执行时间没有得到改善,甚至在只有一个线程正在执行的情况下,也可能会降低,这是因为频率较低或需要额外的管道阶段来容纳线程切换硬件。              总体效率各不相同;Intel声称其超线程技术提高了30%,而仅执行非优化相关浮点操作循环的合成程序在并行运行时实际上获得了100%的速度提高。另一方面,使用MMX或Altivec扩展和执行数据预取(作为一个好的视频编码器)的手工调优的汇编语言程序不会受到缓存未命中或空闲计算资源的影响。因此,这样的程序不能从硬件多线程中获益,而且确实可以看到由于共享资源争用而导致的性能下降。              

从软件的角度来看,软件对多线程的硬件支持更为明显,与多处理相比,需要对应用程序和操作系统进行更多的更改。用于支持多线程的硬件技术通常与用于计算机多任务处理的软件技术并行。线程调度也是多线程处理中的一个主要问题。

多线程的种类

Coarse-grained multithreading粗粒度多线程

最简单的多线程类型发生在一个线程运行到被通常会导致长延迟暂停的事件阻止时。这样的暂停可能是缓存未命中,必须访问片外内存,这可能需要数百个CPU周期才能返回数据。线程处理器将把执行切换到另一个已准备好运行的线程,而不是等待解决暂停问题。只有当前一个线程的数据到达时,前一个线程才会被放回准备运行的线程列表中。  

For example:

  1. Cycle i: instruction j from thread A is issued.
  2. Cycle i + 1: instruction j + 1 from thread A is issued.
  3. Cycle i + 2: instruction j + 2 from thread A is issued, which is a load instruction that misses in all caches.
  4. Cycle i + 3: thread scheduler invoked, switches to thread B.
  5. Cycle i + 4: instruction k from thread B is issued.
  6. Cycle i + 5: instruction k + 1 from thread B is issued.

从概念上讲,它类似于实时操作系统中使用的合作多任务处理,在这种情况下,当任务需要等待某种类型的事件时,它们会自动放弃执行时间。这种类型的多线程被称为块、合作或粗粒度多线程。              

多线程硬件支持的目标是允许在阻塞的线程和另一个准备运行的线程之间快速切换。为了实现这一目标,硬件成本是复制程序可见寄存器以及一些处理器控制寄存器(如程序计数器)。从一个线程切换到另一个线程意味着硬件从使用一个寄存器集切换到另一个;要在活动线程之间高效切换,每个活动线程都需要有自己的寄存器集。例如,要在两个线程之间快速切换,需要将寄存器硬件实例化两次。              

对多线程的额外硬件支持允许在一个CPU周期内完成线程切换,从而提高性能。另外,附加的硬件允许每个线程的行为就像它单独执行一样,并且不与其他线程共享任何硬件资源,从而最小化应用程序和操作系统中支持多线程所需的软件更改量。             许多系列的微控制器和嵌入式处理器都有多个寄存器组,允许对中断进行快速上下文切换。这种方案可以被认为是用户程序线程和中断线程之间的块多线程类型。 

交错多线程

交错多线程的目的是从执行管道中删除所有数据依赖暂停。由于一个线程相对独立于其他线程,因此在一个流水线阶段中,一条指令需要管道中较旧指令的输出的可能性较小。从概念上讲,它类似于操作系统中使用的抢占式多任务处理;一个类比是,给每个活动线程的时间片是一个CPU周期。 

For example:

  1. Cycle i + 1: an instruction from thread B is issued.
  2. Cycle i + 2: an instruction from thread C is issued

这种类型的多线程首先被称为桶处理,其中桶的staves表示管道阶段及其执行线程。交错、抢占、细粒度或分时多线程是更现代的术语。              

除了在多线程块类型中讨论的硬件成本外,交错多线程还有跟踪正在处理的指令的线程ID的每个管道阶段的额外成本。此外,由于管道中并发执行的线程更多,因此共享资源(如缓存和TLB)需要更大一些,以避免不同线程之间的冲突。 

同步线程

最先进的多线程类型适用于超标量处理器。一般的超标量处理器在每个CPU周期从一个线程发出多个指令,而在同时多线程(SMT)中,超标量处理器可以在每个CPU周期从多个线程发出指令。认识到任何单个线程具有有限的指令级并行性,这种类型的多线程处理尝试利用多个线程之间可用的并行性来减少与未使用的问题槽相关的浪费。 

For example:

  1. Cycle i: instructions j and j + 1 from thread A and instruction k from thread B are simultaneously issued.
  2. Cycle i + 1: instruction j + 2 from thread A, instruction k + 1 from thread B, and instruction m from thread C are all simultaneously issued.
  3. Cycle i + 2: instruction j + 3 from thread A and instructions m + 1 and m + 2 from thread C are all simultaneously issued.

为了区分其他类型的多线程和SMT,术语“临时多线程”用于表示何时一次只能发出一个线程的指令。              

除了讨论交错多线程的硬件成本外,SMT还有跟踪每个正在处理的指令的线程ID的每个管道阶段的额外成本。同样,共享资源(如缓存和TLB)必须针对正在处理的大量活动线程调整大小。              

实现包括DEC(稍后的Compaq)EV8(未完成)、Intel超线程技术、IBM POWER5、Sun Microsystems UltraSPARC T2、Cray Xmt、AMD推土机和Zen微体系结构。 

实施细则:

研究的一个主要领域是线程调度程序,它必须从准备运行的线程列表中快速选择以执行下一个线程,并维护准备运行和暂停的线程列表。一个重要的副标题是调度程序可以使用的不同的线程优先级方案。线程调度程序可以完全在软件中实现,完全在硬件中实现,或者作为硬件/软件组合实现。              

另一个研究领域是什么类型的事件应该导致线程切换:缓存未命中、线程间通信、DMA完成等。              

如果多线程方案复制了所有软件可见状态,包括特权控制寄存器和TLB,那么它可以为每个线程创建虚拟机。这允许每个线程在同一处理器上运行自己的操作系统。另一方面,如果只保存用户模式状态,则需要较少的硬件,这将允许同一模具区域或成本同时激活更多线程。 

 

https://en.wikipedia.org/wiki/Multithreading_(computer_architecture)

在计算机体系结构中,多线程是中央处理单元(CPU)或多核处理器中的单个内核同时执行多个进程或线程的能力,由操作系统支持。这种方法不同于多处理。在多线程应用程序中,进程和线程共享单个或多个核心的资源,这些核心包括计算单元、CPU缓存和翻译查询缓冲区(TLB)。              

当多处理系统在一个或多个核心中包含多个完整的处理单元时,多线程旨在通过使用线程级并行和指令级并行来提高单个核心的利用率。由于这两种技术是互补的,因此有时它们在具有多个多线程CPU和具有多个多线程核心的CPU的系统中结合在一起。

概览: 

随着进一步利用指令级并行性的努力在20世纪90年代末停滞,多线程模式变得越来越流行。这使得吞吐量计算的概念从更专业的事务处理领域重新出现。尽管进一步加速单个线程或单个程序非常困难,但大多数计算机系统实际上是多线程或多程序之间的多任务处理。因此,提高所有任务吞吐量的技术会导致总体性能提高。              

吞吐量计算的两种主要技术是多线程和多处理。 

优点:

如果一个线程有很多缓存未命中,其他线程可以继续利用未使用的计算资源,这可能会导致更快的总体执行,因为如果只执行一个线程,这些资源就会处于空闲状态。此外,如果一个线程不能使用CPU的所有计算资源(因为指令依赖于彼此的结果),运行另一个线程可能会阻止这些资源变为空闲。 

缺点:

当共享硬件资源(如缓存或转换后备缓冲区(TLB))时,多个线程可以相互干扰。因此,单线程的执行时间没有得到改善,甚至在只有一个线程正在执行的情况下,也可能会降低,这是因为频率较低或需要额外的管道阶段来容纳线程切换硬件。              总体效率各不相同;Intel声称其超线程技术提高了30%,而仅执行非优化相关浮点操作循环的合成程序在并行运行时实际上获得了100%的速度提高。另一方面,使用MMX或Altivec扩展和执行数据预取(作为一个好的视频编码器)的手工调优的汇编语言程序不会受到缓存未命中或空闲计算资源的影响。因此,这样的程序不能从硬件多线程中获益,而且确实可以看到由于共享资源争用而导致的性能下降。              

从软件的角度来看,软件对多线程的硬件支持更为明显,与多处理相比,需要对应用程序和操作系统进行更多的更改。用于支持多线程的硬件技术通常与用于计算机多任务处理的软件技术并行。线程调度也是多线程处理中的一个主要问题。

 

线程代码

 

 

在计算机科学中,“线程代码”一词指的是一种编程技术,其中代码的形式基本上完全由对子程序的调用组成。通常,但不仅是在编译器实现中,可以发现以这种形式生成代码和/或以这种形式实现代码本身。代码可以由解释器处理,也可以只是一系列机器代码调用指令。

与其他代码生成技术和替代调用约定生成的代码相比,线程代码具有更好的代码密度,有时会以稍微慢一些的执行速度为代价。然而,一个小到足以完全装入计算机处理器缓存的程序可能比一个较大的程序运行得更快,而较大的程序可能会出现许多缓存丢失。小程序也可以运行得更快,当其他程序填满缓存时,当切换线程时。线程代码是一些编程语言中常用的实现技术,如Forth、BASIC的许多实现、COBOL的一些实现、B、的早期版本以及其他用于小型小型计算机和业余无线电卫星的语言。

历史

制作计算机程序的常用方法是使用编译器将用某种符号语言编写的计算机程序“翻译”成机器码。由于二进制代码是为特定的计算机硬件平台设计的,因此代码通常是快速的,但不可移植。另一种方法使用虚拟机指令集,它没有特定的目标硬件。解释器在每个新的目标硬件上执行它。

早期的计算机内存相对较少。例如,大多数数据通用Nova、IBM 1130和许多第一代Apple II计算机只安装了4kb的RAM。因此,我们花了大量的时间试图找到减小程序大小的方法,使它们能够适合可用的内存。与此同时,计算机的速度相对较慢,因此简单的解释明显比执行机器码慢。程序员不是在程序的每个需要的部分写出一个操作的每一步,而是通过只写一次这样的操作的每一步(参见“不要重复”)并将其放入子例程来节省内存。这个过程‍-‌代码重构‍‌今天使用,尽管是出于不同的原因。这些程序中的顶级应用程序可能只包含子例程调用。这些子例程中的许多子例程也只包含较低级别的子例程调用。

大型机和一些早期的微处理器(如RCA 1802)需要几个指令来调用子例程。在顶级应用程序和许多子例程中,该序列不断重复,只有子例程地址从一个调用更改到下一个调用。重复使用内存存储相同的指令是浪费。为了节省空间,程序员将这一系列子例程调用压缩到只包含子例程的连续地址的列表中,并使用一个很小的“解释器”依次调用每个子例程。这与其他程序员将分支表、分派表或虚拟方法表中的一系列跳转压缩到只包含目标地址的列表中的方法相同,并使用一个小选择器将分支转移到所选的目标。在线程代码和这些其他技术中,程序成为要执行的实际代码的入口点列表。

多年来,程序员在“解释器”或“小选择器”上创建了许多变体。地址列表中的特定地址可以使用索引、通用寄存器或指针提取。地址可以是直接的或间接的、连续的或非连续的(通过指针链接)、相对的或绝对的、在编译时解析的或动态构建的。没有一种变异是“最好的”。

为了节省空间,程序员将子例程调用列表压缩为子例程地址的简单列表,并使用一个小循环依次调用每个子例程。例如:

start:
  ip = &thread
top:
  jump *ip++
thread:
  &pushA
  &pushB
  &add
  ...
pushA:
  *sp++ = A
  jump top
pushB:
  *sp++ = B
  jump top
add:
  addend = *--sp
  *sp++ = *--sp + addend
  jump top

在这种情况下,解码字节码在程序编译或程序加载期间执行一次,因此不会在每次执行一条指令时重复。与执行成本相比,当解码和分派开销较大时,这可以节省大量的时间和空间。

但是,注意,线程中的&pushA、&pushB等地址是两个或更多字节,而上面描述的解码和分派解释器(通常)是一个字节。通常,解码和分派解释器的指令可以是任意大小。例如,模拟Intel Pentium的解码和分派解释器对从1字节到16字节的指令进行解码。

然而,字节码系统通常为最常见的操作选择1字节代码。因此,线程的空间开销通常比字节码高。在大多数应用中,解码成本的降低超过了空间成本的增加。

还要注意,虽然字节码在名义上是独立于机器的,但是线程中指针的格式和值通常取决于执行解释器的目标机器。因此,解释器可以加载一个可移植的字节码程序,解码字节码以生成平台相关的线程代码,然后执行线程代码,而无需进一步引用字节码。这个循环很简单,所以在每个处理程序中都进行了复制,将jump top从执行每个解释器指令所需的机器指令列表中删除。例如:

start:
  ip = &thread
  jump *ip++
thread:
  &pushA
  &pushB
  &add
  ...
pushA:
  *sp++ = A
  jump *ip++
pushB:
  *sp++ = B
  jump *ip++
add:
  addend = *--sp
  *sp++ = *--sp + addend
  jump *ip++

这称为直接线程代码(DTC)。虽然这种技术比较古老,但是术语“线程代码”的首次广泛使用可能是Bell在1973年发表的文章“线程代码”Charles H. Moore在1970年为他的第四个虚拟机发明了一种更紧凑的表示法:间接线程代码(interactivedcode, ITC)。最初,摩尔发明这个是因为它在Nova小型计算机上简单而快速,每个地址都有一个间接比特。他说(在发表的评论,字节杂志的第4期),他发现它是如此方便,他传播到所有后来的第4期设计。一些Forth编译器将Forth程序编译成直接线程化的代码,而另一些则生成间接线程化的代码。无论哪种方式,程序的作用都是一样的。

线程模型:

实际上,所有可执行的线程代码都使用这些方法中的一个或另一个来调用子例程(每个方法都称为“线程模型”)。

直接线程:

线程中的地址是机器语言的地址。这种形式很简单,但是可能会有开销,因为线程只由机器地址组成,所以所有进一步的参数都必须从内存中间接加载。一些Forth系统产生直接线程代码。在许多机器上,直接线程化比子例程线程化快(参见下面的参考资料)。堆栈机器的一个例子可能执行序列“push a, push B, add”。这可以转换为以下线程和例程,其中ip初始化为该地址&thread

thread:
  &pushA
  &pushB
  &add
  ...
pushA:
  *sp++ = A
  jump *ip++
pushB:
  *sp++ = B
  jump *ip++
add:
  addend = *--sp
  *sp++ = *--sp + addend
  jump *ip++

或者,可以在线程中包含操作数。这可以删除上面需要的一些间接性,但是会使线程变大:

thread:
  &push
  &A
  &push
  &B
  &add
push:
  *sp++ = *ip++
  jump *ip++
add:
  addend = *--sp
  *sp++ = *--sp + addend
  jump *ip++

间接的线程:

间接线程使用指向指向机器码的位置的指针。间接指针后面可以跟着操作数,这些操作数存储在间接“块”中,而不是在线程中重复存储它们。因此,间接代码通常比直接线程代码更紧凑,但是间接代码通常也会使其变慢,尽管通常仍然比字节码解释器更快。当处理程序操作数同时包含值和类型时,与直接线程代码相比,节省的空间可能非常大。较老的FORTH系统通常产生间接线程代码。例如,如果目标是执行“push A, push B, add”,那么可以使用以下命令。在这里,ip被初始化为address &thread,每个代码片段(push, add)通过ip双内指向找到;每个代码片段的操作数都位于该片段地址之后的第一级间接中。

thread:
  &i_pushA
  &i_pushB
  &i_add
i_pushA:
  &push
  &A
i_pushB:
  &push
  &B
i_add:
  &add
push:
  *sp++ = *(*ip + 1)
  jump *(*ip++)
add:
  addend = *--sp
  *sp++ = *--sp + addend
  jump *(*ip++)

子程序线程:

所谓的“子程序线程代码”(也称为“调用线程代码”)由一系列机器语言的“调用”指令(或“调用”函数的地址,而不是直接线程使用“跳转”)组成。早期的ALGOL、Fortran、Cobol和一些Forth系统的编译器常常生成子程序线程代码。这些系统中的许多代码都是在后进先出(LIFO)操作数堆栈上运行的,后进先出(LIFO)操作数堆栈具有成熟的编译器理论。大多数现代处理器都对子例程“调用”和“返回”指令提供了特殊的硬件支持,因此每次分派一个额外机器指令的开销有所减少。Anton Ertl曾经说过“与流行的神话相反,子例程线程通常比直接线程慢”。然而,Ertl最近的测试[1]显示,在25个测试用例中,有15个子例程线程比直接线程更快。Ertl最近的测试表明,直接线程是Xeon、Opteron和Athlon处理器上最快的线程模型;间接线程是奔腾M处理器上最快的线程模型;子程序线程是奔腾4、奔腾III和PPC处理器上最快的线程模型。例如调用线程“push A, push B, add”:

thread:
  call pushA
  call pushB
  call add
  ret
pushA:
  *sp++ = A
  ret
pushB:
  *sp++ = B
  ret
add:
  addend = *--sp
  *sp++ = *--sp + addend
  ret

令牌线程:

令牌线程代码使用一个指针表的8位或12位索引列表。令牌线程代码非常紧凑,程序员不需要做太多特别的工作。它通常是其他线程代码大小的一半到四分之三,而其他线程代码本身就是编译代码大小的四分之一到八分之一。表的指针可以是间接的,也可以是直接的。一些Forth编译器生成令牌线程代码。一些程序员认为由一些Pascal编译器生成的“p-code”,以及. net、Java、BASIC和一些C编译器使用的字节码是令牌线程。历史上常用的方法是字节码,它使用8位操作码,通常是基于堆栈的虚拟机。一个典型的解释器称为“解码和分派解释器”,并遵循以下形式:

bytecode:
  0 /*pushA*/
  1 /*pushB*/
  2 /*add*/
top:
  i = decode(vpc++)
  addr = table[i]
  jump *addr
pushA:
  *sp++ = A
  jump top
pushB:
  *sp++ = B
  jump top
add:
  addend = *--sp
  *sp++ = *--sp + addend
  jump top

如果虚拟机只使用字节大小的指令,decode()只是从字节码获取数据,但是通常有常用的1字节指令加上一些不太常用的多字节指令,在这种情况下decode()更加复杂。单字节操作码的解码可以非常简单和有效地由一个分支表使用操作码直接作为索引来处理。对于单个操作简单的指令,如“push”和“add”,决定执行什么操作所涉及的开销要大于实际执行它的成本,因此这样的解释器通常比机器码慢得多。然而,对于更复杂的指令(“复合”),开销百分比在比例上不那么显著。

哈夫曼线程:

哈夫曼线程代码由存储为哈夫曼代码的令牌列表组成。哈夫曼码是一个可变长度的位串,用来识别一个唯一的令牌。哈夫曼线程解释器使用索引表或指针树来定位子例程,这些指针可以由哈夫曼代码导航。哈夫曼线程代码是计算机程序中最紧凑的表示形式之一。基本上,索引和代码是通过测量每个子程序在代码中出现的频率来组织的。频繁调用的代码最短。对于频率近似相等的运算,给出了位长近似相等的码。大多数huffman -thread系统都被实现为直接线程的Forth系统,用于将大量运行缓慢的代码打包到小型、廉价的微控制器中。大多数已发布的[5]应用都出现在智能卡、玩具、计算器和手表中。在PBASIC中使用的面向位的标记代码可以看作是一种哈夫曼线程代码。

Lesser-used threading:

字符串线程就是一个例子,其中操作由字符串标识,通常由哈希表查找。这在Charles H. Moore最早的Forth实现和伊利诺伊大学实验性硬件解释计算机语言中得到了应用。它也被用在Bashforth。

分支:

上面的例子没有显示分支。对于所有解释器,分支都会更改线程指针(上面的ip)。例如,当栈顶值为零时,条件分支可能被编码如下。注意,&thread[123]是要跳转到的位置,而不是处理程序的地址,因此必须跳过(ip++),无论是否采用分支。

thread:
  ...
  &brz
  &thread[123]
  ...
brz:
  tmp = ip++
  if (*sp++ == 0)
    ip = tmp
  jump *ip++

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值