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

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

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.

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

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

从维基百科:

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

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

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


当前回答

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

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

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

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

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

运行多线程程序需要明确的操作系统支持。幸运的是,大多数现代操作系统都支持线程,如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).

其他回答

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

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

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

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

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

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

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

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

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

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

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