type
status
slug
date
summary
tags
category
password
icon
什么是异步Rust语言中的异步机制:协程一些基本介绍Rust的异步vs其它语言Rust中的异步vs线程语言和库支持Future Trait200行讲解Rust future绿色线程(Green Threads)回调(略)从回调到承诺(promise)Rust中的FuturesLeaf futuresNon-leaf-futures什么是运行时(Runtimes)A useful mental model of an async runtime这是我的理解图:Rust标准库做了啥I/O密集型 VS CPU密集型任务Bonus section - additional notes on Futures and WakersWaker and ContextUnderstanding the WakerFat pointers in RustBonus sectionGenerators and async/await Stackless coroutines/generatorsHow generators work
PINPinning to the stackPinning to the heapPractical rules for PinningPin and DropImplementing Futures - main exampleThe ExecutorThe Future implementation
The Reactor (important)Async/Await and concurrency
什么是异步
异步的实现甚至可以不需要多线程,它和多线程不是一个维度的概念。但很多时候异步都会在多线程的基础上进行实现更高的效率例如:
我们将异步应用于单线程或多线程当中,多线程只是异步编程的一种实现形式。
现代化的异步编程在使用体验上跟同步编程也几无区别,例如 Go 语言的
go
关键字,也包括我们后面将介绍的 async/await
语法,该语法是 JavaScript
和 Rust
的核心特性之一。Rust语言中的异步机制:协程
(coroutine, Future): A future is a representation of some operation which will complete in the future.
一些基本介绍
Rust的异步vs其它语言
- Rust 中 Futures 是惰性的,并且只有被轮询才会进一步执行。丢弃(Dropping)一个future 可以阻止它继续执行。
- Rust 中的 异步是零成本的,这意味着你只需要为你所使用的东西付出代价。特别来说,你使用异步时可以不需要堆分配或动态分发,这对性能来说是好事!这也使得你能够在约束环境下使用异步,例如嵌入式系统。
- Rust 不提供内置运行时。相反,运行时由社区维护的库提供。
- Rust里 单线程的和多线程的 运行时都可用,而他们会有不同的优劣。
ps:后面两条特点不是很理解
Rust中的异步vs线程
Rust 中异步的首选替代是使用 OS 线程,可以直接通过
std::thread
或者间接通过线程池来使用。从线程模型迁移到异步模型,或者反过来,通常需要一系列重构的工作,既包括内部实现也包括任何暴露的公开接口(如果你在构建一个库)。因此,尽早地选择适合你需要的模型能够节约大量的开发事件。
异步 极大地降低了 CPU 和内存开销,尤其是在负载大量越过IO 边界的任务,例如服务器和数据库。同样,你可以处理比 OS 线程更高数量级的任务,因为异步运行时使用少量(昂贵的)线程来处理大量(便宜的)任务。然而,异步 Rust 会导致更大的二进制体积,因为异步函数会生成状态机,并且每个可执行文件都会绑定一个异步运行时。
最后一点, Rust 不会强制你从线程模型和异步模型中间只选一个。你可以在同一个应用里同时使用两个模型,这在你混合了线程化的和异步的依赖时非常有用。事实上,你甚至可以同时使用不同的并发模型,例如事件驱动编程,只要你能找到一个实现它的库。语言和库支持
async
的底层实现非常复杂,且会导致编译后文件体积显著增加,因此 Rust 没有选择像 Go 语言那样内置了完整的特性和运行时,而是选择了通过 Rust 语言提供了必要的特性支持,再通过社区来提供 async
运行时的支持。 因此要完整的使用 async
异步编程,你需要依赖以下特性和外部库:- 所必须的特征(例如
Future
)、类型和函数,由标准库提供实现
- 关键字
async/await
由 Rust 语言提供,并进行了编译器层面的支持
- 众多实用的类型、宏和函数由官方开发的
futures
包提供(不是标准库),它们可以用于任何async
应用中。
目前主流的
async
运行时几乎都使用了多线程实现,相比单线程虽然增加了并发表现,但是对于执行性能会有所损失,因为多线程实现会有同步和切换上的性能开销,若你需要极致的顺序执行性能,那么 async
目前并不是一个好的选择。Future Trait
Future 能通过调用
poll
的方式推进,这会尽可能地推进 future 到完成状态。如果 future 完成了, 那就会返回 poll::Ready(result)
。如果 future 尚未完成,则返回 poll::Pending
,并且安排 wake()
函数在 Future
准备好进一步执行时调用(译者注:注册回调函数)。当 wake()
调用 时,驱动 Future
的执行器会再次 poll
使得 Future
有所进展。
200行讲解Rust future
翻到的保留原文
首先讲讲多任务处理的其它选项:
绿色线程(Green Threads)
绿色线程使用与操作系统相同的机制,为每个任务创建一个线程,设置一个堆栈,保存 CPU 状态,并通过“上下文切换”从一个任务(线程)跳转到另一个任务(线程)。
我们将控制权交给调度程序(在这样的系统中,调度程序是运行时的核心部分) ,然后调度程序继续运行不同的任务。Rust曾经支持绿色线程,但他们它达到1.0之前被删除了, 执行状态存储在每个栈中,因此在这样的解决方案中不需要
async
await
Futures
或者Pin
。典型的流程是这样的:
- 运行一些非阻塞代码
- 对某些外部资源进行阻塞调用
- 跳转到“main”线程,该线程调度一个不同的线程来运行,并“跳转”到该栈中
- 在新线程上运行一些非阻塞代码,直到新的阻塞调用或任务完成
- “跳转”回到“main"线程 ,调度一个新线程,这个新线程的状态已经是
Ready
,然后跳转到该线程
缺点:
- 栈大小可能需要增长,解决这个问题不容易,并且会有成本: 栈拷贝,指针等问题
- 它不是一个零成本抽象(这也是Rust早期有绿色线程,后来删除的原因之一)
- 如果您想要支持许多不同的平台,就很难正确实现
回调(略)
从回调到承诺(promise)
Rust中的Futures
A future is a representation of some operation which will complete in the future.
Rust中的异步实现基于轮询,每个异步任务分成三个阶段:
- 轮询阶段(The Poll phase). 一个
Future
被轮询(polled)后,会开始执行,直到被阻塞. 我们经常把轮询一个Future这部分称之为执行器(executor)
- 等待阶段. 事件源(通常称为reactor)注册等待一个事件发生,并确保当该事件准备好时唤醒相应的
Future
An event source, most often referred to as a reactor, registers that a Future is waiting for an event to happen and makes sure that it will wake the Future when that event is ready.
- 唤醒阶段. 事件发生,相应的
Future
被唤醒。 现在轮到执行器(executor),就是第一步中的那个执行器,调度Future
再次被轮询,并向前走一步,直到它完成或达到一个阻塞点,不能再向前走, 如此往复,直到最终完成.
当我们谈论
Future
的时候,我发现在早期区分non-leaf-future
和leaf-future
是很有用的,因为实际上它们彼此很不一样。Leaf futures
Runtimes create leaf futures which represent a resource like a socket.
Operations on these resources, like a
Read
on a socket, will be non-blocking and return a future
which we call a leaf future
since it's the future
which we're actually waiting on.
It's unlikely that you'll implement a leaf future yourself unless you're writing a runtime, but we'll go through how they're constructed in this book as well.Non-leaf-futures
Non-leaf-futures are the kind of futures we as users of a runtime write ourselves using the
async
keyword to create a task which can be run on the executor.The bulk(主体) of an async program will consist of non-leaf-futures, which are a kind of pause-able computation. This is an important distinction since these futures represents a set of operations. Often, such a task will
await
a leaf future as one of many operations to complete the task.它们能够将控制权交给运行时的调度程序,然后在稍后停止的地方继续执行。 与
leaf-future
相比,这些Future本身并不代表I/O资源。 当我们对这些Future进行轮询时, 有可能会运行一段时间或者因为等待相关资源而让度给调度器,然后等待相关资源ready的时候唤醒自己。The key to these tasks is that they're able to yield(放弃) control to the runtime's scheduler and then resume(继续) execution again where it left off at a later point.
In contrast to
leaf futures
, these kind of futures do not themselves represent an I/O resource. When we poll them they will run until they get to a leaf-future
which returns Pending
and then yield control to the scheduler (which is a part of what we call the runtime).
什么是运行时(Runtimes)
指「程序运行的时候」,即程序生命周期中的一个阶段。例句:「Rust 比 C 更容易将错误发现在编译时而非运行时。」 指「运行时库」,即 glibc 这类原生语言的标准库。例句:「C 程序的 malloc 函数实现需要由运行时提供。」指「运行时系统」,即某门语言的宿主环境。例句:「Node.js 是一个 JavaScript 的运行时。」
编译阶段是 compile time,链接阶段是 link time,那运行起来的阶段自然就是 run time
像 c# ,JavaScript,Java,GO 和许多其他语言都有一个处理并发的运行时。 所以如果你来自这些语言中的一种,这对你来说可能会有点奇怪。
Rust 与这些语言的不同之处在于 Rust 没有处理并发性的运行时,因此您需要使用一个为您提供此功能的库。
Rust 和其他语言的区别在于,在选择运行时时,您必须进行主动选择。大多数情况下,在其他语言中,你只会使用提供给你的那一种。
异步运行时可以分为两部分: 1. 执行器(The Executor) 2. reactor (The Reactor)
当 Rusts Futures 被设计出来的时候,有一个愿望,那就是将通知
Future
它可以做更多工作的工作与Future
实际做工作分开。你可以认为前者是reactor的工作,后者是executor的工作。 运行时的这两个部分使用
Waker
进行交互。A useful mental model of an async runtime
A fully working async system in Rust can be divided into three parts:
- Reactor(反应器)
- Executor
- Future
So, how does these three parts work together? They do that through an object called the
Waker
.
Waker
用来告诉executor
:一个特定的Future可以跑了(ready了)。需要先了解Waker的life cycle
- 一个Waker是被executor创建的
- 当future第一次被executor轮询的时候,它会被分配一个Waker对象(由这个executor创建的)的clone. 因为这是一个shared object(比如Arc<T>), 所有的clone实际上都是指针指向同样的对象。因此,任何调用了任意一个原始Waker clone的情况都会唤醒对应的哪一个注册的Future
- 这个Future clones the Waker然后把它传给了reactor
A Waker is created by the executor.
When a future is polled the first time by the executor, it’s given a clone of the Waker object created by the executor. Since this is a shared object (e.g. anArc<T>
), all clones actually point to the same underlying object. Thus, anything that calls any clone of the original Waker will wake the particular Future that was registered to it.
The future clones the Waker and passes it to the reactor, which stores it to use later.
You could think of a "future" like a channel for the
Waker
: The channel starts with the future that's polled the first time by the executor and is passed a handle to a Waker
. It ends in a leaf-future which passes that handle to the reactor.这是我的理解图:
At some point in the future, the reactor will decide that the future is ready to run. It will wake the
future
via the Waker
that it stored.Since the interface is the same across all executors, reactors can in theory be completely oblivious(不注意的、不察觉的) to the type of the executor, and vice-versa. Executors and reactors never need to communicate with one another directly.
This design is what gives the futures framework it's power and flexibility and allows the Rust standard library to provide an ergonomic(人类工程学的), zero-cost abstraction for us to use.
居然原文有好的slides解释:
Rust标准库做了啥
- 一个公共接口,
Future trait
- 一个符合人体工程学的方法创建任务, 可以通过async和await关键字进行暂停和恢复
Future
Waker
接口, 可以唤醒暂停的Future
这就是Rust标准库所做的。 不包括:异步I/O的定义,这些异步任务是如何被创建的,如何运行的。
- A common interface representing an operation which will be completed in the future through the
Future
trait.
- An ergonomic way of creating tasks which can be suspended and resumed through the
async
andawait
keywords.
- A defined interface to wake up a suspended task through the
Waker
type.
That's really what Rust's standard library does. As you see there is no definition of non-blocking I/O, how these tasks are created, or how they're run.
I/O密集型 VS CPU密集型任务
一个异步块:
两个
yield
之间的代码与我们的执行器在同一个线程上运行。
这意味着当我们分析器处理数据集时,执行器忙于计算而不是处理新的请求(这样的话,IO任务好了的时候,没办法重新恢复执行)- 我们可以创建一个新的
leaf future
,它将我们的任务发送到另一个线程,并在任务完成时解析。 我们可以像等待其他Future一样等待这个leaf-future
。
- 运行时可以有某种类型的管理程序来监视不同的任务占用多少时间,并将执行器本身移动到不同的线程,这样即使我们的分析程序任务阻塞了原始的执行程序线程,它也可以继续运行。
- 您可以自己创建一个与运行时兼容的
reactor
,以您认为合适的任何方式进行分析,并返回一个可以等待的future。
the code we write between the yield points are run on the same thread as our executor.
That means that while our
analyzer
is working on the dataset, the executor is busy doing calculations instead of handling new requests.(这里的含义我觉得是这样的:当analyzer在工作的时候(cpu秘籍的分析数据工作,也就是计算),这个executor因为和analyzer run在same thread所以会导致它没法工作—handling new requests,只能让位于analyzer的calculations)- We could create a new leaf future which sends our task to another thread and resolves when the task is finished. We could
await
this leaf-future like any other future.
- The runtime could have some kind of supervisor that monitors how much time different tasks take, and move the executor itself to a different thread so it can continue to run even though our
analyzer
task is blocking the original executor thread.
- You can create a
reactor
yourself which is compatible with the runtime which does the analysis any way you see fit, and returns a Future which can be awaited.
Now, #1 is the usual way of handling this, but some
executors
implement #2 as well. The problem with #2 is that if you switch runtime you need to make sure that it supports this kind of supervision as well or else you will end up blocking the executor.And #3 is more of theoretical importance,
normally you'd be happy by sending the task to the thread-pool most runtimes provide.
=======================================================================
Most executors have a way to accomplish #1 using methods like
spawn_blocking
.These methods send the task to a thread-pool created by the runtime where you can either perform CPU-intensive tasks or "blocking" tasks which are not supported by the runtime.
=======================================================================
原文贴了一篇关于Async history的文章:
Bonus section - additional notes on Futures and Wakers
In this section we take a deeper look at some advantages of having a loose coupling(松耦合) between the Executor-part and Reactor-part of an async runtime.
Earlier in this chapter, I mentioned that it is common for the executor to create a new Waker for each Future that is registered with the executor, but that the Waker is a shared object similar to a
Arc<T>
. One of the reasons for this design is that it allows different Reactors the ability to Wake a Future.Waker and Context
The
Waker
type allows for a loose coupling between the reactor-part and the executor-part of a runtime.
By having a wake up mechanism that is not tied to the thing that executes the future, runtime-implementors can come up with interesting new wake-up mechanisms. An example of this can be spawning a thread to do some work that eventually notifies the future, completely independent of the current runtime.
通过wake up的机制(不和执行execute future耦合),运行时实现能够产生一个有趣的新的wake-up 机制。一个例子就是:产生(spawn)一个线程去做最终唤醒future的工作,这个部分完全独立于目前的运行时。自己整理:
Understanding the Waker
在 Rust 中,当我们自己实现 Futures 时,一个比较复杂的部分就是实现 Waker。Waker 是用于唤醒异步任务的对象。
创建一个 Waker 需要创建一个虚函数表(vtable)。虚函数表是一种在编译时确定的数据结构,它包含了一组指向虚函数的指针。通过虚函数表,我们可以在运行时根据对象的实际类型来调用对应的方法,这就是所谓的动态分派。
在这个过程中,我们需要创建一个类型被擦除的 trait 对象。类型擦除是一种技术,它允许我们忽略对象的具体类型,只关注它实现了哪些 trait。这样,我们就可以在运行时动态地调用对象上的方法,而不需要在编译时知道对象的具体类型。
One of the most confusing things we encounter when implementing our own
Future
s is how we implement a Waker
. Creating a Waker
involves creating a vtable
which allows us to use dynamic dispatch to call methods on a type erased trait object we construct ourselves.The
Waker
implementation is specific to the type of executor in use, but all Wakers share a similar interface. It's useful to think of it as a Trait
. It's not implemented as such since that would require us to treat it like a trait object like &dyn Waker
or Arc<dyn Waker>
which either restricts the API by requiring a &dyn Waker
trait object, or would require an Arc<dyn Waker>
which in turn requires a heap allocation which a lot of embedded-like systems can't do.Having the Waker implemented the way it is supports users creating a statically-allocated wakers and even more exotic mechanisms to on platforms where that makes sense.
自己整理:
亲测搭配这个使用更好👇(看整个章):
ok,让我们返回来好好细讲
Fat pointers in Rust
Let's start by taking a look at the size of some different pointer types in Rust.
Example
&[i32]
:- The first 8 bytes is the actual pointer to the first element in the array (or part of an array the slice refers to)
- The second 8 bytes is the length of the slice.
Example
&dyn SomeTrait
:This is the type of fat pointer we'll concern ourselves about going forward.
&dyn SomeTrait
is a reference to a trait, or what Rust calls a trait object.The layout for a pointer to a trait object looks like this:
- The first 8 bytes points to the
data
for the trait object
- The second 8 bytes points to the
vtable
for the trait object
This is the type of fat pointer we'll concern ourselves about going forward.
&dyn SomeTrait
is a reference to a trait, or what Rust calls a trait object.The layout for a pointer to a trait object looks like this:
- The first 8 bytes points to the
data
for the trait object
- The second 8 bytes points to the
vtable
for the trait object
The reason for this is to allow us to refer to an object we know nothing about except that it implements the methods defined by our trait. To accomplish this we use dynamic dispatch.
Bonus section
You might wonder why the
Waker
was implemented like this and not just as a normal trait?The reason is flexibility. Implementing the Waker the way we do here gives a lot of flexibility of choosing what memory management scheme to use.
The "normal" way is by using an
Arc
to use reference count keep track of when a Waker object can be dropped. However, this is not the only way, you could also use purely global functions and state, or any other way you wish.This leaves a lot of options on the table for runtime implementors.
Generators and async/await
插播一个RFC文档:
2033-experimental-coroutines.md
rust-lang
这个我觉得写得不错,摘抄一下如下:
Before we go on to our final solution below it's worth pointing out that a popular solution to this problem of generating a future is to side step(绕过) this completely with the concept of green threads.
With a green thread you can suspend a thread by simply context switching away and there's no need to generate state and such as an allocated stack implicitly holds all this state.
对于绿色线程,您可以通过简单地切换上下文来挂起线程,并且不需要生成状态,例如已分配的堆栈隐式地保存所有这些状态
While this does indeed solve our problem of "how do we translate
#[async]
functions" it unfortunately violates Rust's general theme of "zero cost abstractions" because the allocated stack on the side can be quite costly.
It should suffice to say, though, that the feature of "stackless coroutines" in the compiler is precisely targeted at generating the state machine we wanted to write by hand above, solving our problem!不过,应该说,编译器中的“无堆栈协同程序”特性正是针对生成我们想要手工编写的状态机,解决我们的问题!
Coroutines are, however, a little lower level than futures themselves. The stackless coroutine feature can be used not only for futures but also other language primitives like iterators.
At a high-level, though, stackless coroutines in the compiler would be implemented as:
- No implicit memory allocation
- Coroutines are translated to state machines internally by the compiler
- The standard library has the traits/types necessary to support the coroutines language feature.
It's worth briefly mentioning, however, some high-level design goals of the concept of stackless coroutines:
- Coroutines should be compatible with libcore. That is, they should not require any runtime support along the lines of allocations, intrinsics, etc.
- As a result, coroutines will roughly compile down to a state machine that's advanced forward as its resumed. Whenever a coroutine yields it'll leave itself in a state that can be later resumed from the yield statement.
- Coroutines should work similarly to closures in that they allow for capturing variables and don't impose dynamic dispatch costs. Each coroutine will be compiled separately (monomorphized) in the way that closures are today.
- Coroutines should also support some method of communicating arguments in and out of itself. For example when yielding a coroutine should be able to yield a value. Additionally when resuming a coroutine may wish to require a value is passed in on resumption.
Basically, there were three main options discussed when designing how Rust would handle concurrency:
- Stackful coroutines, better known as green threahds.
- Using combinators.
- Stackless coroutines, better known as generators.
Stackless coroutines/generators
Async in Rust is implemented using Generators. So to understand how async really works we need to understand generators first. Generators in Rust are implemented as state machines.
How generators work
实操跑来跑这段代码,懂了:
Now that you know that the
yield
keyword in reality rewrites your code to become a state machine, you'll also know the basics of how await
works. It's very similar.PIN
Pin consists of the
Pin
type and the Unpin
marker. Pin's purpose in life is to govern the rules that need to apply for types which implement !Unpin
.
Yep, you're right, that's double negation right there. !Unpin
means "not-un-pin".
Now, we can solve this problem by using
Pin
instead. Let's take a look at what our example would look like then:Pinning to the stack
Now, what we've done here is pinning an object to the stack. That will always be
unsafe
if our type implements !Unpin
.
We use the same tricks here, including requiring an init
. If we want to fix that and let users avoid unsafe
we need to pin our data on the heap instead which we've shown above.
Pinning to the heap
For completeness let's remove some unsafe and the need for an
init
method at the cost of a heap allocation. Pinning to the heap is safe so the user doesn't need to implement any unsafe code:The fact that it's safe to pin heap allocated data even if it is
!Unpin
makes sense. Once the data is allocated on the heap it will have a stable address.
There is no need for us as users of the API to take special care and ensure that the self-referential pointer stays valid.Practical rules for Pinning
- If
T: Unpin
(which is the default), thenPin<'a, T>
is entirely equivalent to&'a mut T
. in other words:Unpin
means it's OK for this type to be moved even when pinned, soPin
will have no effect on such a type.
- Getting a
&mut T
to a pinned T requires unsafe ifT: !Unpin
. In other words: requiring a pinned pointer to a type which is!Unpin
prevents the user of that API from moving that value unless they choose to writeunsafe
code.
Unpin
是一个标记 trait,表示一个类型可以被安全地移出 Pin
。如果一个类型没有实现 Unpin
(即 T: !Unpin
),那么这个类型的 Pin
不能被安全地转换为 &mut T
。这是因为 &mut T
允许你移动 T
,但是 Pin
保证 T
的位置不会改变。因此,如果你想要获取一个 Pin<T>
的 &mut T
,你需要使用 unsafe
代码,因为你需要保证你不会移动 T
。这条规则的目的是防止 API 的用户意外地移动被固定的值。如果一个 API 返回一个
Pin<T>
,并且 T: !Unpin
,那么用户不能移动 T
,除非他们选择编写 unsafe
代码。这样,API 的设计者可以确保 T
的位置不会被意外地改变。- Pinning does nothing special with memory allocation like putting it into some "read only" memory or anything fancy. It only uses the type system to prevent certain operations on this value.
- Most standard library types implement
Unpin
. The same goes for most "normal" types you encounter in Rust.Future
s andGenerator
s are two exceptions.
- The main use case for
Pin
is to allow self referential types, the whole justification for stabilizing them was to allow that.
- The implementation behind objects that are
!Unpin
is most likely unsafe. Moving such a type after it has been pinned can cause the universe to crash. As of the time of writing this book, creating and reading fields of a self referential struct still requiresunsafe
(the only way to do it is to create a struct containing raw pointers to itself).
- You can add a
!Unpin
bound on a type on nightly with a feature flag, or by addingstd::marker::PhantomPinned
to your type on stable.
- You can either pin an object to the stack or to the heap.
- Pinning a
!Unpin
object to the stack requiresunsafe
- Pinning a
!Unpin
object to the heap does not requireunsafe
. There is a shortcut for doing this usingBox::pin
.
Unsafe code does not mean it's literally "unsafe", it only relieves the guarantees you normally get from the compiler. An
unsafe
implementation can be perfectly safe to do, but you have no safety net.
我到这里的感觉,pin这个只是给编译器的一个marker,也不会占用内存,只是用于实现规避那些移动内存的操作,用于表示被标记的对象的存储地址不会移动,仅此而已。Pin and Drop
The
Pin
guarantee exists from the moment the value is pinned until it's dropped. In the Drop
implementation you take a mutable reference to self
, which means extra care must be taken when implementing Drop
for pinned types.这几个地方的类型应该注意一下,之前被这个语法搞得懵懵的,现在想清楚类型了就明白了。
Hopefully, after this you'll have an idea of what happens when you use the
yield
or await
keywords inside an async function, and why we need Pin
if we want to be able to safely borrow across yield/await
points.
而为什么要保存pin不变,这里体现在我们示例代码的下面这个环节,
考虑一个生成器或异步任务,它在堆上创建了一个对象,并保存了一个指向该对象的指针。然后,它 yield,暂停其执行。稍后,当它被恢复时,如果该对象被移动了,那么保存的指针就会变得无效,导致未定义行为。
为了防止这种情况,我们可以使用
Pin
来确保对象的位置不会改变。当一个对象被固定(pinned)时,你不能获取到该对象的 &mut
引用(除非该对象实现了 Unpin
trait),因此你不能移动该对象。这样,生成器或异步任务就可以安全地保存指向堆上对象的指针,即使在 yield 点之间。Getting a
&mut T
to a pinned T requires unsafe if T: !Unpin
. In other words: requiring a pinned pointer to a type which is !Unpin
prevents the user of that API from moving that value unless they choose to write unsafe
code.Implementing Futures - main example
The Executor
The executors responsibility is to take one or more futures and run them to completion.
The first thing an
executor
does when it gets a Future
is polling it.When polled one of three things can happen:
- The future returns
Ready
and we schedule whatever chained operations to run
- The future hasn't been polled before so we pass it a
Waker
and suspend it
- The futures has been polled before but is not ready and returns
Pending
Rust provides a way for the Reactor and Executor to communicate through the
Waker
. The reactor stores this Waker
and calls Waker::wake()
on it once a Future
has resolved and should be polled again.
It's worth noting that simply calling
thread::park
as we do here can lead to both deadlocks and errors. We'll explain a bit more later and fix this if you read all the way to the Bonus Section at the end of this chapter.
Future
is a state machine, every await
point is a yield
point. We could borrow data across await
points and we meet the exact same challenges as we do when borrowing across yield
points.
The Future
implementation
Futures has a well defined interface, which means they can be used across the entire ecosystem.
We can chain these
Future
s so that once a leaf-future is ready we'll perform a set of operations until either the task is finished or we reach yet another leaf-future which we'll wait for and yield control to the scheduler.关于里面的mywake部分要注意一个点:
调用
unpark
会将线程的内部状态设置为 "不再阻塞",这意味着线程已经准备好再次运行。然而,这并不保证线程会立即执行。线程何时开始执行取决于操作系统的调度器。如果线程在调用
unpark
时已经在运行,或者在调用 unpark
后立即调用 park
,那么 unpark
的调用可能不会有任何效果。这是因为 unpark
只是将线程的状态设置为 "不再阻塞",而不是强制线程立即运行。因此,虽然
unpark
可以使线程准备好再次运行,但是它并不能保证线程会立即执行。线程何时开始执行仍然取决于操作系统的调度器。The Reactor (important)
Since concurrency mostly makes sense when interacting with the outside world (or at least some peripheral), we need something to actually abstract over this interaction in an asynchronous way.
This is the Reactors job. Most often you'll see reactors in Rust use a library called Mio, which provides non blocking APIs and event notification for several platforms.
The reactor will typically give you something like a
TcpStream
(or any other resource) which you'll use to create an I/O request. What you get in return is a Future
.
Our example task is a timer that only spawns a thread and puts it to sleep for the number of seconds we specify. The reactor we create here will create a leaf-future representing each timer. In return the Reactor receives a waker which it will call once the task is finished.读代码的时候,有个channel的语法忘记了,补充:在 Rust 中,channel
函数用于创建一个新的异步通道。这个函数返回两个值:一个发送端(Sender
)和一个接收端(Receiver
)。你可以在一个线程中使用Sender
发送值,然后在另一个线程中使用Receiver
接收这些值。这是一个使用channel
的简单示例:在这个示例中,我们首先创建了一个新的通道,然后在一个新的线程中发送了一个字符串,最后在主线程中接收了这个字符串。
Async/Await and concurrency
main部分代码是:
The
async
keyword can be used on functions as in async fn(...)
or on a block as in async { ... }
. Both will turn your function, or block, into a Future
.These Futures are rather simple. Imagine our generator from a few chapters back. Every
await
point is like a yield
point.Instead of
yielding
a value we pass in, we yield the result of calling poll
on the next Future
we're awaiting.Our
mainfut
contains two non-leaf futures which it will call poll
on. Non-leaf-futures has a poll
method that simply polls their inner futures and these state machines are polled until some "leaf future" in the end either returns Ready
or Pending
.
The way our example is right now, it's not much better than regular synchronous code. For us to actually await multiple futures at the same time we somehow need to
spawn
them so the executor starts running them concurrently.Our example as it stands now returns this:
If these Futures were executed asynchronously we would expect to see:
Note that this doesn't mean they need to run in parallel. They can run in parallel but there is no requirement. Remember that we're waiting for some external resource so we can fire off many such calls on a single thread and handle each event as it resolves.
- 作者:liamY
- 链接:https://liamy.clovy.top/article/OScamp_prj6_pre01
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。