并发和异步编程:详细概述

01 Concurrency and Asynchronous Programming: a Detailed Overview

并发和异步编程:详细概述

Asynchronous programming is one of those topics many programmers find confusing. You come to the point when you think you’ve got it, only to later realize that the rabbit hole is much deeper than you thought. If you participate in discussions, listen to enough talks, and read about the topic on the internet, you’ll probably also come across statements that seem to contradict each other. At least, this describes how I felt when I first was introduced to the subject.

异步编程是许多程序员感到困惑的话题之一。当你觉得自己已经成功的时候,你才意识到兔子洞比你想象的要深得多。如果你参加了讨论,听了足够多的演讲,并在网上阅读了关于这个话题的文章,你可能也会遇到一些似乎相互矛盾的陈述。至少,这描述了我第一次接触这门学科时的感受。

The cause of this confusion is often a lack of context, or authors assuming a specific context without explicitly stating so, combined with terms surrounding concurrency and asynchronous programming that are rather poorly defined.

造成这种混淆的原因通常是缺乏上下文,或者作者在没有明确说明的情况下假设了一个特定的上下文,再加上围绕并发性和异步编程的术语定义很差。

In this chapter, we’ll be covering a lot of ground, and we’ll divide the content into the following main topics:

  • Async history
  • Concurrency and parallelism
  • The operating system and the CPU
  • Interrupts, firmware, and I/O

在本章中,我们将涉及很多内容,并将内容分为以下主要主题:

  • 异步历史
  • 并发和并行性
  • 操作系统和CPU
  • 中断、固件和I/O

This chapter is general in nature. It doesn’t specifically focus on Rust, or any specific programming language for that matter, but it’s the kind of background information we need to go through so we know that everyone is on the same page going forward. The upside is that this will be useful no matter what programming language you use. In my eyes, that fact also makes this one of the most interesting chapters in this book.

这一章是一般性的。它并没有特别关注Rust,或者任何特定的编程语言,但它是我们需要了解的背景信息,这样我们就知道每个人都在同一个页面上前进。好处是,无论您使用哪种编程语言,这都是有用的。在我看来,这一事实也使这一章节成为本书中最有趣的章节之一。

There’s not a lot of code in this chapter, so we’re off to a soft start. It’s a good time to make a cup of tea, relax, and get comfortable, as we’re about start this journey together.

本章中没有很多代码,所以我们可以轻松开始。这是泡杯茶,放松一下,放松一下的好时机,因为我们即将一起开始这段旅程。

An evolutionary journey of multitasking

多重任务的进化之旅

In the beginning, computers had one CPU that executed a set of instructions written by a programmer one by one. No operating system (OS), no scheduling, no threads, no multitasking. This was how computers worked for a long time. We’re talking back when a program was assembled in a deck of punched cards, and you got in big trouble if you were so unfortunate that you dropped the deck onto the floor.

一开始,计算机只有一个CPU,一个接一个地执行程序员编写的一组指令。没有操作系统(OS),没有调度,没有线程,没有多任务。在很长一段时间里,计算机就是这样工作的。我们说的是当一个程序被组装在一副打孔卡片上时,如果你不幸把卡片掉到地板上,你就有大麻烦了。

There were operating systems being researched very early and when personal computing started to grow in the 80s, operating systems such as DOS were the standard on most consumer PCs.

人们很早就开始研究操作系统,当个人电脑在80年代开始发展时,DOS等操作系统是大多数个人电脑的标准配置。

These operating systems usually yielded control of the entire CPU to the program currently executing, and it was up to the programmer to make things work and implement any kind of multitasking for their program. This worked fine, but as interactive UIs using a mouse and windowed operating systems became the norm, this model simply couldn’t work anymore.

这些操作系统通常将整个CPU的控制权交给当前正在执行的程序,由程序员来完成工作并为程序实现任何类型的多任务处理。这种模式运行良好,但随着使用鼠标和窗口操作系统的交互式ui成为常态,这种模式就不再适用了。

Non-preemptive multitasking

非抢占式多任务

Non-preemptive multitasking was the first method used to be able to keep a UI interactive (and running background processes).

非抢占式多任务是第一种能够保持UI交互(并运行后台进程)的方法。

This kind of multitasking put the responsibility of letting the OS run other tasks, such as responding to input from the mouse or running a background task, in the hands of the programmer.

这种多任务处理将让操作系统运行其他任务的责任,例如响应来自鼠标的输入或运行后台任务,交给了程序员。

Typically, the programmer yielded control to the OS.

通常,程序员将控制权交给操作系统。

Besides offloading a huge responsibility to every programmer writing a program for your platform, this method was naturally error-prone. A small mistake in a program’s code could halt or crash the entire system.

除了将巨大的责任推卸给为您的平台编写程序的每个程序员之外,这种方法自然容易出错。程序代码中的一个小错误可能会使整个系统停止或崩溃。

Another popular term for what we call non-preemptive multitasking is cooperative multitasking. Windows 3.1 used cooperative multitasking and required programmers to yield control to the OS by using specific system calls. One badly-behaving application could thereby halt the entire system.

我们称之为非抢占式多任务的另一个流行术语是合作多任务。Windows 3.1使用协同多任务,并要求程序员通过使用特定的系统调用将控制权交给操作系统。一个行为不佳的应用程序就可能使整个系统瘫痪。

Preemptive multitasking

抢占式多任务

While non-preemptive multitasking sounded like a good idea, it turned out to create serious problems as well. Letting every program and programmer out there be responsible for having a responsive UI in an operating system can ultimately lead to a bad user experience, since every bug out there could halt the entire system.

虽然非抢占式多任务处理听起来是个好主意,但事实证明它也会产生严重的问题。让每个程序和程序员都负责操作系统中的响应性UI,最终会导致糟糕的用户体验,因为每个漏洞都可能导致整个系统瘫痪。

The solution was to place the responsibility of scheduling the CPU resources between the programs that requested it (including the OS itself) in the hands of the OS. The OS can stop the execution of a process, do something else, and switch back.

解决方案是将在请求它的程序(包括操作系统本身)之间调度CPU资源的责任置于操作系统的手中。操作系统可以停止一个进程的执行,做其他事情,然后切换回来。

On such a system, if you write and run a program with a graphical user interface on a single-core machine, the OS will stop your program to update the mouse position before it switches back to your program to continue. This happens so frequently that we don’t usually observe any difference whether the CPU has a lot of work or is idle.

在这样的系统上,如果您在单核机器上编写并运行带有图形用户界面的程序,操作系统将停止您的程序以更新鼠标位置,然后再切换回您的程序继续。这种情况发生得如此频繁,以至于我们通常不会观察到CPU是否有大量工作或空闲有任何区别。

The OS is responsible for scheduling tasks and does this by switching contexts on the CPU. This process can happen many times each second, not only to keep the UI responsive but also to give some time to other background tasks and IO events.

操作系统负责调度任务,并通过在CPU上切换上下文来完成。这个过程每秒可以发生很多次,不仅是为了保持UI响应,也是为了给其他后台任务和IO事件一些时间。

This is now the prevailing way to design an operating system.

这是现在设计操作系统的主流方式。

Later in this book, we’ll write our own green threads and cover a lot of basic knowledge about context switching, threads, stacks, and scheduling that will give you more insight into this topic, so stay tuned.

在本书的后面,我们将编写自己的绿色线程,并介绍有关上下文切换、线程、堆栈和调度的许多基本知识,这些知识将使您对这个主题有更深入的了解,请继续关注。

Hyper-threading

超线程

As CPUs evolved and added more functionality such as several arithmetic logic units (ALUs) and additional logic units, the CPU manufacturers realized that the entire CPU wasn’t fully utilized. For example, when an operation only required some parts of the CPU, an instruction could be run on the ALU simultaneously. This became the start of hyper-threading.

随着CPU的发展和增加更多的功能,如几个算术逻辑单元(alu)和额外的逻辑单元,CPU制造商意识到整个CPU没有得到充分利用。例如,当一个操作只需要CPU的某些部分时,一条指令可以同时在ALU上运行。这就是超线程的开始。

Your computer today, for example, may have 6 cores and 12 logical cores… This is exactly where hyperthreading comes in. It “simulates” two cores on the same core by using unused parts of the CPU to drive progress on thread 2 and simultaneously running the code on thread 1. It does this by using a number of smart tricks (such as the one with the ALU).

例如,你今天的电脑可能有6个核心和12个逻辑核心。这正是超线程的用武之地。它通过使用CPU的未使用部分来驱动线程2上的进程并同时在线程1上运行代码,从而在同一个内核上“模拟”两个内核。它通过使用许多聪明的技巧(比如使用ALU的技巧)来实现这一点。

Now, using hyper-threading, we could actually offload some work on one thread while keeping the UI interactive by responding to events in the second thread even though we only had one CPU core, thereby utilizing our hardware better.

现在,使用超线程,我们实际上可以在一个线程上卸载一些工作,同时通过响应第二个线程中的事件保持UI交互,即使我们只有一个CPU核心,从而更好地利用我们的硬件。

It turns out that hyper-threading has been continuously improved since the 90s. Since you’re not actually running two CPUs, there will be some operations that need to wait for each other to finish. The performance gain of hyper-threading compared to multitasking in a single core seems to be somewhere close to 30% but it largely depends on the workload.

事实证明,自上世纪90年代以来,超线程一直在不断改进。由于实际上并没有运行两个cpu,因此会有一些操作需要等待对方完成。与单核的多任务处理相比,超线程的性能增益似乎接近30%,但这主要取决于工作负载。

Multicore processors

多核处理器

As most know, the clock frequency of processors has been flat for a long time. Processors get faster by improving caches, branch prediction, and speculative execution, and by working on the processing pipelines of the processors, but the gains seem to be diminishing.

众所周知,处理器的时钟频率在很长一段时间内一直是平坦的。处理器通过改进缓存、分支预测和推测执行,以及处理处理器的处理管道来提高速度,但收益似乎正在减少。

On the other hand, new processors are so small that they allow us to have many on the same chip. Now, most CPUs have many cores and most often, each core will also have the ability to perform hyper-threading.

另一方面,新的处理器是如此之小,以至于我们可以在同一个芯片上安装许多处理器。现在,大多数cpu都有许多核心,而且大多数情况下,每个核心都有执行超线程的能力。

Do you really write synchronous code?

你真的在写同步代码吗?

Like many things, this depends on your perspective. From the perspective of your process and the code you write, everything will normally happen in the order you write it.

像许多事情一样,这取决于你的观点。从您的流程和您编写的代码的角度来看,一切通常都将按照您编写的顺序发生。

From the operating system’s perspective, it might or might not interrupt your code, pause it, and run some other code in the meantime before resuming your process.

从操作系统的角度来看,它可能会也可能不会中断您的代码,暂停它,并在恢复您的进程之前同时运行一些其他代码。

From the perspective of the CPU, it will mostly execute instructions one at a time.* It doesn’t care who wrote the code, though, so when a hardware interrupt happens, it will immediately stop and give control to an interrupt handler. This is how the CPU handles concurrency.

从CPU的角度来看,它主要是一次执行一条指令。*它不关心谁写的代码,虽然,所以当硬件中断发生时,它会立即停止并把控制权交给中断处理程序。这就是CPU处理并发的方式。

However, modern CPUs can also do a lot of things in parallel. Most CPUs are pipelined, meaning that the next instruction is loaded while the current one is executing. It might have a branch predictor that tries to figure out what instructions to load next.

然而,现代cpu也可以并行处理很多事情。大多数cpu都是流水线的,这意味着当当前指令执行时,下一条指令会被加载。它可能有一个分支预测器,试图找出下一步加载什么指令。

The processor can also reorder instructions by using out-of-order execution if it believes it makes things faster this way without ‘asking’ or ‘telling’ the programmer or the OS, so you might not have any guarantee that A happens before B.

处理器也可以通过使用乱序执行来重新排序指令,如果它认为这样做可以让事情更快,而不需要“询问”或“告诉”程序员或操作系统,所以你可能无法保证A在B之前发生。

The CPU offloads some work to separate ‘coprocessors’ such as the FPU for floating-point calculations, leaving the main CPU ready to do other tasks et cetera.

CPU将一些工作卸载给独立的“协处理器”,比如FPU进行浮点计算,让主CPU准备好做其他任务等等。

As a high-level overview, it’s OK to model the CPU as operating in a synchronous manner, but for now, let’s just make a mental note that this is a model with some caveats that become especially important when talking about parallelism, synchronization primitives (such as mutexes and atomics), and the security of computers and operating systems.

作为一个高级概述,将CPU建模为以同步方式操作是可以的,但是现在,让我们记住,这个模型带有一些注意事项,在讨论并行性、同步原语(如互斥体和原子)以及计算机和操作系统的安全性时,这些注意事项变得特别重要。

Concurrency versus parallelism

并发与并行

Right off the bat, we’ll dive into this subject by defining what concurrency is. Since it is quite easy to confuse concurrent with parallel, we will try to make a clear distinction between the two from the get-go.

马上,我们将通过定义什么是并发性来深入研究这个主题。由于并发和并行很容易混淆,我们将尝试从一开始就明确区分这两者。

Concurrency is about dealing with a lot of things at the same time.

并发是指同时处理很多事情。

Parallelism is about doing a lot of things at the same time.

并行是指在同一时间做很多事情。

We call the concept of progressing multiple tasks at the same time multitasking. There are two ways to multitask. One is by progressing tasks concurrently, but not at the same time. Another is to progress tasks at the exact same time in parallel. Figure 1.1 depicts the difference between the two scenarios:

我们把同时处理多个任务的概念称为多任务。多任务处理有两种方法。一种是同时进行任务,但不是同时进行。另一种方法是在同一时间并行地推进任务。图1.1描述了两种场景的区别:

在这里插入图片描述

First, we need to agree on some definitions:

  • Resource: This is something we need to be able to progress a task. Our resources are limited.This could be CPU time or memory.
  • Task: This is a set of operations that requires some kind of resource to progress. A task mustconsist of several sub-operations.
  • Parallel: This is something happening independently at the exact same time.
  • Concurrent: These are tasks that are in progress at the same time, but not necessarilyprogressing simultaneously.

首先,我们需要就一些定义达成一致:

  • 资源:这是我们需要能够推进任务的东西。我们的资源有限。这可能是CPU时间或内存。
  • 任务:这是一组需要某种资源才能进行的操作。一个任务必须由几个子操作组成。
  • 平行:这是在同一时间独立发生的事情。
  • 并发:这些任务是同时进行的,但不一定是同时进行的。

This is an important distinction. If two tasks are running concurrently, but are not running in parallel, they must be able to stop and resume their progress. We say that a task is interruptible if it allows for this kind of concurrency.

这是一个重要的区别。如果两个任务并发运行,但不是并行运行,它们必须能够停止并恢复其进程。如果一个任务允许这种并发性,我们就说它是可中断的。

The mental model I use

我使用的心智模型

I firmly believe the main reason we find parallel and concurrent programming hard to differentiate stems from how we model events in our everyday life. We tend to define these terms loosely, so our intuition is often wrong.

我坚信,我们发现并行编程和并发编程难以区分的主要原因源于我们在日常生活中如何建模事件。我们倾向于松散地定义这些术语,所以我们的直觉经常是错误的。

It doesn’t help that concurrent is defined in the dictionary as operating or occurring at the same time, which doesn’t really help us much when trying to describe how it differs from parallel.

字典中将并发定义为同时操作或同时发生,这对我们描述它与并行的区别并没有多大帮助。

For me, this first clicked when I started to understand why we want to make a distinction between parallel and concurrent in the first place!

对我来说,当我开始理解为什么我们首先要区分并行和并发时,我第一次明白了这一点!

The why has everything to do with resource utilization and efficiency.

为什么与资源利用和效率有关。

Efficiency is the (often measurable) ability to avoid wasting materials, energy, effort, money, and time in doing something or in producing a desired result.

效率是在做某事或产生预期结果时避免浪费材料、能量、努力、金钱和时间的能力(通常是可测量的)。

Parallelism is increasing the resources we use to solve a task. It has nothing to do with efficiency.

并行性增加了我们用于解决任务的资源。这与效率无关。

Concurrency has everything to do with efficiency and resource utilization. Concurrency can never make one single task go faster. It can only help us utilize our resources better and thereby finish a set of tasks faster.

并发性与效率和资源利用密切相关。并发永远不能使单个任务运行得更快。它只能帮助我们更好地利用我们的资源,从而更快地完成一组任务。

Let’s draw some parallels to process economics

让我们与过程经济学做一些类比

In businesses that manufacture goods, we often talk about LEAN processes. This is pretty easy to compare with why programmers care so much about what we can achieve if we handle tasks concurrently.

在制造产品的企业中,我们经常谈论精益生产过程。这很容易与程序员如此关心并发处理任务的效果进行比较。

Let’s pretend we’re running a bar. We only serve Guinness beer and nothing else, but we serve our Guinness to perfection. Yes, I know, it’s a little niche, but bear with me.

假设我们在经营一家酒吧。我们只供应健力士啤酒,没有别的,但我们的健力士啤酒非常完美。是的,我知道,这是一个小众市场,但请耐心听我说。

You are the manager of this bar, and your goal is to run it as efficiently as possible. Now, you can think of each bartender as a CPU core, and each order as a task. To manage this bar, you need to know the steps to serve a perfect Guinness:

  • Pour the Guinness draught into a glass tilted at 45 degrees until it’s 3-quarters full (15 seconds).
  • Allow the surge to settle for 100 seconds.
  • Fill the glass completely to the top (5 seconds).
  • Serve.

你是这家酒吧的经理,你的目标是尽可能高效地经营它。现在,您可以将每个调酒师想象成一个CPU核心,每个订单都是一个任务。要管理好这个酒吧,你需要知道提供一杯完美的吉尼斯黑啤酒的步骤:

  • 将生啤倒入倾斜45度的玻璃杯中,直到满3 / 4(15秒)。
  • 允许浪涌沉淀100秒。
  • 将玻璃杯完全斟满(5秒)。
  • 服务。

Since there is only one thing to order in the bar, customers only need to signal using their fingers how many they want to order, so we assume taking new orders is instantaneous. To keep things simple, the same goes for payment. In choosing how to run this bar, you have a few alternatives.

由于酒吧里只有一种东西可以点,顾客只需要用手指示意他们想点多少,所以我们假设接受新订单是即时的。为了简单起见,支付也是如此。在选择如何经营这家酒吧时,你有几种选择。

Alternative 1 – Fully synchronous task execution with one bartender

替代方案1 -与一个调酒师完全同步的任务执行

You start out with only one bartender (CPU). The bartender takes one order, finishes it, and progresses to the next. The line is out the door and going two blocks down the street – great! One month later, you’re almost out of business and you wonder why.

一开始你只有一个调酒师(CPU)。酒保端上一杯酒,喝完,然后继续下一杯。队伍已经排到门外了,沿着这条街走两个街区——太棒了!一个月后,你几乎破产了,你想知道为什么。

Well, even though your bartender is very fast at taking new orders, they can only serve 30 customers an hour. Remember, they’re waiting for 100 seconds while the beer settles and they’re practically just standing there, and they only use 20 seconds to actually fill the glass. Only after one order is completely finished can they progress to the next customer and take their order.

虽然你的酒保接新单子的速度很快,但他们一小时只能接待30位顾客。记住,他们要等100秒啤酒沉淀下来,他们实际上只是站在那里,他们只用了20秒就把杯子装满了。只有在一个订单完全完成后,他们才能进入下一个客户并接受他们的订单。

The result is bad revenue, angry customers, and high costs. That’s not going to work.

结果就是糟糕的收入、愤怒的客户和高昂的成本。这是行不通的。

Alternative 2 – Parallel and synchronous task execution

备选方案2 -并行和同步任务执行

So, you hire 12 bartenders, and you calculate that you can serve about 360 customers an hour. The line is barely going out the door now, and revenue is looking great.

所以,你雇佣了12名调酒师,你计算出你每小时可以为360名顾客服务。这条线现在几乎没有出线,收入看起来很不错。

One month goes by and again, you’re almost out of business. How can that be?

一个月又一个月过去了,你几乎破产了。这怎么可能呢?

It turns out that having 12 bartenders is pretty expensive. Even though revenue is high, the costs are even higher. Throwing more resources at the problem doesn’t really make the bar more efficient.

事实证明,请12个调酒师是相当昂贵的。虽然收入很高,但成本更高。在这个问题上投入更多的资源并不能真正提高酒吧的效率。

Alternative 3 – Asynchronous task execution with one bartender

备选方案3 -使用一个调酒师异步执行任务

So, we’re back to square one. Let’s think this through and find a smarter way of working instead of throwing more resources at the problem.

所以,我们又回到了起点。让我们仔细考虑一下,找到一种更聪明的工作方式,而不是在这个问题上投入更多的资源。

You ask your bartender whether they can start taking new orders while the beer settles so that they’re never just standing and waiting while there are customers to serve. The opening night comes and…

你问你的酒保,他们是否可以在啤酒沉淀下来的时候开始接新单,这样他们就不用站着等客人了。开幕之夜来了……

Wow! On a busy night where the bartender works non-stop for a few hours, you calculate that they now only use just over 20 seconds on an order. You’ve basically eliminated all the waiting. Your theoretical throughput is now 240 beers per hour. If you add one more bartender, you’ll have higher throughput than you did while having 12 bartenders.

哇!在一个忙碌的夜晚,酒保不停地工作几个小时,你计算一下,他们现在只需要20秒多一点的时间来处理一个订单。你基本上省去了所有的等待。你的理论产量现在是每小时240瓶啤酒。如果你多增加一个调酒师,你的产量就会比你有12个调酒师时高。

However, you realize that you didn’t actually accomplish 240 beers an hour, since orders come somewhat erratically and not evenly spaced over time. Sometimes, the bartender is busy with a new order, preventing them from topping up and serving beers that are finished almost immediately. In real life, the throughput is only 180 beers an hour.

然而,你意识到你实际上并没有完成每小时240瓶啤酒的任务,因为订单来的有些不规律,而且不是均匀间隔的。有时,调酒师正忙着处理新订单,以至于他们无法给客人加满酒,也无法给客人端上几乎马上就喝完了的啤酒。在现实生活中,每小时的吞吐量只有180瓶。

Still, two bartenders could serve 360 beers an hour this way, the same amount that you served while employing 12 bartenders.

不过,用这种方法,两名调酒师每小时可以提供360瓶啤酒,与你雇佣12名调酒师时提供的数量相同。

This is good, but you ask yourself whether you can do even better.

这很好,但你会问自己是否还能做得更好。

Alternative 4 – Parallel and asynchronous task execution with two bartenders

备选方案4 -与两个调酒师并行和异步执行任务

What if you hire two bartenders, and ask them to do just what we described in Alternative 3, but with one change: you allow them to steal each other’s tasks, so bartender 1 can start pouring and set the beer down to settle, and bartender 2 can top it up and serve it if bartender 1 is busy pouring a new order at that time? This way, it is only rarely that both bartenders are busy at the same time as one of the beers-in-progress becomes ready to get topped up and served. Almost all orders are finished and served in the shortest amount of time possible, letting customers leave the bar with their beer faster and giving space to customers who want to make a new order.

如果你雇佣两个调酒师,并要求他们做我们在选项3中描述的事情,但有一个改变:你允许他们窃取彼此的任务,所以调酒师1可以开始倒酒并放下啤酒,而调酒师2可以在调酒师1忙着倒新酒的时候把它加满并上桌。这样一来,两名调酒师同时忙碌的情况就很少了,因为其中一种正在酿造的啤酒已经准备好要加满酒了。几乎所有的订单都能在尽可能短的时间内完成并上桌,让顾客更快地带着啤酒离开酒吧,并给想要下新订单的顾客留出空间。

Now, this way, you can increase throughput even further. You still won’t reach the theoretical maximum, but you’ll get very close. On the opening night, you realize that the bartenders now process 230 orders an hour each, giving a total throughput of 460 beers an hour.

现在,通过这种方式,您可以进一步提高吞吐量。你仍然不会达到理论最大值,但你会非常接近。在开业之夜,你意识到调酒师现在每小时处理230份订单,每小时总共处理460瓶啤酒。

Revenue looks good, customers are happy, costs are kept at a minimum, and you’re one happy manager of the weirdest bar on earth (an extremely efficient bar, though).

收入看起来不错,顾客很高兴,成本保持在最低水平,你是地球上最奇怪的酒吧(尽管是一个非常高效的酒吧)的快乐经理。

Concurrency is about working smarter. Parallelism is a way of throwing more resources at the problem.

并发是关于更聪明地工作。并行是在问题上投入更多资源的一种方式。

Concurrency and its relation to I/O

并发性及其与I/O的关系

As you might understand from what I’ve written so far, writing async code mostly makes sense when you need to be smart to make optimal use of your resources.

正如您可能从我到目前为止所写的内容中所理解的那样,当您需要聪明地优化使用资源时,编写异步代码大多是有意义的。

Now, if you write a program that is working hard to solve a problem, there is often no help in concurrency. This is where parallelism comes into play, since it gives you a way to throw more resources at the problem if you can split it into parts that you can work on in parallel.

现在,如果您编写了一个努力解决问题的程序,那么并发性通常没有任何帮助。这就是并行性发挥作用的地方,因为如果您可以将问题分解为可以并行处理的部分,它可以为您提供一种将更多资源投入问题的方法。

Consider the following two different use cases for concurrency:

  • When performing I/O and you need to wait for some external event to occur
  • When you need to divide your attention and prevent one task from waiting too long

考虑以下两个不同的并发用例:

  • 当执行I/O时,需要等待一些外部事件发生
  • 当你需要分散注意力,防止一个任务等待太久时

The first is the classic I/O example: you have to wait for a network call, a database query, or something else to happen before you can progress a task. However, you have many tasks to do so instead of waiting, you continue to work elsewhere and either check in regularly to see whether the task is ready to progress, or make sure you are notified when that task is ready to progress.

第一种是经典的I/O示例:在执行任务之前,必须等待网络调用、数据库查询或其他事情发生。但是,您有许多任务要做,而不是等待,您继续在其他地方工作,并且定期检查任务是否准备好进行,或者确保在任务准备好进行时通知您。

The second is an example that is often the case when having a UI. Let’s pretend you only have one core. How do you prevent the whole UI from becoming unresponsive while performing other CPU-intensive tasks?

第二个例子是在UI中经常出现的情况。让我们假设你只有一个核心。如何防止整个UI在执行其他cpu密集型任务时变得无响应?

Well, you can stop whatever task you’re doing every 16 ms, run the update UI task, and then resume whatever you were doing afterward. This way, you will have to stop/resume your task 60 times a second, but you will also have a fully responsive UI that has a roughly 60 Hz refresh rate.

你可以每16毫秒停止你正在做的任何任务,运行updatuittask,然后恢复你之后做的任何事情。这样,你将不得不每秒停止/恢复任务60次,但你也将拥有一个具有大约60 Hz刷新率的完全响应的UI。

What about threads provided by the operating system?

操作系统提供的线程呢?

We’ll cover threads a bit more when we talk about strategies for handling I/O later in this book, but I’ll mention them here as well. One challenge when using OS threads to understand concurrency is that they appear to be mapped to cores. That’s not necessarily a correct mental model to use, even though most operating systems will try to map one thread to one core up to the number of threads equal to the number of cores.

在本书后面讨论处理I/O的策略时,我们将更多地讨论线程,但我在这里也会提到它们。当使用操作系统线程来理解并发性时,一个挑战是它们似乎被映射到内核。这并不一定是一个正确的心智模型,即使大多数操作系统会尝试将一个线程映射到一个内核,直到线程数量等于内核数量。

Once we create more threads than there are cores, the OS will switch between our threads and progress each of them concurrently using its scheduler to give each thread some time to run. You also must consider the fact that your program is not the only one running on the system. Other programs might spawn several threads as well, which means there will be many more threads than there are cores on the CPU.

一旦我们创建的线程数量超过了内核数量,操作系统就会在我们的线程之间切换,并使用调度程序并发地处理每个线程,从而给每个线程一些运行时间。您还必须考虑这样一个事实,即您的程序不是系统上唯一运行的程序。其他程序也可能产生多个线程,这意味着线程数量将远远超过CPU上的内核数量。

Therefore, threads can be a means to perform tasks in parallel, but they can also be a means to achieve concurrency.

因此,线程可以是并行执行任务的一种手段,但它们也可以是实现并发性的一种手段。

This brings me to the last part about concurrency. It needs to be defined in some sort of reference frame.

这就引出了关于并发的最后一部分。它需要在某种参考系中定义。

Choosing the right reference frame

选择正确的参考系

When you write code that is perfectly synchronous from your perspective, stop for a second and consider how that looks from the operating system perspective.

当您编写的代码从您的角度来看是完全同步的时,请停下来一秒钟,并考虑从操作系统的角度来看它是什么样子的。

The operating system might not run your code from start to end at all. It might stop and resume your process many times. The CPU might get interrupted and handle some inputs while you think it’s only focused on your task.

操作系统可能根本不会从头到尾运行您的代码。它可能会多次停止和恢复您的进程。当你认为CPU只专注于你的任务时,它可能会被中断并处理一些输入。

So, synchronous execution is only an illusion. But from the perspective of you as a programmer, it’s not, and that is the important takeaway:

因此,同步执行只是一种错觉。但从你作为程序员的角度来看,它不是,这是重要的收获:

When we talk about concurrency without providing any other context, we are using you as a programmer and your code (your process) as the reference frame. If you start pondering concurrency without keeping this in the back of your head, it will get confusing very fast.

当我们在不提供任何其他上下文的情况下讨论并发性时,我们使用的是作为程序员的您和您的代码(您的进程)作为参考框架。如果你在开始考虑并发性的时候没有把它记在脑子里,它很快就会让你感到困惑。

The reason I’m spending so much time on this is that once you realize the importance of having the same definitions and the same reference frame, you’ll start to see that some of the things you hear and learn that might seem contradictory really are not. You’ll just have to consider the reference frame first.

我花这么多时间在这上面的原因是,一旦你意识到拥有相同的定义和相同的参考框架的重要性,你就会开始看到你听到和学到的一些看似矛盾的事情实际上并不是这样的。你只需要先考虑一下参考系。

Asynchronous versus concurrent

异步与并发

So, you might wonder why we’re spending all this time talking about multitasking, concurrency, and parallelism, when the book is about asynchronous programming.

因此,您可能会奇怪,为什么我们花这么多时间讨论多任务、并发性和并行性,而这本书是关于异步编程的。

The main reason for this is that all these concepts are closely related to each other, and can even have the same (or overlapping) meanings, depending on the context they’re used in.

主要原因是所有这些概念彼此密切相关,甚至可能具有相同(或重叠)的含义,这取决于它们所使用的上下文。

In an effort to make the definitions as distinct as possible, we’ll define these terms more narrowly than you’d normally see. However, just be aware that we can’t please everyone and we do this for our own sake of making the subject easier to understand. On the other hand, if you fancy heated internet debates, this is a good place to start. Just claim someone else’s definition of concurrent is 100 % wrong or that yours is 100 % correct, and off you go.

为了使定义尽可能清晰,我们将比您通常看到的更狭义地定义这些术语。然而,请注意,我们不能让每个人都满意,我们这样做是为了让这个主题更容易理解。另一方面,如果你喜欢激烈的网络辩论,这是一个很好的开始。只要声称别人对并发的定义是百分之百错误的,或者你的定义是百分之百正确的,然后你就可以走了。

For the sake of this book, we’ll stick to this definition: asynchronous programming is the way a programming language or library abstracts over concurrent operations, and how we as users of a language or library use that abstraction to execute tasks concurrently.

为了本书的目的,我们将坚持这个定义:异步编程是编程语言或库对并发操作的抽象方式,以及我们作为语言或库的用户如何使用这种抽象来并发地执行任务。

The operating system already has an existing abstraction that covers this, called threads. Using OS threads to handle asynchrony is often referred to as multithreaded programming. To avoid confusion, we’ll not refer to using OS threads directly as asynchronous programming, even though it solves the same problem.

操作系统已经有一个现有的抽象来涵盖这一点,称为线程。使用操作系统线程处理异步通常被称为多线程编程。为了避免混淆,我们将不把直接使用操作系统线程称为异步编程,尽管它解决了同样的问题。

Given that asynchronous programming is now scoped to be about abstractions over concurrent or parallel operations in a language or library, it’s also easier to understand that it’s just as relevant on embedded systems without an operating system as it is for programs that target a complex system with an advanced operating system. The definition itself does not imply any specific implementation even though we’ll look at a few popular ones throughout this book.

考虑到异步编程现在的范围是关于语言或库中并发或并行操作的抽象,也更容易理解它与没有操作系统的嵌入式系统一样相关,因为它适用于具有高级操作系统的复杂系统。定义本身并不意味着任何特定的实现,尽管我们将在本书中看到一些流行的实现。

If this still sounds complicated, I understand. Just sitting and reflecting on concurrency is difficult, but if we try to keep these thoughts in the back of our heads when we work with async code I promise it will get less and less confusing.

如果这听起来仍然很复杂,我能理解。只是坐下来思考并发性是很困难的,但如果我们在处理异步代码时尽量把这些想法放在脑后,我保证它会越来越少令人困惑。

The role of the operating system

操作系统的角色

The operating system (OS) stands in the center of everything we do as programmers (well, unless you’re writing an operating system or working in the embedded realm), so there is no way for us to discuss any kind of fundamentals in programming without talking about operating systems in a bit of detail.

操作系统(OS)是我们作为程序员所做的一切工作的中心(好吧,除非您正在编写操作系统或在嵌入式领域工作),因此,如果不详细讨论操作系统,我们就不可能讨论任何编程基础知识。

Concurrency from the operating system’s perspective

从操作系统的角度来看并发性

This ties into what I talked about earlier when I said that concurrency needs to be talked about within a reference frame, and I explained that the OS might stop and start your process at any time.

这与我之前所说的需要在参考框架内讨论并发性的内容有关,并且我解释了操作系统可能随时停止和启动您的进程。

What we call synchronous code is, in most cases, code that appears synchronous to us as programmers. Neither the OS nor the CPU lives in a fully synchronous world.

在大多数情况下,我们所说的同步代码对我们程序员来说是同步的。操作系统和CPU都不是完全同步的。

Operating systems use preemptive multitasking and as long as the operating system you’re running is preemptively scheduling processes, you won’t have a guarantee that your code runs instruction by instruction without interruption.

操作系统使用抢占式多任务处理,只要您正在运行的操作系统是抢占式调度进程,您就不能保证您的代码一条指令一条指令地运行而不中断。

The operating system will make sure that all important processes get some time from the CPU to make progress.

操作系统将确保所有重要的进程从CPU获得一些时间来取得进展。

This is not as simple when we’re talking about modern machines with 4, 6, 8, or 12 physical cores, since you might actually execute code on one of the CPUs uninterrupted if the system is under very little load. The important part here is that you can’t know for sure and there is no guarantee that your code will be left to run uninterrupted.

当我们谈论具有4、6、8或12个物理内核的现代机器时,这就不那么简单了,因为如果系统负载很小,您可能会在其中一个cpu上不间断地执行代码。这里的重要部分是,您不能确定,也不能保证您的代码将不间断地运行。

Teaming up with the operating system

与操作系统合作

When you make a web request, you’re not asking the CPU or the network card to do something for you – you’re asking the operating system to talk to the network card for you.

当你发出网络请求时,你并不是在要求CPU或网卡为你做什么——你是在要求操作系统为你与网卡对话。

There is no way for you as a programmer to make your system optimally efficient without playing to the strengths of the operating system. You basically don’t have access to the hardware directly. You must remember that the operating system is an abstraction over the hardware.

作为一名程序员,如果不发挥操作系统的优势,就不可能使系统达到最佳效率。你基本上不能直接访问硬件。您必须记住,操作系统是对硬件的抽象。

However, this also means that to understand everything from the ground up, you’ll also need to know how your operating system handles these tasks.

然而,这也意味着要从头开始理解一切,您还需要知道操作系统如何处理这些任务。

To be able to work with the operating system, you’ll need to know how you can communicate with it, and that’s exactly what we’re going to go through next.

为了能够与操作系统一起工作,您需要知道如何与它通信,这正是我们接下来要讨论的内容。

Communicating with the operating system

与操作系统通信

Communication with an operating system happens through what we call a system call (syscall). We need to know how to make system calls and understand why it’s so important for us when we want to cooperate and communicate with the operating system. We also need to understand how the basic abstractions we use every day use system calls behind the scenes. We’ll have a detailed walkthrough in Chapter 3, so we’ll keep this brief for now.

与操作系统的通信是通过我们所说的系统调用(sycall)进行的。我们需要知道如何进行系统调用,并理解为什么当我们想要与操作系统进行合作和通信时,它对我们如此重要。我们还需要了解我们每天使用的基本抽象是如何在幕后使用系统调用的。我们将在第3章中进行详细的演练,所以现在我们将保持简要。

A system call uses a public API that the operating system provides so that programs we write in ‘userland’ can communicate with the OS.

系统调用使用操作系统提供的公共API,以便我们在“用户区”编写的程序可以与操作系统通信。

Most of the time, these calls are abstracted away for us as programmers by the language or the runtime we use.

大多数情况下,作为程序员,这些调用被我们使用的语言或运行时抽象掉了。

Now, a syscall is an example of something that is unique to the kernel you’re communicating with, but the UNIX family of kernels has many similarities. UNIX systems expose this through libc.

现在,系统调用是与您通信的内核所特有的一个例子,但是UNIX内核家族有许多相似之处。UNIX系统通过libc公开这一点。

Windows, on the other hand, uses its own API, often referred to as WinAPI, and it can operate radically differently from how the UNIX-based systems operate.

另一方面,Windows使用自己的API,通常称为WinAPI,它的操作方式与基于unix的系统的操作方式截然不同。

Most often, though, there is a way to achieve the same things. In terms of functionality, you might not notice a big difference but as we’ll see later, and especially when we dig into how epoll, kqueue, and IOCP work, they can differ a lot in how this functionality is implemented.

然而,大多数情况下,有一种方法可以达到同样的目的。在功能方面,你可能没有注意到很大的区别,但正如我们稍后会看到的,特别是当我们深入研究epoll, kqueue和IOCP是如何工作的时候,它们在实现这个功能的方式上可能会有很大的不同。

However, a syscall is not the only way we interact with our operating system, as we’ll see in the following section.

然而,系统调用并不是我们与操作系统交互的唯一方式,我们将在下一节中看到这一点。

The CPU and the operating system

CPU和操作系统

Does the CPU cooperate with the operating system?

CPU是否与操作系统配合?

If you had asked me this question when I first thought I understood how programs work, I would most likely have answered no. We run programs on the CPU and we can do whatever we want if we know how to do it. Now, first of all, I wouldn’t have thought this through, but unless you learn how CPUs and operating systems work together, it’s not easy to know for sure.

如果你在我第一次认为我了解程序是如何工作的时候问我这个问题,我很可能会回答不。我们在CPU上运行程序,如果我们知道怎么做,我们可以做任何我们想做的事情。首先,我不会把这个问题想得很透彻,但是除非你了解cpu和操作系统是如何协同工作的,否则要确定这一点并不容易。

What started to make me think I was very wrong was a segment of code that looked like what you’re about to see. If you think inline assembly in Rust looks foreign and confusing, don’t worry just yet. We’ll go through a proper introduction to inline assembly a little later in this book. I’ll make sure to go through each of the following lines until you get more comfortable with the syntax:

让我开始觉得我错了的是一段代码,看起来就像你即将看到的。如果您认为Rust中的内联汇编看起来很陌生且令人困惑,请不要担心。我们将在本书稍后的部分对内联汇编进行适当的介绍。我将确保遍历下面的每一行,直到您对语法更熟悉:

fn main() {let t = 100;let t_ptr: *const usize = &t;let x = dereference(t_ptr);println!("{}", x);
}
fn dereference(ptr: *const usize) -> usize {let mut res: usize;unsafe {asm!("mov {0}, [{1}]", out(reg) res, in(reg) ptr)};res
}

What you’ve just looked at is a dereference function written in assembly.

你刚才看到的是一个用汇编语言写的解引用函数。

The mov {0}, [{1}] line needs some explanation. {0} and {1} are templates that tell the compiler that we’re referring to the registers that out(reg) and in(reg) represent. The number is just an index, so if we had more inputs or outputs they would be numbered {2}, {3}, and so on. Since we only specify reg and not a specific register, we let the compiler choose what registers it wants to use.

mov{0},[{1}]行需要一些解释。{0}和{1}是模板,它们告诉编译器我们正在引用out(reg)和in(reg)所代表的寄存器。数字只是一个索引,所以如果我们有更多的输入或输出,它们将被编号为{2},{3},以此类推。因为我们只指定了reg而不是一个特定的寄存器,所以我们让编译器选择它想要使用的寄存器。

The mov instruction instructs the CPU to take the first 8 bytes (if we’re on a 64-bit machine) it gets when reading the memory location that {1} points to and place that in the register represented by {0}. The [] brackets will instruct the CPU to treat the data in that register as a memory address, and instead of simply copying the memory address itself to {0}, it will fetch what’s at that memory location and move it over.

mov指令指示CPU在读取{1}指向的内存位置时获取前8字节(如果我们在64位机器上),并将其放入由{0}表示的寄存器中。[]括号将指示CPU将寄存器中的数据视为内存地址,而不是简单地将内存地址本身复制到{0},它将获取该内存位置的数据并将其移动。

Anyway, we’re just writing instructions to the CPU here. No standard library, no syscall; just raw instructions. There is no way the OS is involved in that dereference function, right?

不管怎样,我们只是在给CPU写指令。没有标准库,就没有系统调用;只是原始指令。操作系统不可能参与到解参函数中,对吧?

If you run this program, you get what you’d expect:

100

Now, if you keep the dereference function but replace the main function with a function that creates a pointer to the 99999999999999 address, which we know is invalid, we get this function:

现在,如果保留解引用函数,但将主函数替换为创建一个指向99999999999999地址的指针的函数,我们知道这个地址是无效的,我们得到这个函数:

fn main() {let t_ptr = 99999999999999 as *const usize;let x = dereference(t_ptr);println!("{}", x);
}

Now, if we run that we get the following results.

This is the result on Linux:

Segmentation fault (core dumped)

This is the result on Windows:

error: process didn't exit successfully: `target\debug\ac-assemblydereference.exe` (exit code: 0xc0000005, STATUS_ACCESS_VIOLATION)

We get a segmentation fault. Not surprising, really, but as you also might notice, the error we get is different on different platforms. Surely, the OS is involved somehow. Let’s take a look at what’s really happening here.

我们得到一个分割错误。这并不奇怪,但你可能也注意到了,我们在不同平台上得到的错误是不同的。当然,操作系统在某种程度上是有影响的。让我们来看看这里到底发生了什么。

Down the rabbit hole

掉进兔子洞

It turns out that there is a great deal of cooperation between the OS and the CPU, but maybe not in the way you would naively think.

事实证明,在操作系统和CPU之间有大量的合作,但可能不是你天真地认为的方式。

Many modern CPUs provide some basic infrastructure that operating systems use. This infrastructure gives us the security and stability we expect. Actually, most advanced CPUs provide a lot more options than operating systems such as Linux, BSD, and Windows actually use.

许多现代cpu提供了操作系统使用的一些基本基础设施。这种基础设施为我们提供了我们所期望的安全性和稳定性。实际上,大多数高级cpu提供的选项比Linux、BSD和Windows等操作系统实际使用的选项多得多。

There are two in particular that I want to address here:

  • How the CPU prevents us from accessing memory we’re not supposed to access
  • How the CPU handles asynchronous events such as I/O

我在这里特别想谈两点:

  • CPU如何阻止我们访问我们不应该访问的内存
  • CPU如何处理I/O等异步事件

We’ll cover the first one here and the second in the next section.

我们将在这里介绍第一个,下一节将介绍第二个。

How does the CPU prevent us from accessing memory we’re not supposed to access?

CPU是如何阻止我们访问我们不应该访问的内存的?

As I mentioned, modern CPU architectures define some basic concepts by design. Some examples of this are as follows:

  • Virtual memory
  • Page table
  • Page fault
  • Exceptions
  • Privilege level

Exactly how this works will differ depending on the specific CPU, so we’ll treat them in general terms here.

具体的工作方式取决于特定的CPU,所以我们在这里用一般的术语来对待它们。

Most modern CPUs have a memory management unit (MMU). This part of the CPU is often etched on the same dye, even. The MMU’s job is to translate the virtual address we use in our programs to a physical address.

大多数现代cpu都有一个内存管理单元(MMU)。这部分的CPU往往蚀刻在相同的染料,甚至。MMU的工作是将我们在程序中使用的虚拟地址转换为物理地址。

When the OS starts a process (such as our program), it sets up a page table for our process and makes sure a special register on the CPU points to this page table.

当操作系统启动一个进程(比如我们的程序)时,它会为我们的进程设置一个页表,并确保CPU上有一个特殊的寄存器指向这个页表。

Now, when we try to dereference t_ptr in the preceding code, the address is at some point sent for translation to the MMU, which looks it up in the page table to translate it to a physical address in the memory where it can fetch the data.

现在,当我们在前面的代码中尝试解引用t_ptr时,该地址在某个时刻被发送给MMU进行转换,MMU在页表中查找它,将其转换为内存中的物理地址,以便从中获取数据。

In the first case, it will point to a memory address on our stack that holds the value 100.

在第一种情况下,它将指向堆栈中保存值100的内存地址。

When we pass in 99999999999999 and ask it to fetch what’s stored at that address (which is what dereferencing does), it looks for the translation in the page table but can’t find it.

当我们传入9999999999999999并要求它获取存储在该地址的内容(这就是解引用所做的)时,它在页表中查找翻译,但找不到它。

The CPU then treats this as a page fault.

然后CPU将此视为页面错误。

At boot, the OS provided the CPU with an interrupt descriptor table. This table has a predefined format where the OS provides handlers for the predefined conditions the CPU can encounter.

在启动时,操作系统向CPU提供一个中断描述符表。该表具有预定义的格式,其中操作系统为CPU可能遇到的预定义条件提供处理程序。

Since the OS provided a pointer to a function that handles page fault, the CPU jumps to that function when we try to dereference 99999999999999 and thereby hands over control to the operating system.

由于操作系统提供了一个指向处理页面错误的函数的指针,当我们试图解引用9999999999999999时,CPU跳转到该函数,从而将控制权交给操作系统。

The OS then prints a nice message for us, letting us know that we encountered what it calls a segmentation fault. This message will therefore vary depending on the OS you run the code on.

然后,操作系统为我们打印一条很好的消息,让我们知道我们遇到了它所谓的分段错误。因此,该消息将根据运行代码的操作系统而有所不同。

But can’t we just change the page table in the CPU?

但是我们不能直接改变CPU中的页表吗?

Now, this is where the privilege level comes in. Most modern operating systems operate with two ring levels: ring 0, the kernel space, and ring 3, the user space.

这就是特权级别的用武之地。大多数现代操作系统都使用两个环级别:环0(内核空间)和环3(用户空间)。

在这里插入图片描述

Most CPUs have a concept of more rings than what most modern operating systems use. This has historical reasons, which is also why ring 0 and ring 3 are used (and not 1 and 2).

大多数cpu都有一个比大多数现代操作系统使用的更多环的概念。这是有历史原因的,这也是为什么使用环0和环3(而不是环1和环2)。

Every entry in the page table has additional information about it. Amongst that information is the information about which ring it belongs to. This information is set up when your OS boots up.

页表中的每个条目都有关于它的附加信息。在这些信息中有关于它属于哪个戒指的信息。此信息在操作系统启动时设置。

Code executed in ring 0 has almost unrestricted access to external devices and memory, and is free to change registers that provide security at the hardware level.

在ring 0中执行的代码几乎可以不受限制地访问外部设备和内存,并且可以自由地更改在硬件级别提供安全性的寄存器。

The code you write in ring 3 will typically have extremely restricted access to I/O and certain CPU registers (and instructions). Trying to issue an instruction or setting a register from ring 3 to change the page table will be prevented by the CPU. The CPU will then treat this as an exception and jump to the handler for that exception provided by the OS.

您在ring 3中编写的代码通常对I/O和某些CPU寄存器(和指令)的访问非常有限。试图从ring 3发出指令或设置寄存器来更改页表将被CPU阻止。然后CPU将此视为异常,并跳转到操作系统提供的异常处理程序。

This is also the reason why you have no other choice than to cooperate with the OS and handle I/O tasks through syscalls. The system wouldn’t be very secure if this wasn’t the case.

这也是为什么除了与操作系统合作并通过系统调用处理I/O任务之外别无选择的原因。如果不是这样,系统就不会很安全。

So, to sum it up: yes, the CPU and the OS cooperate a great deal. Most modern desktop CPUs are built with an OS in mind, so they provide the hooks and infrastructure that the OS latches onto upon bootup. When the OS spawns a process, it also sets its privilege level, making sure that normal processes stay within the borders it defines to maintain stability and security.

所以,总结一下:是的,CPU和操作系统合作得很好。大多数现代桌面cpu都是在考虑操作系统的情况下构建的,因此它们提供了操作系统在启动时锁定的钩子和基础设施。当操作系统生成一个进程时,它还会设置其特权级别,确保正常进程保持在它为维护稳定性和安全性而定义的边界内。

Interrupts, firmware, and I/O

中断、固件和I/O

We’re nearing the end of the general CS subjects in this book, and we’ll start to dig our way out of the rabbit hole soon.

我们已经接近这本书中一般CS主题的结尾了,我们很快就会开始从兔子洞里挖出来。

This part tries to tie things together and look at how the whole computer works as a system to handle I/O and concurrency.

这一部分试图将这些东西联系在一起,并研究整个计算机如何作为一个系统来处理I/O和并发性。

Let’s get to it!

A simplified overview

一个简单的概述

Let’s look at some of the steps where we imagine that we read from a network card:

让我们来看看从网卡读取数据的一些步骤:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Remember that we’re simplifying a lot here. This is a rather complex operation but we’ll focus on the parts that are of most interest to us and skip a few steps along the way.

记住这里我们化简了很多。这是一个相当复杂的操作,但我们将专注于我们最感兴趣的部分,并在此过程中跳过一些步骤。

Step 1 – Our code

We register a socket. This happens by issuing a syscall to the OS. Depending on the OS, we either get a file descriptor (macOS/Linux) or a socket (Windows).

我们注册一个套接字。这是通过向操作系统发出系统调用来实现的。根据操作系统的不同,我们可以得到一个文件描述符(macOS/Linux)或一个套接字(Windows)。

The next step is that we register our interest in Read events on that socket.

下一步是在套接字上注册我们对Read事件的兴趣。

**Step 2 – Registering events with the OS **

This is handled in one of three ways:

  • We tell the operating system that we’re interested in Read events but we want to wait for it tohappen by yielding control over our thread to the OS. The OS then suspends our threadby storing the register state and switches to some other thread.
  • We tell the operating system that we’re interested in Read events but we just want a handle toa task that we can poll to check whether the event is ready or not.
  • We tell the operating system that we are probably going to be interested in many events, butwe want to subscribe to one event queue. When we poll this queue, it will block our threaduntil one or more events occur.

Chapters 3 and 4 will go into detail about the third method, as it’s the most used method for modern async frameworks to handle concurrency.

Step 3 – The network card

We’re skipping some steps here, but I don’t think they’re vital to our understanding.

我们在这里跳过了一些步骤,但我认为它们对我们的理解并不重要。

On the network card, there is a small microcontroller running specialized firmware. We can imagine that this microcontroller is polling in a busy loop, checking whether any data is incoming.

在网卡上,有一个小型微控制器运行专门的固件。我们可以想象这个微控制器正在一个繁忙的循环中轮询,检查是否有数据传入。

The exact way the network card handles its internals is a little different from what I suggest here, and will most likely vary from vendor to vendor. The important part is that there is a very simple but specialized CPU running on the network card doing work to check whether there are incoming events.

网卡处理其内部的确切方式与我在这里建议的略有不同,并且很可能因供应商而异。重要的部分是,在网卡上运行一个非常简单但专门的CPU来检查是否有传入事件。

Once the firmware registers incoming data, it issues a hardware interrupt.

一旦固件注册了传入数据,它就会发出一个硬件中断。

Step 4 – Hardware interrupt

A modern CPU has a set of interrupt request line (IRQs) for it to handle events that occur from external devices. A CPU has a fixed set of interrupt lines.

现代CPU有一组中断请求行(irq),用于处理来自外部设备的事件。CPU有一组固定的中断线。

A hardware interrupt is an electrical signal that can occur at any time. The CPU immediately interrupts its normal workflow to handle the interrupt by saving the state of its registers and looking up the interrupt handler. The interrupt handlers are defined in the interrupt descriptor table (IDT).

硬件中断是一种可以在任何时间发生的电信号。CPU立即中断其正常工作流程,通过保存其寄存器的状态并查找中断处理程序来处理中断。中断处理程序在中断描述符表(IDT)中定义。

Step 5 – Interrupt handler

The IDT is a table where the OS (or a driver) registers handlers for different interrupts that may occur. Each entry points to a handler function for a specific interrupt. The handler function for a network card would typically be registered and handled by a driver for that card.

IDT是一个表,操作系统(或驱动程序)为可能发生的不同中断注册处理程序。每个入口指向一个特定中断的处理程序函数。网卡的处理程序函数通常由该卡的驱动程序注册和处理。

The IDT is not stored on the CPU as it might seem in Figure 1.3. It’s located in a fixed and known location in the main memory. The CPU only holds a pointer to the table in one of its registers.

IDT并不像图1.3所示的那样存储在CPU上。它位于主存储器中一个固定且已知的位置。CPU只在其中一个寄存器中保存一个指向表的指针。

Step 6 – Writing the data

This is a step that might vary a lot depending on the CPU and the firmware on the network card. If the network card and the CPU support direct memory access (DMA), which should be the standard on all modern systems today, the network card will write data directly to a set of buffers that the OS already has set up in the main memory.

这个步骤可能会根据网卡上的CPU和固件而有很大的不同。如果网卡和CPU支持直接内存访问(DMA),这应该是当今所有现代系统的标准,那么网卡将直接将数据写入操作系统已经在主内存中设置的一组缓冲区。

In such a system, the firmware on the network card might issue an interrupt when the data is written to memory. DMA is very efficient, since the CPU is only notified when the data is already in memory. On older systems, the CPU needed to devote resources to handle the data transfer from the network card.

在这样的系统中,当数据被写入内存时,网卡上的固件可能会发出中断。DMA非常高效,因为只有当数据已经在内存中时才会通知CPU。在较旧的系统上,CPU需要投入资源来处理来自网卡的数据传输。

The direct memory access controller ( DMAC) is added to the diagram since in such a system, it would control the access to memory. It’s not part of the CPU as indicated in the previous diagram. We’re deep enough in the rabbit hole now, and exactly where the different parts of a system are is not really important to us right now, so let’s move on.

直接内存访问控制器(DMAC)被添加到图中,因为在这样的系统中,它将控制对内存的访问。它不是前面图中所示的CPU的一部分。我们现在在兔子洞里已经够深了,系统中不同部分的确切位置现在对我们来说并不重要,所以让我们继续。

Step 7 – The driver

The driver would normally handle the communication between the OS and the network card. At some point, the buffers are filled and the network card issues an interrupt. The CPU then jumps to the handler of that interrupt. The interrupt handler for this exact type of interrupt is registered by the driver, so it’s actually the driver that handles this event and, in turn, informs the kernel that the data is ready to be read.

驱动程序通常会处理操作系统和网卡之间的通信。在某个时刻,缓冲区被填满,网卡发出中断。然后CPU跳转到该中断的处理程序。这种中断类型的中断处理程序是由驱动程序注册的,所以实际上是驱动程序处理这个事件,然后通知内核数据已经准备好可以读取。

Step 8 – Reading the data

Depending on whether we chose method 1, 2, or 3, the OS will do as follows:

  • Wake our thread
  • Return Ready on the next poll
  • Wake the thread and return a Read event for the handler we registered

Interrupts

As you know by now, there are two kinds of interrupts:

  • Hardware interrupts
  • Software interrupts

They are very different in nature.

Hardware interrupts are created by sending an electrical signal through an IRQ. These hardware lines signal the CPU directly.

These are interrupts issued from software instead of hardware. As in the case of a hardware interrupt, the CPU jumps to the IDT and runs the handler for the specified interrupt.

Firmware

Firmware doesn’t get much attention from most of us; however, it’s a crucial part of the world we live in. It runs on all kinds of hardware and has all kinds of strange and peculiar ways to make the computers we program on work.

固件并没有得到我们大多数人的太多关注;然而,它是我们生活的世界的重要组成部分。它可以在各种硬件上运行,并有各种奇怪的方式使我们编写程序的计算机工作。

Now, the firmware needs a microcontroller to be able to work. Even the CPU has firmware that makes it work. That means there are many more small ‘CPUs’ on our system than the cores we program against.

现在,固件需要一个微控制器才能工作。甚至CPU也有固件使其工作。这意味着在我们的系统上有更多的小“cpu”,而不是我们编程的核心。

Why is this important? Well, you remember that concurrency is all about efficiency, right? Since we have many CPUs/microcontrollers already doing work for us on our system, one of our concerns is to not replicate or duplicate that work when we write code.

为什么这很重要?好吧,你记得并发是关于效率的,对吧?由于我们的系统上已经有许多cpu /微控制器在为我们工作,因此我们的关注点之一是在编写代码时不要复制或重复这些工作。

If a network card has firmware that continually checks whether new data has arrived, it’s pretty wasteful if we duplicate that by letting our CPU continually check whether new data arrives as well. It’s much better if we either check once in a while, or even better, get notified when data has arrived.

如果网卡的固件不断检查是否有新数据到达,如果我们让CPU不断检查是否有新数据到达,这是相当浪费的。如果我们每隔一段时间检查一次,或者在数据到达时得到通知,那就更好了。

Summary

This chapter covered a lot of ground, so good job on doing all that legwork. We learned a little bit about how CPUs and operating systems have evolved from a historical perspective and the difference between non-preemptive and preemptive multitasking. We discussed the difference between concurrency and parallelism, talked about the role of the operating system, and learned that system calls are the primary way for us to interact with the host operating system. You’ve also seen how the CPU and the operating system cooperate through an infrastructure designed as part of the CPU.

这一章涉及了很多内容,所以你要好好做那些跑腿的工作。我们从历史的角度了解了cpu和操作系统是如何演变的,以及非抢占式和抢占式多任务之间的区别。我们讨论了并发性和并行性之间的区别,讨论了操作系统的角色,并了解到系统调用是我们与主机操作系统交互的主要方式。您还了解了CPU和操作系统如何通过作为CPU一部分设计的基础设施进行协作。

Lastly, we went through a diagram on what happens when you issue a network call. You know there are at least three different ways for us to deal with the fact that the I/O call takes some time to execute, and we have to decide which way we want to handle that waiting time.

最后,我们通过一个图表来说明发出网络调用时会发生什么。您知道,至少有三种不同的方法可以处理I/O调用需要一些时间来执行的事实,我们必须决定我们想要哪种方法来处理等待时间。

This covers most of the general background information we need so that we have the same definitions and overview before we go on. We’ll go into more detail as we progress through the book, and the first topic that we’ll cover in the next chapter is how programming languages model asynchronous program flow by looking into threads, coroutines and futures.

这涵盖了我们需要的大部分一般背景信息,以便我们在继续之前有相同的定义和概述。随着本书的深入,我们将更详细地讨论,下一章将讨论的第一个主题是编程语言如何通过查看线程、协程和future来对异步程序流进行建模。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/845547.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

162.二叉树:填充每个节点的下一个右侧节点指针(力扣)

代码解决 /* // Definition for a Node. class Node { public:int val;Node* left;Node* right;Node* next;Node() : val(0), left(NULL), right(NULL), next(NULL) {}Node(int _val) : val(_val), left(NULL), right(NULL), next(NULL) {}Node(int _val, Node* _left, Node* _…

ipv6基础

地址 前缀子网主机位 PI法则3.14 前缀:3个16位 子网:1个16位 接口ID:4个16位 地址分类 未指定地址 ::/128 ,类似于0.0.0.0 本地回环地址 ::1/128 ,用于本地测试,类似于127.0.0.1 本地链路地址&#x…

利用GNSS IMU集成提高车道级定位精度

准确的定位对于很多不同的事情都是至关重要的。导航系统可以引导我们去某个地方,自动驾驶汽车可以利用这些数据在道路上安全行驶。尽管全球导航卫星系统(GNSS)在定位方面非常出色,但它们可能并不总是提供最准确的车道水平事实。解决这个问题的一个有希望…

如何矢将量数据转换为栅格数据

在我们分析GIS数据时,有时候也可能需要将矢量数据转换为栅格数据来使用,例如:使用AI图像识别技术进行GIS数据分析或导航的时候!矢量数据就可能不满足需求了! GIS数据转换器-矢量V5.0具有矢量数据转换为栅格数据的功能…

Python魔法之旅-魔法方法(07)

目录 一、概述 1、定义 2、作用 二、应用场景 1、构造和析构 2、操作符重载 3、字符串和表示 4、容器管理 5、可调用对象 6、上下文管理 7、属性访问和描述符 8、迭代器和生成器 9、数值类型 10、复制和序列化 11、自定义元类行为 12、自定义类行为 13、类型检…

linux部署运维1——centos7.9离线安装部署涛思taos2.6时序数据库TDengine

在实际项目开发过程中,并非一直都使用关系型数据库,对于工业互联网类型的项目来说,时序型数据库也是很重要的一种,因此掌握时序数据库的安装配置也是必要的技能,不过对于有关系型数据库使用的开发工作者来说&#xff0…

如何获取SSL证书,消除网站不安全警告

获取SSL证书通常涉及以下几个步骤: 选择证书颁发机构(CA): 你需要从受信任的SSL证书颁发机构中选择一个,比如DigiCert、GlobalSign、JoySSL等。部分云服务商如阿里云、腾讯云也提供免费或付费的SSL证书服务。 生成证…

大数据之HIVE,一次HIVESQL执行的过程(四)

在hive中执行如下sql INSERT OVERWRITE TABLE XXX SELECT * from XXX 数据最终是怎么存储到hdfs上的过程 执行的过程当中,打印出如下的日志过程,本质上是一个在MapReduce中进行Shuffle的过程 所以下面就Shuffle的过程进行分析 Shuffle 描述的是数据从 Map 端到 Reduce 端的…

曾巩,散文的艺术与哲思

曾巩,字子固,世称南丰先生,南丰(今江西)人,生于北宋真宗天禧三年(公元1019年),卒于北宋元丰六年(公元1083年),享年64岁。他是中国北宋…

http协议及httpd安装组成

文章目录 一、http协议http协议通信过程http相关技术网站访问量HTTP工作机制HTTP协议版本HTTP请求访问的完整过程HTTP报文头部响应报文 二、httpd安装组成apache介绍和特点工作模式( MPM multi-processing module )Http相关文件Http编译安装httpd常见配置…

显示器与电脑如何分屏显示?

1.点击电脑屏幕右键--显示设置 2、然后找到屏幕---找到多显示器---选择扩展显示器

OpenAI 推出ChatGPT Edu,为高校定制版本

近日,OpenAI 宣布推出 ChatGPT Edu,这是一款专为高校打造的 ChatGPT 版本,旨在帮助学生、教师、研究人员和校园运营部门以负责任的方式部署和使用 AI。 ChatGPT Edu 由 GPT-4o 提供支持,具备强大的文本和图像推理能力,…

Java18新版本特性!

Java 18引入了多项新特性,主要包括默认UTF-8字符集、简单的Web服务器、栈步进API等。Java 18是Oracle在2022年发布的版本,其旨在通过一系列创新特性来提升开发效率与性能。下面将逐一探讨Java 18的主要新特性以及它们对开发者的具体影响: 默认…

7、css3实现边框不停地跑动效果

效果例图&#xff1a; 1、上html代码&#xff1a; <!DOCTYPE html> <html lang"zh"> <head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><meta …

文明互鉴促发展——2024“国际山地旅游日”主题活动在法国启幕

5月29日&#xff0c;2024“国际山地旅游日”主题活动在法国尼斯市成功举办。中国驻法国使领馆、法国文化旅游部门、地方政府、国际组织、国际山地旅游联盟会员代表、旅游机构、企业、专家、媒体等围绕“文明互鉴的山地旅游”大会主题和“气候变化与山地旅游应对之策”论坛主题展…

GNU Radio实现OFDM Radar

文章目录 前言一、GNU Radio Radar Toolbox编译及安装二、ofdm radar 原理讲解三、GNU Radio 实现 OFDM Radar1、官方提供的 grc①、grc 图②、运行结果 2、修改后的便于后续可实现探测和通信的 grc①、grc 图②、运行结果 四、资源自取 前言 本文使用 GNU Radio 搭建 OFDM Ra…

项目3 构建移动电商服务器集群

项目引入 经过前期加班加点地忙碌&#xff0c;我们的网站顺利上线了&#xff01;年中促销活动也如约而至&#xff0c;虽然公司全体对这次活动进行多方面地准备和“布防”&#xff0c;可是意外还是发生了。就在促销优惠购物活动的当天&#xff0c;猛然增加的用户访问量直接导致浏…

java线程状态介绍

1.新建&#xff08;New&#xff09;: 线程对象已创建&#xff0c;但还没有调用 start() 方法。 2.可运行&#xff08;Runnable&#xff09;: 线程已启动&#xff0c;处于就绪状态&#xff0c;等待 JVM 的线程调度器分配CPU时间。 3.阻塞&#xff08;Blocked&#xff09;: 线程…

利用博弈论改进大模型性能:MIT最新研究解读

引言 在人工智能和大模型的发展过程中&#xff0c;我们常常遇到一个有趣的现象&#xff1a;同一个问题在不同形式下可能得到不同的答案。这种不一致性不仅降低了大模型的可信度&#xff0c;也限制了其在实际应用中的效果。为了应对这一问题&#xff0c;来自MIT的研究人员提出了…

Element快速入门

Vue组件库Element 1 Element介绍 vue是侧重于VM开发的&#xff0c;主要用于数据绑定到视图的&#xff0c;ElementUI就是一款侧重于V开发的前端框架&#xff0c;主要用于开发美观的页面的。 Element&#xff1a;是饿了么公司前端开发团队提供的一套基于 Vue 的网站组件库&…