什么是“线程”?(真的)?

我一直在试图找到一个好的定义,并理解线程到底是什么。

似乎我一定遗漏了一些明显的东西,但每次我读到什么是线程,它几乎是一个循环的定义,比如“线程是执行的线程”或“一种划分运行任务的方法”。嗯嗯。嗯?

从我所读到的内容来看,线程并不像进程那样是真正的某物具体的。它实际上只是一个概念。根据我对这种工作方式的理解,处理器为一个程序执行一些命令(被称为执行线程),然后当它需要切换到处理其他一些程序时,它将当前正在执行的程序的状态存储到某个地方(线程本地存储),然后开始执行其他程序的指令。来来回回。因此,线程实际上只是当前正在运行的程序的“执行路径之一”的概念。

不像一个过程,它是一个资源的集合,等等。

作为一个定义的例子,它并没有真正帮助到我…

维基百科:

“在计算机科学中,线程是执行线程的缩写。线程是程序将自身分割(称为“分裂”)为两个或多个同时(或伪同时)运行的任务的一种方法。不同操作系统的线程和进程各不相同,但一般来说,线程包含在进程中,同一进程中的不同线程共享相同的资源,而同一多任务操作系统中的不同进程则不相同。”

我说的对吗?错了吗?线程到底是什么?

显然,线程也被赋予了自己的调用堆栈,因此这有点像具体的事情

182738 次浏览

不幸的是,线程确实存在。线是有形的东西。就算你杀了一个,其他的人还是会逃。您可以生成新的线程....虽然每个线程不是它自己的进程,但它们在进程中单独运行。在多核机器上,两个线程可以同时运行。

http://en.wikipedia.org/wiki/Simultaneous_multithreading

http://www.intel.com/intelpress/samples/mcp_samplech01.pdf

线程是处理器寄存器(对于单核)的一组独立值。因为这包括指令指针(又名程序计数器),它控制执行的顺序。它还包括堆栈指针,它最好为每个线程指向一个唯一的内存区域,否则它们会相互干扰。

线程是受控制流(函数调用、循环、goto)影响的软件单元,因为这些指令操作在指令指针上,而指令指针属于特定的线程。线程通常根据一些优先级方案进行调度(尽管可以设计一个系统,每个处理器核心有一个线程,在这种情况下,每个线程总是在运行,不需要调度)。

事实上,该指令指针的值和存储在该位置的指令足以确定该指令指针的新值。对于大多数指令来说,这只是按照指令的大小将IP向前推进,但是控制流指令以其他可预测的方式改变IP。IP值的序列形成了在程序代码中编织的执行路径,从而产生了“线程”的名称。

以下是来自雅虎的回答:

线程是一个编码构造 的体系结构不受影响 应用程序。单个过程 通常可能包含多个 线程。线程也可以直接 彼此沟通,因为他们 共享相同的变量。< / p >

进程是独立执行的 单位有自己的状态 信息。他们也用自己的 地址空间和只能交互 通过其他过程 进程间通信机制

然而,简单地说,线程就像不同的“任务”。想象一下,当你在做某事的时候,比如你在一张纸上写下一个公式。这可以看作是一个线程。然后另一个线程是你在另一张纸上写别的东西。这就是多任务处理的原因。

据说英特尔处理器有“超线程”(AMD也有),它意味着能够更好地执行多个“线程”或多任务。

我不确定如何处理线程的后勤。我确实记得听说过处理器在它们之间来回切换,但我不是100%确定,希望其他人能回答这个问题。

流程就像使用两台不同计算机的两个人,他们在必要时使用网络共享数据。线程就像使用同一台计算机的两个人,他们不必显式地共享数据,但必须小心地轮流使用。

从概念上讲,线程只是在同一个地址空间中忙碌的多个工蜂。每个线程都有自己的堆栈、程序计数器等等,但是一个进程中的所有线程都共享相同的内存。假设两个程序同时运行,但它们都可以访问相同的对象。

将此与流程进行对比。每个进程都有自己的地址空间,这意味着一个进程中的指针不能用于引用另一个进程中的对象(除非使用共享内存)。

我想需要理解的关键是:

  • 进程和线程可以“同时运行”。
  • 进程不共享内存(默认情况下),但是线程与同一进程中的其他线程共享它们的所有内存。
  • 进程中的每个线程都有自己的堆栈和指令指针。

在不同的系统和不同的实现中,答案是不同的,但最重要的部分是:

  1. 一个线程有一个独立的执行线程(也就是说,你可以从它的上下文切换,然后返回,它将继续运行在它原来的地方)。
  2. 一个线程有一个生命周期(它可以由另一个线程创建,另一个线程可以等待它完成)。
  3. 它可能比“过程”附带的包袱更少。

除此之外:线程可以由语言运行时在单个进程中实现,线程可以是协程,线程可以由线程库在单个进程中实现,或者线程可以是内核构造。

在一些现代Unix系统中,包括我最熟悉的Linux, 一切是线程——一个进程只是线程的类型,它与其父线程共享相对较少的东西(即它有自己的内存映射,自己的文件表和权限等)。读取man 2 clone,特别是标记列表,在这里非常有指导意义。

线程是一个执行上下文,它是CPU执行指令流所需的所有信息。

假设你正在阅读一本书,你现在想休息一下,但你希望能够回到你停下来的地方继续阅读。实现这一目标的一个方法是记下页码、行号和字数。所以你读书的执行环境就是这3个数字。

如果你有一个室友,她也在用同样的方法,她可以在你不用的时候把书拿走,然后从她停下来的地方继续读。然后你可以把它拿回来,从你刚才的地方重新开始。

线程以同样的方式工作。CPU会给你一种错觉,它会同时进行多个计算。它通过在每次计算上花费一些时间来做到这一点。它可以这样做,因为它有每个计算的执行上下文。就像您可以与您的朋友共享一本书一样,许多任务可以共享一个CPU。

在技术层面上,一个执行上下文(因此是一个线程)由CPU寄存器的值组成。

最后:线程与进程不同。线程是执行的上下文,而进程是与计算相关的一堆资源。一个进程可以有一个或多个线程。

说明:与进程相关的资源包括内存页(进程中的所有线程都有相同的内存视图)、文件描述符(例如,打开的套接字)和安全凭证(例如,启动进程的用户ID)。

线程不过是一个具有执行规则的内存上下文(Tanenbaum更恰当的说法是资源分组)。这是一个软件结构。CPU不知道线程是什么(有些例外,有些处理器有硬件线程),它只是执行指令。

内核引入了线程和进程的概念,以有意义的方式管理内存和指令的顺序。

我将使用ABRAHAM SILBERSCHATZ、PETER BAER GALVIN和GREG GAGNE所著的《操作系统概念》一书中的大量文本以及我自己对事物的理解。

过程

任何应用程序都以文本(或代码)的形式驻留在计算机中。

我们强调程序本身不是进程。程序是一个 被动的实体,如存储在磁盘上的包含指令列表的文件

.(通常称为可执行文件)
当我们启动一个应用程序时,我们创建了一个执行实例。这个执行实例称为流程。 编辑:(根据我的解释,类似于一个类和一个类的实例,类的实例是一个过程。)< / p > 进程的一个例子是谷歌Chrome。 当我们启动谷歌Chrome时,生成了3个进程:

浏览器进程负责管理用户界面 以及磁盘和网络I/O。创建一个新的浏览器进程时 已启动Chrome浏览器。只创建了一个浏览器进程。< / p >

渲染器进程包含渲染网页的逻辑。因此,他们 包含处理HTML、Javascript、图像等的逻辑。 作为一般规则,每个网站都会创建一个新的渲染器进程 在新选项卡中打开,因此几个呈现程序进程可能是活动的 同时。

. 插件进程是为每一种类型的插件(如Flash . exe)创建的 或QuickTime)。插件进程包含用于 插件以及使插件能够 与相关的呈现程序进程和浏览器通信 过程。< / p >

线程

要回答这个问题,我认为你首先应该知道什么是处理器。处理器是实际执行计算的硬件。 EDIT:(计算,如两个数字相加,排序数组,基本上执行已编写的代码)

现在开始讨论线程的定义。

一个线程是CPU利用率的基本单位;它包括一个线程ID,一个程序 计数器,寄存器集和堆栈

编辑:来自英特尔网站的线程定义:

线程(Thread)或执行线程(Thread of execution)是一个软件术语,指的是指令的基本有序序列,可以由单个CPU内核传递或处理。

因此,如果Chrome应用程序的Renderer进程对数字数组进行排序,排序将发生在执行的线程/线程上。(关于线程的语法让我感到困惑)

我对事物的解读

流程是一个执行实例。线程是通过CPU访问执行计算的实际工作者。当一个进程有多个线程运行时,该进程提供公共内存。

< p >编辑: 其他信息,我发现有用的,以提供更多的上下文

所有现代计算机都有多个线程。计算机中的线程数取决于计算机的核数。

并行计算:

从维基百科:

并发计算是一种计算形式,在这种计算中,多个计算在重叠的时间段内(并发地)执行,而不是按顺序执行(一个在下一个开始之前完成)。这是一个系统的属性——可以是一个单独的程序、一台计算机或一个网络——每个计算(“进程”)都有一个单独的执行点或“控制线程”。

所以,我可以写一个程序来计算4个数字的和:

(1 + 3) + (4 + 5)

在程序中计算这个和(这将是一个运行在执行线程上的进程),我可以fork另一个进程,它可以在不同的线程上运行来计算(4 + 5)并将结果返回给原始进程,而原始进程计算(1 + 3)的和。

为了正式地定义线程,我们必须首先了解线程操作的边界。

当计算机程序从某个存储器加载到计算机内存并开始执行时,它就成为过程。一个进程可以由一个处理器或一组处理器执行。内存中的进程描述包含重要信息,如程序计数器,它跟踪程序中的当前位置(即当前正在执行的指令),寄存器,变量存储,文件句柄,信号,等等。

线程是程序中这样的指令序列,可以独立于其他代码执行。如图所示为概念: enter image description here < / p >

线程在相同的进程地址空间中,因此,进程内存描述中的大部分信息可以跨线程共享。

有些信息不能复制,比如堆栈(每个线程指向不同内存区域的堆栈指针)、寄存器和特定于线程的数据。此信息足以允许线程独立调度是程序主线程的此信息足以允许线程独立调度,也可能是程序中的一个或多个其他线程。

运行多线程程序需要明确的操作系统支持。幸运的是,大多数现代操作系统都支持线程,如Linux(通过NPTL)、BSD变体、Mac OS X、Windows、Solaris、AIX、HP-UX等。操作系统可以使用不同的机制来实现多线程支持。

这里,图形化地表示了概念。 < / >

在这里,你可以找到关于这个主题的更多信息。这也是我的信息来源。

让我添加一个句子,来自嵌入式系统概论,由爱德华•李Seshia:

线程是并行运行并共享内存空间的命令式程序。它们可以访问彼此的变量。该领域的许多从业者使用术语“线程”更狭义地指构建共享内存的程序的特定方法,[其他人]广泛地指命令式程序并发运行并共享内存的任何机制。从广义上讲,线程以中断的形式存在于几乎所有的微处理器上,即使没有任何操作系统(裸机)。

线程是可以被执行的(CPU)指令)的

但是为了更好地理解线程是什么,需要一些计算机体系结构知识。

计算机所做的就是按照指令操作数据。 RAM是指令和数据保存的地方,处理器使用这些指令对保存的数据执行操作。

CPU有一些内部存储单元,称为寄存器。它可以对存储在这些寄存器中的数字进行简单的数学运算。它还可以在RAM和这些寄存器之间移动数据。这些是CPU可以被指示执行的典型操作的例子:

  • 将数据从内存位置#220复制到寄存器#3
  • 将寄存器#3中的数字与寄存器#1中的数字相加。

CPU能做的所有操作的集合称为指令集。指令集中的每个操作都被分配了一个编号。计算机代码本质上是表示CPU操作的数字序列。这些操作以数字的形式存储在RAM中。我们存储输入/输出数据、部分计算和计算机代码,所有这些都混合在RAM中。

CPU工作在一个没有结束的循环中,总是从内存中获取和执行指令。这个循环的核心是电脑注册程序计数器。它是一个特殊的寄存器,存储下一条要执行的指令的内存地址。

CPU将:

  1. 从PC给出的内存地址处获取指令,
  2. PC加1,
  3. 执行指令,
  4. 回到步骤1。

CPU可以被指示写一个新的值到PC,导致执行分支,或“跳转”。回到记忆中的某个地方。这种分支可以是有条件的。例如,一条CPU指令可以说:“如果寄存器#1 = 0,则将PC设置为地址#200”。这允许计算机执行如下内容:

if  x = 0
compute_this()
else
compute_that()

计算机科学精华使用的资源。

就像一个进程代表一个虚拟计算机一样,线程 抽象表示一个虚拟处理器

所以线程是抽象

抽象降低了复杂性。因此,第一个问题是线程解决什么问题。第二个问题是如何实施。

关于第一个问题:线程使实现多任务处理更容易。实际上,就目前而言,可以进一步概括定义,并说线程抽象代表一个虚拟工作者。

现在,假设你有一个机器人,你想给它多个任务。不幸的是,它只能执行一个单独的、一步一步的任务描述。好吧,如果你想让它多任务,你可以试着通过交织你已经有的独立任务来创建一个大任务描述。这是一个好的开始,但问题是机器人坐在桌子前工作时把东西放在上面。为了使事情正确,您不能只是穿插指令,还必须保存和恢复表上的项目。

这是可行的,但现在仅通过查看您创建的大任务描述就很难将单独的任务分开。此外,保存和恢复表上的项的过程很乏味,并且使任务描述更加混乱。

这就是线程抽象的用武之地。它让你假设你有无数个机器人,每个机器人都坐在不同的房间里自己的办公桌前。现在,你可以把任务描述扔到一个罐子里,其他的一切都由线程抽象的实现者负责。还记得吗?如果有足够多的工人,没有人需要一心多用。

通常,表明你的视角是有用的,说robot是指真正的机器人,虚拟机器人是指线程抽象提供给你的机器人。

至此,在任务完全独立的情况下,多任务处理问题得到了解决。然而,让机器人走出自己的房间,相互交流,朝着一个共同的目标努力,这不是很好吗?你可能已经猜到了,这需要协调。红绿灯,排队,应有尽有。

作为一种中间总结,线程抽象解决了多任务处理的问题,并为合作创造了机会。没有它,我们只有一个机器人,所以合作是不可想象的。但是,它也给我们带来了协调(同步)的问题。现在我们知道了胎面抽象解决了什么问题,作为奖励,我们也知道了它带来了什么新挑战。


但是等等,为什么我们一开始就关心多任务处理呢?

首先,如果任务涉及等待,多任务处理可以提高性能。例如,当洗衣机在运转时,你可以很容易地开始准备晚餐。当你的晚餐在上面的时候,你可以把衣服晾出来。请注意,在这里等待是因为一个独立的组件为您完成了这项工作。涉及等待的任务被称为I / O绑定任务。

其次,如果多任务处理是快速完成的,从鸟瞰的角度来看,它就表现为并行性。这有点像人眼将一系列静止图像感知为快速连续显示的运动图像。如果我给Alice写了一秒给Bob也写了一秒,你能分辨出这两封信是同时写的还是交替写的吗,如果你只看我每两秒在做什么?搜索多任务操作系统以获得更多信息。


现在,让我们关注如何实现线程抽象。的问题

从本质上讲,实现线程抽象就是编写一个任务,一个主任务,它负责调度所有其他任务。

要问的一个基本问题是:如果调度器调度所有任务,并且调度器也是一个任务,则谁安排调度程序? .

让我们冷静下来。假设你编写了一个调度器,编译它并将它加载到计算机的主存中,地址是1024,这恰好是计算机启动时加载到处理器指令指针中的地址。现在,您的调度程序继续执行,并在主内存中找到一些预编译的任务。例如,任务从地址1,048,576开始。调度器想要执行这个任务,所以它将任务的地址(1,048,576)加载到指令指针中。嗯,这是一个考虑不周全的举动,因为现在调度程序没有办法从它刚刚启动的任务中重新获得控制。

一种解决方案是在执行之前在任务描述中插入到调度程序(地址1024)的跳转指令。实际上,你不应该忘记保存机器人正在工作的桌子上的物品,所以在跳跃之前你还必须保存处理器的寄存器。这里的问题是,很难判断在哪里插入跳转指令。如果它们太多,就会产生过多的开销;如果它们太少,就会有一个任务独占处理器。

第二种方法是要求任务作者指定几个可以将控制转移回调度器的位置。请注意,作者不必编写保存寄存器和插入跳转指令的逻辑,因为他们标记了适当的位置,而调度器负责其余的工作就足够了。这看起来是个好主意,因为任务作者可能知道,例如,他们的任务在加载和启动洗衣机后将等待一段时间,因此他们让调度程序在那里进行控制。

上述两种方法都无法解决错误或恶意任务的问题,例如,它陷入无限循环,无法跳转到调度器所在的地址。

现在,如果你不能用软件解决问题该怎么办?在硬件上解决!所需要的是一个连接到处理器上的可编程电路,就像一个闹钟一样。调度器设置一个定时器和它的地址(1024),当定时器用完时,告警保存寄存器并将指令指针设置为调度器所在的地址。这种方法称为抢占式调度。


现在您可能已经开始意识到实现线程抽象与实现链表不同。线程抽象最著名的实现者是操作系统。它们提供的线程有时被称为内核级线程。由于操作系统无法承受失去控制的后果,所有主要的通用操作系统都使用抢占式调度。

可以说,操作系统是实现线程抽象的合适地方,因为它们控制所有硬件组件,可以非常明智地挂起和恢复线程。如果一个线程从操作系统请求存储在硬盘上的文件的内容,它立即知道这个操作很可能需要一段时间,并且可能会让另一个任务同时占用处理器。然后,一旦文件的内容可用,它就可以暂停当前任务并恢复发出请求的任务。

然而,故事并未就此结束,因为线程也可以在用户空间中实现。这些实现者通常是编译器。有趣的是,据我所知,内核级线程是线程所能得到的最强大的线程。那么我们为什么要用用户级线程呢?原因当然是性能。用户级线程更轻量级,因此您可以创建更多的线程,通常暂停和恢复线程的开销很小。

用户级线程可以使用async/await实现。您还记得实现控制返回调度程序的一个选项是让任务作者指定可以发生转换的位置吗?好吧,asyncawait关键字正是用于此目的。


现在,如果你已经做到了这一步,请做好准备,因为真正的乐趣即将到来!

你有没有注意到我们几乎没有谈论并行性?我的意思是,我们不是使用线程并行地运行相关的计算,从而提高吞吐量吗?嗯,不安静..实际上,如果你只想要并行,你根本不需要这个机制。您只需创建与您拥有的处理单元数量一样多的任务,并且没有任何任务必须暂停或恢复。你甚至不需要调度程序,因为你不需要一心多用。

在某种意义上,并行是一个实现细节。如果您仔细想想,线程抽象的实现者可以在底层使用尽可能多的处理器。你可以从1950年开始编译一些编写良好的多线程代码,在今天的多核上运行,然后看到它利用了所有的核。重要的是,编写这段代码的程序员可能没有预料到这段代码会在多核上运行。

您甚至可以认为,当线程被用于实现并行性时,它们被滥用了:即使人们知道他们不需要多任务这一核心功能,但他们还是使用线程来获得并行性。


最后需要注意的是,用户级线程本身不能提供并行性。还记得开头的那句话吗?操作系统在虚拟计算机(进程)中运行程序,默认情况下通常配备单个虚拟处理器(线程)。无论您在用户空间中使用什么魔法,如果您的虚拟计算机只有一个虚拟处理器,那么您就不能并行运行代码。

我们想要什么?当然,我们需要并行性。但是我们也需要轻量级线程。因此,许多线程抽象的实现者开始使用混合方法:他们启动与硬件中处理单元一样多的内核级线程,并在一些内核级线程之上运行许多用户级线程。本质上,并行由内核级处理,多任务由用户级线程处理。


现在,一个有趣的设计决策是语言所公开的线程接口。例如,Go提供了一个单一的接口,允许用户创建混合线程,称为goroutines。在Go中,我们不可能只要求一个内核级线程。其他语言为不同类型的线程提供了单独的接口。在Rust中,内核级线程存在于标准库中,而用户级和混合线程可以在诸如async-stdtokio这样的外部库中找到。在Python中,asyncio包提供用户级线程,而multithreadingmultiprocessing包提供内核级线程。有趣的是,multithreading提供的线程不能并行运行。另一方面,multiprocessing提供的线程可以并行运行,但正如库的名字所暗示的那样,每个内核级线程都位于不同的进程(虚拟机)中。这使得multiprocessing不适合某些任务,因为在不同虚拟机之间传输数据通常很慢。

更多资源:

操作系统:原理和实践,作者:Thomas and Anderson

并发不是并行罗伯派克

并行和并发需要不同的工具

Rust异步编程

在Rust的Async转换中

Rust的Async/Await之旅

你的函数是什么颜色?< / >

为什么用goroutines代替thread ?< / >

为什么我的程序在更多的cpu下运行得更快?< / >

John Reese - Thinking Outside GIL with AsyncIO and Multiprocessing - PyCon 2018