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

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

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.

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

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

从维基百科:

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

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

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


当前回答

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

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

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

在一些现代Unix系统中,包括我最熟悉的Linux,一切都是线程——一个进程只是一种线程类型,它与其父线程共享相对较少的东西(例如,它有自己的内存映射,自己的文件表和权限等)。

其他回答

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

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

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

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

所以线程是一种抽象。

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

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

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

以下是来自雅虎的回答:

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

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

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

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

线程是一组可以被执行的(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()

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

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

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

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

在一些现代Unix系统中,包括我最熟悉的Linux,一切都是线程——一个进程只是一种线程类型,它与其父线程共享相对较少的东西(例如,它有自己的内存映射,自己的文件表和权限等)。