什么是“线程”? (真的吗)?
我一直在努力寻找一个好的定义,并理解线程到底是什么。
看来我一定错过了一些明显的东西,但每次我读到线程是什么时,它几乎都是一个循环定义,就像“线程是执行线程”或“划分运行任务的一种方式”。呃呃。啊?
从我读到的内容看来,线程并不像进程那样具体。其实这只是一个概念。根据我对其工作方式的理解,处理器为程序执行一些命令(称为执行线程),然后当它需要切换到处理其他程序时位,它存储当前正在某处(线程本地存储)执行的程序的状态,然后开始执行其他程序的指令。来回。这样,线程实际上只是当前正在运行的程序的“执行路径之一”的概念。
与流程不同,流程确实是某种东西 - 它是资源的集合,等等。
作为定义的示例并没有真正帮助我太多。 。 。
来自维基百科:
“计算机科学中的线程是执行线程的缩写。线程是程序将自身划分(称为“分割”)为两个或多个同时(或伪同时)运行的任务的一种方式。线程和进程不同的操作系统有所不同,但一般来说,线程包含在进程内,同一进程中的不同线程共享相同的资源,而同一多任务操作系统中的不同进程则不共享相同的资源。”
那么我说得对吗?错误的?线程到底是什么?
编辑:显然,线程也有自己的调用堆栈,因此这在某种程度上是一个具体的事物。
I have been trying to find a good definition, and get an understanding, of what a thread really is.
It seems that I must be missing something obvious, but every time I read about what a thread is, it's almost a circular definition, a la "a thread is a thread of execution" or " a way to divide into running tasks". Uh uh. Huh?
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.
Unlike a process, which really is something - it is a conglomeration of resources, etc.
As an example of a definition that didn't really help me much . . .
From Wikipedia:
"A thread in computer science is short for a thread of execution. Threads are a way for a program to divide (termed "split") itself into two or more simultaneously (or pseudo-simultaneously) running tasks. Threads and processes differ from one operating system to another but, in general, a thread is contained inside a process and different threads in the same process share same resources while different processes in the same multitasking operating system do not."
So am I right? Wrong? What is a thread really?
Edit: Apparently a thread is also given its own call stack, so that is somewhat of a concrete thing.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(12)
线程是一个执行上下文,它是 CPU 执行指令流所需的所有信息。
假设您正在阅读一本书,并且您想立即休息一下,但您希望能够返回并从您停止的确切位置继续阅读。实现此目的的一种方法是记下页码、行号和字数。所以你阅读一本书的执行上下文就是这 3 个数字。
如果你有一个室友,并且她正在使用相同的技术,那么她可以在你不使用这本书时拿走这本书,并从她停止的地方继续阅读。然后您可以将其收回并从原来的位置恢复。
线程以同样的方式工作。 CPU 会给您一种它正在同时进行多项计算的错觉。它通过在每次计算上花费一些时间来实现这一点。它可以做到这一点,因为它为每个计算都有一个执行上下文。就像您可以与朋友共享一本书一样,许多任务可以共享一个 CPU。
在更技术的层面上,执行上下文(因此是线程)由 CPU 寄存器的值组成。
最后:线程与进程不同。线程是执行上下文,而进程是与计算相关的一堆资源。一个进程可以有一个或多个线程。
澄清:与进程相关的资源包括内存页(进程中的所有线程都具有相同的内存视图)、文件描述符(例如,打开的套接字)和安全凭证(例如,启动该进程的用户的 ID)。过程)。
A thread is an execution context, which is all the information a CPU needs to execute a stream of instructions.
Suppose you're reading a book, and you want to take a break right now, but you want to be able to come back and resume reading from the exact point where you stopped. One way to achieve that is by jotting down the page number, line number, and word number. So your execution context for reading a book is these 3 numbers.
If you have a roommate, and she's using the same technique, she can take the book while you're not using it, and resume reading from where she stopped. Then you can take it back, and resume it from where you were.
Threads work in the same way. A CPU is giving you the illusion that it's doing multiple computations at the same time. It does that by spending a bit of time on each computation. It can do that because it has an execution context for each computation. Just like you can share a book with your friend, many tasks can share a CPU.
On a more technical level, an execution context (therefore a thread) consists of the values of the CPU's registers.
Last: threads are different from processes. A thread is a context of execution, while a process is a bunch of resources associated with a computation. A process can have one or many threads.
Clarification: the resources associated with a process include memory pages (all the threads in a process have the same view of the memory), file descriptors (e.g., open sockets), and security credentials (e.g., the ID of the user who started the process).
线程是处理器寄存器(对于单核)的一组独立值。由于这包括指令指针(又名程序计数器),因此它控制按什么顺序执行。它还包括堆栈指针,它最好指向每个线程的唯一内存区域,否则它们会相互干扰。
线程是受控制流(函数调用、循环、goto)影响的软件单元,因为这些指令对指令指针进行操作,并且属于特定线程。线程通常根据某种优先级方案进行调度(尽管可以设计一个每个处理器核心一个线程的系统,在这种情况下,每个线程始终运行并且不需要调度)。
事实上,指令指针的值和存储在该位置的指令足以确定指令指针的新值。对于大多数指令,这只是将 IP 增加指令的大小,但控制流指令会以其他可预测的方式更改 IP。 IP 所采用的值序列形成了贯穿程序代码的执行路径,从而产生了“线程”这个名称。
A thread is an independent set of values for the processor registers (for a single core). Since this includes the Instruction Pointer (aka Program Counter), it controls what executes in what order. It also includes the Stack Pointer, which had better point to a unique area of memory for each thread or else they will interfere with each other.
Threads are the software unit affected by control flow (function call, loop, goto), because those instructions operate on the Instruction Pointer, and that belongs to a particular thread. Threads are often scheduled according to some prioritization scheme (although it's possible to design a system with one thread per processor core, in which case every thread is always running and no scheduling is needed).
In fact the value of the Instruction Pointer and the instruction stored at that location is sufficient to determine a new value for the Instruction Pointer. For most instructions, this simply advances the IP by the size of the instruction, but control flow instructions change the IP in other, predictable ways. The sequence of values the IP takes on forms a path of execution weaving through the program code, giving rise to the name "thread".
为了正式定义线程,我们必须首先了解线程操作的边界。
当计算机程序从某个存储加载到计算机内存并开始执行时,它就成为一个进程。进程可以由一个处理器或一组处理器执行。内存中的进程描述包含重要信息,例如跟踪程序中当前位置(即当前正在执行哪条指令)的程序计数器、寄存器、变量存储、文件句柄、信号等。
线程是程序中可以独立于其他代码执行的一系列指令。该图展示了这个概念:
线程位于同一进程地址空间内,因此,进程内存描述中存在的许多信息可以在线程之间共享。
有些信息无法复制,例如堆栈(指向每个线程不同内存区域的堆栈指针)、寄存器和线程特定数据。 这一信息足以允许线程独立于程序的主线程以及程序内的一个或多个其他线程进行调度。
运行多线程程序需要显式操作系统支持。幸运的是,大多数现代操作系统都支持线程,例如 Linux(通过 NPTL)、BSD 变体、Mac OS X、Windows、Solaris、AIX、HP-UX 等。操作系统可能使用不同的机制来实现多线程支持。
在这里,您可以找到有关以下内容的更多信息:主题。这也是我的信息来源。
让我添加一句来自嵌入式系统简介作者:Edward Lee 和 Sanjit Seshia:
In order to define a thread formally, we must first understand the boundaries of where a thread operates.
A computer program becomes a process when it is loaded from some store into the computer's memory and begins execution. A process can be executed by a processor or a set of processors. A process description in memory contains vital information such as the program counter which keeps track of the current position in the program (i.e. which instruction is currently being executed), registers, variable stores, file handles, signals, and so forth.
A thread is a sequence of such instructions within a program that can be executed independently of other code. The figure shows the concept:
Threads are within the same process address space, thus, much of the information present in the memory description of the process can be shared across threads.
Some information cannot be replicated, such as the stack (stack pointer to a different memory area per thread), registers and thread-specific data. This information suffices to allow threads to be scheduled independently of the program's main thread and possibly one or more other threads within the program.
Explicit operating system support is required to run multithreaded programs. Fortunately, most modern operating systems support threads such as Linux (via NPTL), BSD variants, Mac OS X, Windows, Solaris, AIX, HP-UX, etc. Operating systems may use different mechanisms to implement multithreading support.
Here, you can find more information about the topic. That was also my information-source.
Let me just add a sentence coming from Introduction to Embedded Systems by Edward Lee and Sanjit Seshia:
进程就像两个人使用两台不同的计算机,在必要时使用网络来共享数据。线程就像使用同一台计算机的两个人,他们不必显式地共享数据,但必须小心地轮流。
从概念上讲,线程只是在同一地址空间中嗡嗡作响的多只工蜂。每个线程都有自己的堆栈、自己的程序计数器等,但进程中的所有线程共享相同的内存。想象两个程序同时运行,但它们都可以访问相同的对象。
将此与流程进行对比。每个进程都有自己的地址空间,这意味着一个进程中的指针不能用于引用另一个进程中的对象(除非您使用共享内存)。
我想要理解的关键是:
Processes are like two people using two different computers, who use the network to share data when necessary. Threads are like two people using the same computer, who don't have to share data explicitly but must carefully take turns.
Conceptually, threads are just multiple worker bees buzzing around in the same address space. Each thread has its own stack, its own program counter, etc., but all threads in a process share the same memory. Imagine two programs running at the same time, but they both can access the same objects.
Contrast this with processes. Processes each have their own address space, meaning a pointer in one process cannot be used to refer to an object in another (unless you use shared memory).
I guess the key things to understand are:
我将使用 ABRAHAM SILBERSCHATZ、PETER BAER GALVIN 和 GREG GAGNE 所著的《Operating Systems Concepts》一书中的大量文字以及我自己对事物的理解。
进程
任何应用程序都以文本(或代码)的形式驻留在计算机中。
当我们启动一个应用程序时,我们会创建一个执行实例。这个执行实例称为进程。
编辑:(根据我的解释,类似于类和类的实例,类的实例是一个进程。)
进程的一个例子是 Google Chrome。
当我们启动 Google Chrome 时,会生成 3 个进程:
线程
要回答这个问题,我认为你首先应该知道处理器是什么。处理器是实际执行计算的硬件。
编辑:(诸如添加两个数字、对数组进行排序、基本上执行已编写的代码之类的计算)
现在继续讨论线程的定义。
编辑:来自英特尔网站的线程定义:
因此,如果 Chrome 应用程序的渲染器进程对数字数组进行排序,则排序将在执行线程上进行。 (关于线程的语法对我来说似乎很混乱)
我对事物的解释
进程是一个执行实例。线程是通过 CPU 访问执行计算的实际工作人员。当一个进程运行多个线程时,该进程提供公共内存。
编辑:
我发现有助于提供更多背景信息的其他信息
所有现代计算机都有多个线程。计算机中的线程数量取决于计算机中的核心数量。
并发计算:
来自维基百科:
因此,我可以编写一个计算 4 个数字之和的程序:
在计算这个和的程序中(这将是一个在执行线程上运行的进程),我可以派生另一个可以在不同线程上运行的进程来计算(4 + 5)并将结果返回给原进程,而原进程计算(1 + 3)的总和。
I am going to use a lot of text from the book Operating Systems Concepts by ABRAHAM SILBERSCHATZ, PETER BAER GALVIN and GREG GAGNE along with my own understanding of things.
Process
Any application resides in the computer in the form of text (or code).
When we start an application, we create an instance of execution. This instance of execution is called a process.
EDIT:(As per my interpretation, analogous to a class and an instance of a class, the instance of a class being a process. )
An example of processes is that of Google Chrome.
When we start Google Chrome, 3 processes are spawned:
Thread
To answer this I think you should first know what a processor is. A Processor is the piece of hardware that actually performs the computations.
EDIT: (Computations like adding two numbers, sorting an array, basically executing the code that has been written)
Now moving on to the definition of a thread.
EDIT: Definition of a thread from intel's website:
So, if the Renderer process from the Chrome application sorts an array of numbers, the sorting will take place on a thread/thread of execution. (The grammar regarding threads seems confusing to me)
My Interpretation of Things
A process is an execution instance. Threads are the actual workers that perform the computations via CPU access. When there are multiple threads running for a process, the process provides common memory.
EDIT:
Other Information that I found useful to give more context
All modern day computer have more than one threads. The number of threads in a computer depends on the number of cores in a computer.
Concurrent Computing:
From Wikipedia:
So, I could write a program which calculates the sum of 4 numbers:
In the program to compute this sum (which will be one process running on a thread of execution) I can fork another process which can run on a different thread to compute (4 + 5) and return the result to the original process, while the original process calculates the sum of (1 + 3).
线程只不过是具有执行规则的内存上下文(或者 Tanenbaum 更好的说法是资源分组)。这是一个软件构造。 CPU不知道线程是什么(这里有一些例外,一些处理器有硬件线程),它只是执行指令。
内核引入了线程和进程的概念,以有意义的方式管理内存和指令顺序。
A thread is nothing more than a memory context (or how Tanenbaum better puts it, resource grouping) with execution rules. It's a software construct. The CPU has no idea what a thread is (some exceptions here, some processors have hardware threads), it just executes instructions.
The kernel introduces the thread and process concept to manage the memory and instructions order in a meaningful way.
线程是一组可以执行的(CPU)指令。
但为了更好地理解什么是线程,需要一些计算机体系结构知识。
计算机的作用是遵循指令并操作数据。
RAM是保存指令和数据的地方,处理器使用这些指令对保存的数据进行操作。
CPU有一些称为寄存器的内部存储单元。它可以使用这些寄存器中存储的数字执行简单的数学运算。它还可以在 RAM 和这些寄存器之间移动数据。这些是可以指示 CPU 执行的典型操作的示例:
CPU 可以执行的所有操作的集合称为指令集。指令集中的每个操作都分配有一个编号。计算机代码本质上是代表CPU操作的数字序列,这些数字存储在RAM中。
CPU 以永无休止的循环方式工作,始终从内存中获取并执行指令。该周期的核心是PC寄存器,或程序计数器。它是一个特殊的寄存器,用于存储下一条要执行的指令的内存地址。
CPU 将:
可以指示 CPU 将新值写入 PC,从而导致执行分支,或“跳转”到内存中的其他位置。并且这种分支可以是有条件的。例如,CPU 指令可能会说:“如果寄存器 #1 等于 0,则将 PC 设置为地址 #200”。这允许计算机执行如下内容:
使用来自计算机科学提炼的资源。
A thread is a set of (CPU)instructions which can be executed.
But in order to have a better understanding of what a thread is, some computer architecture knowledge is required.
What a computer does, is to follow instructions and manipulate data.
RAM is the place where the instructions and data are saved, the processor uses those instructions to perform operations on the saved data.
The CPU has some internal memory cells called, registers. It can perform simple mathematical operations with numbers stored in these registers. It can also move data between the RAM and these registers. These are examples of typical operations a CPU can be instructed to execute:
The collection of all operations a CPU can do is called instruction set. Each operation in the instruction set is assigned a number. Computer code is essentially a sequence of numbers representing CPU operations, these numbers are stored in RAM.
The CPU works in a never-ending loop, always fetching and executing an instruction from memory. At the core of this cycle is the PC register, or Program Counter. It's a special register that stores the memory address of the next instruction to be executed.
The CPU will:
The CPU can be instructed to write a new value to the PC, causing the execution to branch, or "jump" to somewhere else in the memory. And this branching can be conditional. For instance, a CPU instruction could say: "set PC to address #200 if register #1 equals zero". This allows computers to execute stuff like this:
Resources used from Computer Science Distilled.
这是摘自雅虎答案:
然而,简单地说,线程就像不同的“任务”。因此,想想当你在做某事时,例如你正在一张纸上写下一个公式。这可以被认为是一个线程。然后另一个线程是你在另一张纸上写别的东西。这就是多任务的用武之地。
据说英特尔处理器具有“超线程”(AMD 也有),它意味着能够更好地执行多个“线程”或多任务。
我不确定如何处理线程的逻辑。我确实记得听说过处理器在它们之间来回移动,但我对此并不能 100% 确定,希望其他人可以回答这个问题。
This was taken from a Yahoo Answer:
However, to put in simpler terms threads are like different "tasks". So think of when you are doing something, for instance you are writing down a formula on one paper. That can be considered one thread. Then another thread is you writing something else on another piece of paper. That is where multitasking comes in.
Intel processors are said to have "hyper-threading" (AMD has it too) and it is meant to be able to perform multiple "threads" or multitask much better.
I am not sure about the logistics of how a thread is handled. I do recall hearing about the processor going back and forth between them, but I am not 100% sure about this and hopefully somebody else can answer that.
不幸的是,线程确实存在。线程是有形的东西。你可以杀死一个,其他的仍然会跑。你可以产生新的线程......虽然每个线程不是它自己的进程,但它们在进程内单独运行。在多核机器上,2个线程可以同时运行。
http://en.wikipedia.org/wiki/Simultaneous_multithreading
http://www.intel.com/intelpress/samples/mcp_samplech01.pdf
Unfortunately, threads do exist. A thread is something tangible. You can kill one, and the others will still be running. You can spawn new threads.... although each thread is not its own process, they are running separately inside the process. On multi-core machines, 2 threads could run at the same time.
http://en.wikipedia.org/wiki/Simultaneous_multithreading
http://www.intel.com/intelpress/samples/mcp_samplech01.pdf
在不同的系统和不同的实现中,答案有很大的不同,但最重要的部分是:
除此之外:线程可以由语言运行时在单个进程中实现,线程可以是协程,线程可以由线程库在单个进程中实现,或者线程可以是内核构造。
在几个现代 Unix 系统中,包括我最熟悉的 Linux,一切都是线程——进程只是线程的一种类型,与其共享相对较少的东西。父级(即它拥有自己的内存映射、自己的文件表和权限等)。阅读
man 2 clone
,尤其是标志列表,在这里确实很有启发性。The answer varies hugely across different systems and different implementations, but the most important parts are:
Beyond that: threads could be implemented within a single process by a language runtime, threads could be coroutines, threads could be implemented within a single process by a threading library, or threads could be a kernel construct.
In several modern Unix systems, including Linux which I'm most familiar with, everything is threads -- a process is merely a type of thread that shares relatively few things with its parent (i.e. it gets its own memory mappings, its own file table and permissions, etc.) Reading
man 2 clone
, especially the list of flags, is really instructive here.因此线程是一个抽象。
抽象降低了复杂性。因此,第一个问题是线程解决什么问题。第二个问题是如何实施它们。
关于第一个问题:线程使多任务的实现变得更加容易。 这背后的主要思想是,如果每个任务都可以分配给一个唯一的工作者,那么多任务处理是不必要的。实际上,暂时可以进一步概括这个定义,并说线程抽象表示虚拟工作者。
现在,假设您有一个机器人,您想要为其分配多项任务。不幸的是,它只能执行单个、逐步的任务描述。好吧,如果你想让它成为多任务,你可以尝试通过交错已有的单独任务来创建一个大任务描述。这是一个好的开始,但问题是机器人坐在桌子前,工作时将物品放在上面。为了把事情做好,你不能只是交错指令,还必须保存和恢复桌子上的项目。
这可行,但现在很难通过简单地查看您创建的大任务描述来理清单独的任务。此外,保存和恢复表上项目的仪式很乏味,并且进一步使任务描述变得混乱。
这就是线程抽象发挥作用并拯救世界的地方。 它让你假设你有无限数量的机器人,每个机器人都坐在不同的房间里,有自己的桌子。现在,你只需将任务描述扔进锅里,其他一切都由机器人来处理线程抽象的实现者。记住?如果有足够的工人,就没有人需要同时处理多项任务。
通常,表明您的观点很有用,并说机器人表示真正的机器人,而使用虚拟机器人表示线程抽象为您提供的机器人。
至此,任务完全独立的情况下的多任务问题就解决了。然而,让机器人走出自己的房间,进行互动并为共同的目标而共同努力不是很好吗?嗯,正如您可能猜到的,这需要协调。红绿灯、队列——凡是你能想到的。
作为中间总结,线程抽象解决了多任务的问题,并创造了合作的机会。没有它,我们只有一个机器人,所以合作是不可想象的。但是,这也给我们带来了协调(同步)的问题。现在我们知道胎面抽象解决了什么问题,而且作为奖励,我们还知道它带来了什么新挑战。
但是等等,为什么我们首先关心多任务处理?
首先,如果任务涉及等待,多任务处理可以提高性能。例如,当洗衣机运行时,您可以轻松开始准备晚餐。当你的晚餐结束时,你可以把衣服挂起来。请注意,您在这里等待是因为一个独立的组件会为您完成这项工作。涉及等待的任务称为I/O 绑定任务。
其次,如果多任务处理速度很快,并且从鸟瞰的角度来看,它会表现为并行性。这有点像人眼将一系列快速连续显示的静止图像视为运动。如果我给爱丽丝写一封信一秒钟,给鲍勃也写一秒钟,如果你只看我每两秒在做什么,你能看出我是同时写还是交替写这两封信吗?搜索多任务操作系统以获取更多相关信息。
现在,让我们关注如何实现线程抽象的问题。
本质上,实现线程抽象就是编写一个任务,一个主任务,负责调度所有其他任务
要问的一个基本问题是:如果调度程序调度所有任务并且调度程序也是一个任务,那么谁调度调度程序?
让我们停止这个问题。假设您编写了一个调度程序,对其进行编译并将其加载到计算机主存的地址 1024 处,该地址恰好是计算机启动时加载到处理器指令指针中的地址。现在,您的调度程序继续并找到一些预编译在主内存中的任务。例如,任务从地址 1,048,576 开始。调度程序想要执行此任务,因此它将任务的地址 (1,048,576) 加载到指令指针中。嗯,这是一个考虑不周的举动,因为现在调度程序无法从刚刚启动的任务中重新获得控制权。
一种解决方案是在执行之前将调度程序的跳转指令(地址1024)插入到任务描述中。实际上,您不应该忘记保存机器人正在工作的桌子上的物品,因此您还必须在跳转之前保存处理器的寄存器。这里的问题是很难判断在哪里插入跳转指令。如果太多,它们会产生太多开销,如果它们太少,一个任务可能会独占处理器。
第二种方法是要求任务作者指定一些可以将控制权转移回调度程序的位置。请注意,作者不必编写保存寄存器和插入跳转指令的逻辑,因为他们标记适当的位置就足够了,调度程序会处理其余的事情。这看起来是个好主意,因为任务作者可能知道,例如,他们的任务将在加载并启动洗衣机后等待一段时间,因此他们让调度程序在那里进行控制。
上述两种方法都不能解决错误或恶意任务的问题,例如,该任务陷入无限循环并且永远不会跳转到调度程序所在的地址。
现在,如果您无法用软件解决某些问题该怎么办?用硬件来解决!所需要的是连接到处理器的可编程电路,其作用类似于闹钟。调度程序设置一个定时器及其地址(1024),当定时器超时时,警报会保存寄存器并将指令指针设置为调度程序所在的地址。这种方法称为抢占式调度。
也许现在您开始意识到实现线程抽象并不像实现链表。线程抽象最著名的实现者是操作系统。它们提供的线程有时称为内核级线程。由于操作系统无法承受失去控制的后果,因此所有主要的通用操作系统都使用抢占式调度。
可以说,操作系统感觉像是实现线程抽象的正确位置,因为它们控制所有硬件组件,并且可以非常明智地挂起和恢复线程。如果一个线程向操作系统请求存储在硬盘驱动器上的文件的内容,它立即知道该操作很可能需要一段时间,并且可以同时让另一个任务占用处理器。然后,一旦文件内容可用,它就可以暂停当前任务并恢复发出请求的任务。
然而,故事并没有到此结束,因为线程也可以在用户空间中实现。这些实现者通常是编译器。有趣的是,据我所知,内核级线程与线程一样强大。那么我们为什么要为用户级线程烦恼呢?原因当然是性能。用户级线程更加轻量级,因此您可以创建更多线程,并且通常暂停和恢复它们的开销很小。
用户级线程可以使用async/await 来实现。您是否还记得实现控制权返回调度程序的一种选择是让任务作者指定可以发生转换的位置?那么,
async
和await
关键字正是达到这个目的。现在,如果您已经做到了这一步,请做好准备,因为真正的乐趣来了!
您是否注意到我们几乎没有谈论并行性?我的意思是,我们不是使用线程来并行运行相关计算从而提高吞吐量吗?好吧,不安静......实际上,如果你只想要并行性,你根本不需要这个机制。您只需创建与您拥有的处理单元数量一样多的任务,并且无需暂停或恢复任何任务。您甚至不需要调度程序,因为您不需要执行多任务。
从某种意义上说,并行性是一个实现细节。如果您考虑一下,线程抽象的实现者可以在幕后使用任意数量的处理器。您只需编译 1950 年编写的一些编写良好的多线程代码,在今天的多核上运行它,就会看到它利用了所有核心。重要的是,编写该代码的程序员可能没有预料到该代码会在多核上运行。
您甚至可以认为,线程在用于实现并行性时被滥用了:尽管人们知道他们不需要核心功能(多任务处理),但他们还是使用线程来访问并行性。
最后一个想法是,请注意,仅用户级线程无法提供并行性。还记得开头的那句话吗?操作系统在默认情况下通常配备单个虚拟处理器(线程)的虚拟计算机(进程)内运行程序。无论您在用户空间中使用什么魔法,如果您的虚拟计算机只有一个虚拟处理器,您就无法并行运行代码。
那么我们想要什么呢?当然,我们需要并行性。但我们也想要轻量级线程。因此,许多线程抽象的实现者开始使用混合方法:他们启动与硬件中的处理单元一样多的内核级线程,并在几个线程之上运行许多用户级线程。内核级线程。 本质上,并行性由内核级线程负责,多任务由用户级线程负责。
现在,一个有趣的设计决策是语言公开什么线程接口。例如,Go 提供了一个接口,允许用户创建混合线程,即所谓的 goroutine。在 Go 中,没有办法只要求一个内核级线程。其他语言对于不同类型的线程有单独的接口。在 Rust 中,内核级线程存在于标准库中,而用户级和混合线程可以在
async-std
和tokio
等外部库中找到。在 Python 中,asyncio
包提供用户级线程,而multithreading
和multiprocessing
提供内核级线程。有趣的是,多线程提供的线程不能并行运行。另一方面,multiprocessing
提供的线程可以并行运行,但正如该库的名称所暗示的那样,每个内核级线程都位于不同的进程(虚拟机)中。这使得多处理不适合某些任务,因为在不同虚拟机之间传输数据通常很慢。更多资源:
操作系统:Thomas 和 Anderson 的原理与实践
并发不是并行,作者 Rob Pike
并行性和并发性需要不同的工具
Rust 异步编程
Rust 的异步转换内部
Rust 的异步/等待之旅< /a>
您的函数是什么颜色?
为什么使用 goroutine 而不是线程?
为什么我的程序没有使用更多 CPU 运行得更快?
John Reese - 使用 AsyncIO 和多重处理在 GIL 之外思考 - PyCon 2018
So threads are an abstraction.
Abstractions reduce complexity. Thus, the first question is what problem threads solve. The second question is how they can be implemented.
As to the first question: Threads make implementing multitasking easier. The main idea behind this is that multitasking is unnecessary if every task can be assigned to a unique worker. Actually, for the time being, it's fine to generalize the definition even further and say that the thread abstraction represents a virtual worker.
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.
This works, but now it's hard to disentangle the separate tasks by simply looking at the big task description that you created. Also, the ceremony of saving and restoring the items on the tabe is tedious and further clutters the task description.
Here is where the thread abstraction comes in and saves the day. It lets you assume that you have an infinite number of robots, each sitting in a different room at its own desk. Now, you can just throw task descriptions in a pot and everything else is taken care of by the thread abstraction's implementer. Remember? If there are enough workers, nobody has to multitask.
Often it is useful to indicate your perspective and say robot to mean real robots and virtual robot to mean the robots the thread abstraction provides you with.
At this point the problem of multitasking is solved for the case when the tasks are fully independent. However, wouldn't it be nice to let the robots go out of their rooms, interact and work together towards a common goal? Well, as you probably guessed, this requires coordination. Traffic lights, queues - you name it.
As an intermediate summary, the thread abstraction solves the problem of multitasking and creates an opportunity for cooperation. Without it, we only had a single robot, so cooperation was unthinkable. However, it has also brought the problem of coordination (synchronization) on us. Now we know what problem the tread abstraction solves and, as a bonus, we also know what new challenge it creates.
But wait, why do we care about multitasking in the first place?
First, multitasking can increase performance if the tasks involve waiting. For example, while the washing machine is running, you can easily start preparing dinner. And while your dinner is in the over, you can hang out the clothes. Note that here you wait because an independent component does the job for you. Tasks that involve waiting are called I/O bound tasks.
Second, if multitasking is done rapidly, and you look at it from a bird's eyes view, it appears as parallelism. It's a bit like how the human eye perceives a series of still images as motion if shown in quick succession. If I write a letter to Alice for one second and to Bob for one second as well, can you tell if I wrote the two letters simultaneously or alternately, if you only look at what I'm doing every two seconds? Search for Multitasking Operating System for more on this.
Now, let's focus on the question of how the thread abstraction can be implemented.
Essentially, implementing the thread abstraction is about writing a task, a main task, that takes care of scheduling all the other tasks.
A fundamental question to ask is: If the scheduler schedules all tasks and the scheduler is also a task, then who schedules the scheduler?
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.
One solution is to insert jump instructions to the scheduler (address 1024) into the task descriptions before execution. Actually, you shouldn't forget to save the items on the desk the robot is working at, so you also have to save the processor's registers before jumping. The issue here is that it is hard to tell where to insert the jump instructions. If there are too many, they create too much overhead and if there are too few of them, one task might monopolize the processor.
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.
The problem that neither of the above approaches solve is that of an erroneous or malicious task that, for example, gets caught up in an infinite loop and never jumps to the address where the scheduler lives.
Now, what to do if you cannot solve something in software? Solve it in hardware! What is needed is a programmable circuitry wired up to the processor that acts like an alarm clock. The scheduler sets a timer and its address (1024) and when the timer runs out, the alarm saves the registers and sets the instruction pointer to the address where the scheduler lives. This approach is called preemptive scheduling.
Probably by now you start to sense that implementing the thread abstraction is not like implementing a linked list. The most well-known implementers of the thread abstraction are operating systems. The threads they provide are sometimes called kernel-level threads. Since an operating system cannot afford losing control, all major, general-purpose operating systems uses preemptive scheduling.
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.
However, the story doesn't end here because threads can also be implemented in user space. These implementers are normally compilers. Interestingly, as far as I know, kernel-level threads are as powerful as threads can get. So why do we bother with user-level threads? The reason, of course, is performance. User-level threads are more lightweight so you can create more of them and normally the overhead of pausing and resuming them is small.
User-level threads can be implemented using async/await. Do you remember that one option to achieve that control gets back to the scheduler is to make task authors designate places where the transition can happen? Well, the
async
andawait
keywords serve exactly this purpose.Now, if you've made it this far, be prepared because here comes the real fun!
Have you noticed that we barely talked about parallelism? I mean, don't we use threads to run related computations in parallel and thereby increase throughput? Well, not quiet.. Actually, if you only want parallelism, you don't need this machinery at all. You just create as many tasks as the number of processing units you have and none of the tasks has to be paused or resumed ever. You don't even need a scheduler because you don't multitask.
In a sense, parallelism is an implementation detail. If you think about it, implementers of the thread abstraction can utilize as many processors as they wish under the hood. You can just compile some well-written multithreaded code from 1950, run it on a multicore today and see that it utilizes all cores. Importantly, the programmer who wrote that code probably didn't anticipate that piece of code being run on a multicore.
You could even argue that threads are abused when they are used to achieve parallelism: Even though people know they don't need the core feature, multitasking, they use threads to get access to parallelism.
As a final thought, note that user-level threads alone cannot provide parallelism. Remember the quote from the beginning? Operating systems run programs inside a virtual computer (process) that is normally equipped with a single virtual processor (thread) by default. No matter what magic you do in user space, if your virtual computer has only a single virtual processor, you cannot run code in parallel.
So what do we want? Of course, we want parallelism. But we also want lightweight threads. Therefore, many implementers of the thread abstraction started to use a hybrid approach: They start as many kernel-level threads as there are processing units in the hardware and run many user-level threads on top of a few kernel-level threads. Essentially, parallelism is taken care of by the kernel-level and multitasking by the user-level threads.
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
andtokio
. In Python, theasyncio
package provides user-level threads whilemultithreading
andmultiprocessing
provide kernel-level threads. Interestingly, the threadsmultithreading
provides cannot run in parallel. On the other hand, the threadsmultiprocessing
provides can run in parallel but, as the library's name suggests, each kernel-level thread lives in a different process (virtual machine). This makesmultiprocessing
unsuitable for certain tasks because transferring data between different virtual machines is often slow.Further resources:
Operating Systems: Principles and Practice by Thomas and Anderson
Concurrency is not parallelism by Rob Pike
Parallelism and concurrency need different tools
Asynchronous Programming in Rust
Inside Rust's Async Transform
Rust's Journey to Async/Await
What Color is Your Function?
Why goroutines instead of threads?
Why doesn't my program run faster with more CPUs?
John Reese - Thinking Outside the GIL with AsyncIO and Multiprocessing - PyCon 2018
线程与进程一样抽象。
进程是执行代码所需的一组资源,因此非常粗略地说,就是内存(数据和代码)和执行线程。
线程是在进程上下文中执行代码的一组资源,因此它大致具有自己的堆栈和指令指针,但与同一进程中的其他线程共享所有其他内容,因此分配给进程的所有内存。
A thread is no more no less abstract than a process is.
A process is a set of resources necessary to execute a code, thus very roughly, memory (data and code) and threads of execution.
A thread is a set of resources to execute a code in the context of a process, so it has, roughly its own stack and instruction pointer, but shares everything else with other threads in the same process, thus all the memory allocated to the process.