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

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

It seems from what I have read that a thread is not really something concrete, like a process is. It is in fact just a concept. From what I understand of the way this works, a processor executes some commands for a program (which has been termed a thread of execution), then when it needs to switch to processing for some other program for a bit, it stores the state of the program it's currently executing for somewhere (Thread Local Storage) and then starts executing the other program's instructions. And back and forth. Such that, a thread is really just a concept for "one of the paths of execution" of a program that is currently running.

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

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

从维基百科:

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

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

编辑:显然,线程也有自己的调用堆栈,所以这是一个具体的东西。


当前回答

以下是来自雅虎的回答:

线程是一种编码构造 的体系结构不受影响 应用程序。单个过程 通常可能包含多个 线程。线程也可以直接 彼此沟通,因为他们 共享相同的变量。 进程是独立执行的 单位有自己的状态 信息。他们也用自己的 地址空间和只能交互 通过其他过程 进程间通信机制。

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

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

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

其他回答

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

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

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

过程

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

我们强调程序本身不是一个过程。程序是一个 被动的实体,如存储在磁盘上的包含指令列表的文件 (通常称为可执行文件)。

当我们启动一个应用程序时,我们创建了一个执行实例。这个执行实例称为流程。 编辑:(根据我的解释,类似于一个类和一个类的实例,类的实例是一个过程。)

进程的一个例子是谷歌Chrome。 当我们启动谷歌Chrome, 3个进程被衍生:

• The browser process is responsible for managing the user interface as well as disk and network I/O. A new browser process is created when Chrome is started. Only one browser process is created. • Renderer processes contain logic for rendering web pages. Thus, they contain the logic for handling HTML, Javascript, images, and so forth. As a general rule, a new renderer process is created for each website opened in a new tab, and so several renderer processes may be active at the same time. • A plug-in process is created for each type of plug-in (such as Flash or QuickTime) in use. Plug-in processes contain the code for the plug-in as well as additional code that enables the plug-in to communicate with associated renderer processes and the browser process.

线程

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

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

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

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

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

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

我对事物的解读

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

编辑: 其他我认为有用的信息,以提供更多的背景

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

并行计算:

从维基百科:

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

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

(1 + 3) + (4 + 5)

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

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

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

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

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

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

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

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

让我补充一句来自Edward Lee和Seshia的《嵌入式系统导论》:

Threads are imperative programs that run concurrently and share a memory space. They can access each others’ variables. Many practitioners in the field use the term “threads” more narrowly to refer to particular ways of constructing programs that share memory, [others] to broadly refer to any mechanism where imperative programs run concurrently and share memory. In this broad sense, threads exist in the form of interrupts on almost all microprocessors, even without any operating system at all (bare iron).

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

所以线程是一种抽象。

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

关于第一个问题:线程使实现多任务更容易。这背后的主要思想是,如果每一项任务都可以分配给唯一的员工,那么多任务处理就没有必要了。实际上,就目前而言,可以进一步概括这个定义,并说线程抽象代表一个虚拟工作者。

Now, imagine you have a robot that you want to give multiple tasks. Unfortunately, it can only execute a single, step by step task description. Well, if you want to make it multitask, you can try creating one big task description by interleaving the separate tasks you already have. This is a good start but the issue is that the robot sits at a desk and puts items on it while working. In order to get things right, you cannot just interleave instructions but also have to save and restore the items on the table.

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

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

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

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

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


但是等等,为什么我们首先关心多任务处理呢?

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

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


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

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

要问的一个基本问题是:如果调度器调度所有任务,并且调度器也是一个任务,那么谁调度调度器?

Let's brake this down. Say you write a scheduler, compile it and load it into the main memory of a computer at the address 1024, which happens to be the address that is loaded into the processor's instruction pointer when the computer is started. Now, your scheduler goes ahead and finds some tasks sitting precompiled in the main memory. For example, a task starts at the address 1,048,576. The scheduler wants to execute this task so it loads the task's address (1,048,576) into the instruction pointer. Huh, that was quite an ill considered move because now the scheduler has no way to regain control from the task it has just started.

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

A second approach is to ask the task authors to designate a few places where control can be transferred back to the scheduler. Note that the authors don't have to write the logic for saving the registers and inserting the jump instruction because it suffices that they mark the appropriate places and the scheduler takes care of the rest. This looks like a good idea because task authors probably know that, for example, their task will wait for a while after loading and starting a washing machine, so they let the scheduler take control there.

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

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


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

Arguably, operating systems feel like the right place to implement the thread abstraction because they control all the hardware components and can suspend and resume threads very wisely. If a thread requests the contents of a file stored on a hard drive from the operating system, it immediately knows that this operation will most likely take a while and can let another task occupy the processor in the meanwhile. Then, it can pause the current task and resume the one that made the request, once the file's contents are available.

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

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


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

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

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

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


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

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


Now, an interesting design decision is what threading interface a language exposes. Go, for example, provides a single interface that allows users to create hybrid threads, so called goroutines. There is no way to ask for, say, just a single kernel-level thread in Go. Other languages have separate interfaces for different kinds of threads. In Rust, kernel-level threads live in the standard library, while user-level and hybrid threads can be found in external libraries like async-std and tokio. In Python, the asyncio package provides user-level threads while multithreading and multiprocessing provide kernel-level threads. Interestingly, the threads multithreading provides cannot run in parallel. On the other hand, the threads multiprocessing provides can run in parallel but, as the library's name suggests, each kernel-level thread lives in a different process (virtual machine). This makes multiprocessing unsuitable for certain tasks because transferring data between different virtual machines is often slow.

更多资源:

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

并发不是罗伯派克的并行

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

Rust中的异步编程

Rust的异步转换内部

Rust的Async/Await之旅

你的功能是什么颜色?

为什么goroutine而不是线程?

为什么我的程序在更多cpu时运行得更快?

John Reese -用AsyncIO和Multiprocessing跳出GIL思考- PyCon 2018

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

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

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

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

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

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

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

CPU将:

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

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

if  x = 0
    compute_this()
else
    compute_that()

资源使用自计算机科学蒸馏。