├── .DS_Store ├── .gitignore ├── README.md ├── book.toml └── src ├── .DS_Store ├── SUMMARY.md ├── async.md ├── bridging-with-sync.md ├── channels.md ├── frame.md ├── getting-startted.md ├── graceful-shutdown.md ├── io.md ├── overview.md ├── select.md ├── shared-state.md ├── spawning.md └── stream.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunface/tokio-course/50114f167ba128c0101ed01a185d4710081385cd/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tokio异步编程 2 | 该书是 [<>](https://github.com/sunface/rust-course) 中 "Tokio 使用指南专题" 的镜像内容,高质量手翻并扩展了 tokio 官网的教程, 深入讲述了如何编写 Rust 高并发异步程序 3 | 4 | > 本书由 [RustTT 翻译小组](https://rusttt.com) 进行翻译,并对内容进行了一些调整,更便于国内读者阅读 5 | > 6 | > 英文原文 [Tokio Tutorial](https://tokio.rs/tokio/tutorial) 7 | 8 | ## 目录 9 | - [tokio概览](src/overview.md) 10 | - [使用初印象](src/getting-startted.md) 11 | - [创建异步任务](src/spawning.md) 12 | - [共享状态](src/shared-state.md) 13 | - [消息传递](src/channels.md) 14 | - [I/O](src/io.md) 15 | - [解析数据帧](src/frame.md) 16 | - [深入async](src/async.md) 17 | - [select](src/select.md) 18 | - [类似迭代器的Stream](src/stream.md)) 19 | - [优雅的关闭](src/graceful-shutdown.md) 20 | - [异步跟同步共存](src/bridging-with-sync.md) 21 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Sunface"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunface/tokio-course/50114f167ba128c0101ed01a185d4710081385cd/src/.DS_Store -------------------------------------------------------------------------------- /src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | - [tokio概览](overview.md) 3 | - [使用初印象](getting-startted.md) 4 | - [创建异步任务](spawning.md) 5 | - [共享状态](shared-state.md) 6 | - [消息传递](channels.md) 7 | - [I/O](io.md) 8 | - [解析数据帧](frame.md) 9 | - [深入async](async.md) 10 | - [select](select.md) 11 | - [类似迭代器的Stream](stream.md)) 12 | - [优雅的关闭](graceful-shutdown.md) 13 | - [异步跟同步共存](bridging-with-sync.md) 14 | -------------------------------------------------------------------------------- /src/async.md: -------------------------------------------------------------------------------- 1 | # 深入 Tokio 背后的异步原理 2 | 在经过多个章节的深入学习后,Tokio 对我们来说不再是一座隐于云雾中的高山,它其实蛮简单好用的,甚至还有一丝丝的可爱!? 3 | 4 | 但从现在开始,如果想要进一步的深入 Tokio ,首先需要深入理解 `async` 的原理,其实我们在[之前的章节](https://course.rs/async/intro.html)已经深入学习过,这里结合 Tokio 再来回顾下。 5 | 6 | ## Future 7 | 先来回顾一下 `async fn` 异步函数 : 8 | ```rust 9 | use tokio::net::TcpStream; 10 | 11 | async fn my_async_fn() { 12 | println!("hello from async"); 13 | // 通过 .await 创建 socket 连接 14 | let _socket = TcpStream::connect("127.0.0.1:3000").await.unwrap(); 15 | println!("async TCP operation complete"); 16 | // 关闭socket 17 | } 18 | ``` 19 | 20 | 接着对它进行调用获取一个返回值,再在返回值上调用 `.await`: 21 | ```rust 22 | #[tokio::main] 23 | async fn main() { 24 | let what_is_this = my_async_fn(); 25 | // 上面的调用不会产生任何效果 26 | 27 | // ... 执行一些其它代码 28 | 29 | 30 | what_is_this.await; 31 | // 直到 .await 后,文本才被打印,socket 连接也被创建和关闭 32 | } 33 | ``` 34 | 35 | 在上面代码中 `my_async_fn` 函数为何可以惰性执行( 直到 .await 调用时才执行)?秘密就在于 `async fn` 声明的函数返回一个 `Future`。 36 | 37 | `Future` 是一个实现了 [`std::future::Future`](https://doc.rust-lang.org/std/future/trait.Future.html) 特征的值,该值包含了一系列异步计算过程,而这个过程直到 `.await` 调用时才会被执行。 38 | 39 | `std::future::Future` 的定义如下所示: 40 | ```rust 41 | use std::pin::Pin; 42 | use std::task::{Context, Poll}; 43 | 44 | pub trait Future { 45 | type Output; 46 | 47 | fn poll(self: Pin<&mut Self>, cx: &mut Context) 48 | -> Poll; 49 | } 50 | ``` 51 | 52 | 代码中有几个关键点: 53 | 54 | - [关联类型](https://course.rs/basic/trait/advance-trait.html#关联类型) `Output` 是 `Future` 执行完成后返回的值的类型 55 | - `Pin` 类型是在异步函数中进行借用的关键,在[这里]((https://course.rs/async/pin-unpin.html))有非常详细的介绍 56 | 57 | 和其它语言不同,Rust中的 `Future` 不代表一个发生在后台的计算,而是 `Future` 就代表了计算本身,因此 58 | `Future` 的所有者有责任去推进该计算过程的执行,例如通过 `Future::poll` 函数。听上去好像还挺复杂?但是大家不必担心,因为这些都在 Tokio 中帮你自动完成了 :) 59 | 60 | #### 实现 Future 61 | 下面来一起实现个五脏俱全的 `Future`,它将:1. 等待某个特定时间点的到来 2. 在标准输出打印文本 3. 生成一个字符串 62 | 63 | ```rust 64 | use std::future::Future; 65 | use std::pin::Pin; 66 | use std::task::{Context, Poll}; 67 | use std::time::{Duration, Instant}; 68 | 69 | struct Delay { 70 | when: Instant, 71 | } 72 | 73 | // 为我们的 Delay 类型实现 Future 特征 74 | impl Future for Delay { 75 | type Output = &'static str; 76 | 77 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) 78 | -> Poll<&'static str> 79 | { 80 | if Instant::now() >= self.when { 81 | // 时间到了,Future 可以结束 82 | println!("Hello world"); 83 | // Future 执行结束并返回 "done" 字符串 84 | Poll::Ready("done") 85 | } else { 86 | // 目前先忽略下面这行代码 87 | cx.waker().wake_by_ref(); 88 | Poll::Pending 89 | } 90 | } 91 | } 92 | 93 | #[tokio::main] 94 | async fn main() { 95 | let when = Instant::now() + Duration::from_millis(10); 96 | let future = Delay { when }; 97 | 98 | // 运行并等待 Future 的完成 99 | let out = future.await; 100 | 101 | // 判断 Future 返回的字符串是否是 "done" 102 | assert_eq!(out, "done"); 103 | } 104 | ``` 105 | 106 | 以上代码很清晰的解释了如何自定义一个 `Future`,并指定它如何通过 `poll` 一步一步执行,直到最终完成返回 "done" 字符串。 107 | 108 | #### async fn 作为 Future 109 | 大家有没有注意到,上面代码我们在 `main` 函数中初始化一个 `Future` 并使用 `.await` 对其进行调用执行,如果你是在 `fn main` 中这么做,是会报错的。 110 | 111 | 原因是 `.await` 只能用于 `async fn` 函数中,因此我们将 `main` 函数声明成 `async fn main` 同时使用 `#[tokio::main]` 进行了标注,此时 `async fn main` 生成的代码类似下面: 112 | ```rust 113 | use std::future::Future; 114 | use std::pin::Pin; 115 | use std::task::{Context, Poll}; 116 | use std::time::{Duration, Instant}; 117 | 118 | enum MainFuture { 119 | // 初始化,但永远不会被 poll 120 | State0, 121 | // 等待 `Delay` 运行,例如 `future.await` 代码行 122 | State1(Delay), 123 | // Future 执行完成 124 | Terminated, 125 | } 126 | 127 | impl Future for MainFuture { 128 | type Output = (); 129 | 130 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) 131 | -> Poll<()> 132 | { 133 | use MainFuture::*; 134 | 135 | loop { 136 | match *self { 137 | State0 => { 138 | let when = Instant::now() + 139 | Duration::from_millis(10); 140 | let future = Delay { when }; 141 | *self = State1(future); 142 | } 143 | State1(ref mut my_future) => { 144 | match Pin::new(my_future).poll(cx) { 145 | Poll::Ready(out) => { 146 | assert_eq!(out, "done"); 147 | *self = Terminated; 148 | return Poll::Ready(()); 149 | } 150 | Poll::Pending => { 151 | return Poll::Pending; 152 | } 153 | } 154 | } 155 | Terminated => { 156 | panic!("future polled after completion") 157 | } 158 | } 159 | } 160 | } 161 | } 162 | ``` 163 | 164 | 可以看出,编译器会将 `Future` 变成状态机, 其中 `MainFuture` 包含了 `Future` 可能处于的状态:从 `State0` 状态开始,当 `poll` 被调用时, `Future` 会尝试去尽可能的推进内部的状态,若它可以被完成时,就会返回 `Poll::Ready`,其中还会包含最终的输出结果。 165 | 166 | 若 `Future` 无法被完成,例如它所等待的资源还没有准备好,此时就会返回 `Poll::Pending`,该返回值会通知调用者: `Future` 会在稍后才能完成。 167 | 168 | 同时可以看到:当一个 `Future` 由其它 `Future` 组成时,调用外层 `Future` 的 `poll` 函数会同时调用一次内部 `Future` 的 `poll` 函数。 169 | 170 | ## 执行器( Excecutor ) 171 | `async fn` 返回 `Future` ,而后者需要通过被不断的 `poll` 才能往前推进状态,同时该 `Future` 还能包含其它 `Future` ,那么问题来了谁来负责调用最外层 `Future` 的 `poll` 函数? 172 | 173 | 回一下之前的内容,为了运行一个异步函数,我们必须使用 `tokio::spawn` 或 通过 `#[tokio::main]` 标注的 `async fn main` 函数。它们有一个非常重要的作用:将最外层 `Future` 提交给 Tokio 的执行器。该执行器负责调用 `poll` 函数,然后推动 `Future` 的执行,最终直至完成。 174 | 175 | #### mini tokio 176 | 为了更好理解相关的内容,我们一起来实现一个迷你版本的 Tokio,完整的代码见[这里](https://github.com/tokio-rs/website/blob/master/tutorial-code/mini-tokio/src/main.rs)。 177 | 178 | 先来看一段基础代码: 179 | ```rust 180 | use std::collections::VecDeque; 181 | use std::future::Future; 182 | use std::pin::Pin; 183 | use std::task::{Context, Poll}; 184 | use std::time::{Duration, Instant}; 185 | use futures::task; 186 | 187 | fn main() { 188 | let mut mini_tokio = MiniTokio::new(); 189 | 190 | mini_tokio.spawn(async { 191 | let when = Instant::now() + Duration::from_millis(10); 192 | let future = Delay { when }; 193 | 194 | let out = future.await; 195 | assert_eq!(out, "done"); 196 | }); 197 | 198 | mini_tokio.run(); 199 | } 200 | 201 | struct MiniTokio { 202 | tasks: VecDeque, 203 | } 204 | 205 | type Task = Pin + Send>>; 206 | 207 | impl MiniTokio { 208 | fn new() -> MiniTokio { 209 | MiniTokio { 210 | tasks: VecDeque::new(), 211 | } 212 | } 213 | 214 | /// 生成一个 Future并放入 mini-tokio 实例的任务队列中 215 | fn spawn(&mut self, future: F) 216 | where 217 | F: Future + Send + 'static, 218 | { 219 | self.tasks.push_back(Box::pin(future)); 220 | } 221 | 222 | fn run(&mut self) { 223 | let waker = task::noop_waker(); 224 | let mut cx = Context::from_waker(&waker); 225 | 226 | while let Some(mut task) = self.tasks.pop_front() { 227 | if task.as_mut().poll(&mut cx).is_pending() { 228 | self.tasks.push_back(task); 229 | } 230 | } 231 | } 232 | } 233 | ``` 234 | 235 | 以上代码运行了一个 `async` 语句块 `mini_tokio.spawn(async {...})`, 还创建了一个 `Delay` 实例用于等待所需的时间。看上去相当不错,但这个实现有一个 **重大缺陷**:我们的执行器永远也不会休眠。执行器会持续的循环遍历所有的 `Future` ,然后不停的 `poll` 它们,但是事实上,大多数 `poll` 都是没有用的,因为此时 `Future` 并没有准备好,因此会继续返回 `Poll::Pending` ,最终这个循环遍历会让你的CPU疲于奔命,真打工人! 236 | 237 | 鉴于此,我们的 mini-tokio 只应该在 `Future` 准备好可以进一步运行后,才去 `poll` 它,例如该 `Future` 之前阻塞等待的**资源**已经准备好并可以被使用了,就可以对其进行 `poll`。再比如,如果一个 `Future` 任务在阻塞等待从 TCP socket 中读取数据,那我们只想在 `socket` 中有数据可以读取后才去 `poll` 它,而不是没事就 `poll` 着玩。 238 | 239 | 回到在上面的代码中,mini-tokio 只应该当任务的延迟时间到了后,才去 `poll` 它。 为了实现这个功能,我们需要 `通知 -> 运行` 机制:当任务可以进一步被推进运行时,它会主动通知执行器,然后执行器再来 `poll`。 240 | 241 | ## Waker 242 | 一切的答案都在 `Waker` 中,资源可以用它来通知正在等待的任务:该资源已经准备好,可以继续运行了。 243 | 244 | 再来看下 `Future::poll` 的定义: 245 | ```rust 246 | fn poll(self: Pin<&mut Self>, cx: &mut Context) 247 | -> Poll; 248 | ``` 249 | 250 | `Context` 参数中包含有 `waker()`方法。该方法返回一个绑定到当前任务上的 `Waker`,然后 `Waker` 上定义了一个 `wake()` 方法,用于通知执行器相关的任务可以继续执行。 251 | 252 | 准确来说,当 `Future` 阻塞等待的资源已经准备好时(例如 socket 中有了可读取的数据),该资源可以调用 `wake()` 方法,来通知执行器可以继续调用该 `Future` 的 `poll` 函数来推进任务的执行。 253 | 254 | #### 发送 wake 通知 255 | 现在,为 `Delay` 添加下 `Waker` 支持: 256 | ```rust 257 | use std::future::Future; 258 | use std::pin::Pin; 259 | use std::task::{Context, Poll}; 260 | use std::time::{Duration, Instant}; 261 | use std::thread; 262 | 263 | struct Delay { 264 | when: Instant, 265 | } 266 | 267 | impl Future for Delay { 268 | type Output = &'static str; 269 | 270 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) 271 | -> Poll<&'static str> 272 | { 273 | if Instant::now() >= self.when { 274 | println!("Hello world"); 275 | Poll::Ready("done") 276 | } else { 277 | // 为当前任务克隆一个 waker 的句柄 278 | let waker = cx.waker().clone(); 279 | let when = self.when; 280 | 281 | // 生成一个计时器线程 282 | thread::spawn(move || { 283 | let now = Instant::now(); 284 | 285 | if now < when { 286 | thread::sleep(when - now); 287 | } 288 | 289 | waker.wake(); 290 | }); 291 | 292 | Poll::Pending 293 | } 294 | } 295 | } 296 | ``` 297 | 298 | 此时,计时器用来模拟一个阻塞等待的资源,一旦计时结束(该资源已经准备好),资源会通过 `waker.wake()` 调用通知执行器我们的任务再次被调度执行了。 299 | 300 | 当然,现在的实现还较为粗糙,等会我们会来进一步优化,在此之前,先来看看如何监听这个 `wake` 通知。 301 | 302 | > 当 Future 会返回 `Poll::Pending` 时,一定要确保 `wake` 能被正常调用,否则会导致任务永远被挂起,再也不会被执行器 `poll`。 303 | > 304 | > **忘记在返回 `Poll::Pending` 时调用 `wake` 是很多难以发现 bug 的潜在源头!** 305 | 306 | 再回忆下最早实现的 `Delay` 代码: 307 | ```rust 308 | impl Future for Delay { 309 | type Output = &'static str; 310 | 311 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) 312 | -> Poll<&'static str> 313 | { 314 | if Instant::now() >= self.when { 315 | // 时间到了,Future 可以结束 316 | println!("Hello world"); 317 | // Future 执行结束并返回 "done" 字符串 318 | Poll::Ready("done") 319 | } else { 320 | // 目前先忽略下面这行代码 321 | cx.waker().wake_by_ref(); 322 | Poll::Pending 323 | } 324 | } 325 | } 326 | ``` 327 | 328 | 在返回 `Poll::Pending` 之前,先调用了 `cx.waker().wake_by_ref()` ,由于此时我们还没有模拟计时资源,因此这里直接调用了 `wake` 进行通知,这样做会导致当前的 `Future` 被立即再次调度执行。 329 | 330 | 由此可见,这种通知的控制权是在你手里的,甚至可以像上面代码这样,还没准备好资源,就直接进行 `wake` 通知,但是总归意义不大,而且浪费了 CPU,因为这种 `执行 -> 立即通知再调度 -> 执行` 的方式会造成一个非常繁忙的循环。 331 | 332 | #### 处理 wake 通知 333 | 下面,让我们更新 mint-tokio 服务,让它能接受 wake 通知:当 `waker.wake()` 被调用后,相关联的任务会被放入执行器的队列中,然后等待执行器的调用执行。 334 | 335 | 为了实现这一点,我们将使用消息通道来排队存储这些被唤醒并等待调度的任务。有一点需要注意,从消息通道接收消息的线程(执行器所在的线程)和发送消息的线程(唤醒任务时所在的线程)可能是不同的,因此消息( `Waker` )必须要实现 `Send`和 `Sync`,才能跨线程使用。 336 | 337 | > 关于 `Send` 和 `Sync` 的具体讲解见[这里](https://course.rs/advance/concurrency-with-threads/send-sync.html) 338 | 339 | 基于以上理由,我们选择使用来自于 `crossbeam` 的消息通道,因为标准库中的消息通道不是 `Sync` 的。在 `Cargo.toml` 中添加以下依赖: 340 | ```toml 341 | crossbeam = "0.8" 342 | ``` 343 | 344 | 再来更新下 `MiniTokio` 结构体: 345 | ```rust 346 | use crossbeam::channel; 347 | use std::sync::Arc; 348 | 349 | struct MiniTokio { 350 | scheduled: channel::Receiver>, 351 | sender: channel::Sender>, 352 | } 353 | 354 | struct Task { 355 | // 先空着,后面会填充代码 356 | } 357 | ``` 358 | 359 | `Waker` 实现了 `Sync` 特征,同时还可以被克隆,当 `wake` 被调用时,任务就会被调度执行。 360 | 361 | 为了实现上述的目的,我们引入了消息通道,当 `waker.wake()` 函数被调用时,任务会被发送到该消息通道中: 362 | ```rust 363 | use std::sync::{Arc, Mutex}; 364 | 365 | struct Task { 366 | // `Mutex` 是为了让 `Task` 实现 `Sync` 特征,它能保证同一时间只有一个线程可以访问 `Future`。 367 | // 事实上 `Mutex` 并没有在 Tokio 中被使用,这里我们只是为了简化: Tokio 的真实代码实在太长了 :D 368 | future: Mutex + Send>>>, 369 | executor: channel::Sender>, 370 | } 371 | 372 | impl Task { 373 | fn schedule(self: &Arc) { 374 | self.executor.send(self.clone()); 375 | } 376 | } 377 | ``` 378 | 379 | 接下来,我们需要让 `std::task::Waker` 能准确的找到所需的调度函数 关联起来,对此标准库中提供了一个底层的 API [`std::task::RawWakerVTable`](https://doc.rust-lang.org/std/task/struct.RawWakerVTable.html) 可以用于手动的访问 `vtable`,这种实现提供了最大的灵活性,但是需要大量 `unsafe` 的代码。 380 | 381 | 因此我们选择更加高级的实现:由 `futures` 包提供的 [`ArcWake`](https://docs.rs/futures/0.3.19/futures/task/trait.ArcWake.html) 特征,只要简单实现该特征,就可以将我们的 `Task` 转变成一个 `waker`。在 `Cargo.toml` 中添加以下包: 382 | ```toml 383 | futures = "0.3" 384 | ``` 385 | 386 | 然后为我们的任务 `Task` 实现 `ArcWake`: 387 | ```rust 388 | use futures::task::{self, ArcWake}; 389 | use std::sync::Arc; 390 | impl ArcWake for Task { 391 | fn wake_by_ref(arc_self: &Arc) { 392 | arc_self.schedule(); 393 | } 394 | } 395 | ``` 396 | 397 | 当之前的计时器线程调用 `waker.wake()` 时,所在的任务会被推入到消息通道中。因此接下来,我们需要实现接受端的功能,然后 `MiniTokio::run()` 函数中执行该任务: 398 | ```rust 399 | impl MiniTokio { 400 | // 从消息通道中接收任务,然后通过 poll 来执行 401 | fn run(&self) { 402 | while let Ok(task) = self.scheduled.recv() { 403 | task.poll(); 404 | } 405 | } 406 | 407 | /// 初始化一个新的 mini-tokio 实例 408 | fn new() -> MiniTokio { 409 | let (sender, scheduled) = channel::unbounded(); 410 | 411 | MiniTokio { scheduled, sender } 412 | } 413 | 414 | 415 | /// 在下面函数中,通过参数传入的 future 被 `Task` 包裹起来,然后会被推入到调度队列中,当 `run` 被调用时,该 future 将被执行 416 | fn spawn(&self, future: F) 417 | where 418 | F: Future + Send + 'static, 419 | { 420 | Task::spawn(future, &self.sender); 421 | } 422 | } 423 | 424 | impl Task { 425 | fn poll(self: Arc) { 426 | // 基于 Task 实例创建一个 waker, 它使用了之前的 `ArcWake` 427 | let waker = task::waker(self.clone()); 428 | let mut cx = Context::from_waker(&waker); 429 | 430 | // 没有其他线程在竞争锁时,我们将获取到目标 future 431 | let mut future = self.future.try_lock().unwrap(); 432 | 433 | // 对 future 进行 poll 434 | let _ = future.as_mut().poll(&mut cx); 435 | } 436 | 437 | // 使用给定的 future 来生成新的任务 438 | // 439 | // 新的任务会被推到 `sender` 中,接着该消息通道的接收端就可以获取该任务,然后执行 440 | fn spawn(future: F, sender: &channel::Sender>) 441 | where 442 | F: Future + Send + 'static, 443 | { 444 | let task = Arc::new(Task { 445 | future: Mutex::new(Box::pin(future)), 446 | executor: sender.clone(), 447 | }); 448 | 449 | let _ = sender.send(task); 450 | } 451 | 452 | } 453 | ``` 454 | 455 | 首先,我们实现了 `MiniTokio::run()` 函数,它会持续从消息通道中接收被唤醒的任务,然后通过 `poll` 来推动其继续执行。 456 | 457 | 其次,`MiniTokio::new()` 和 `MiniTokio::spawn()` 使用了消息通道而不是一个 `VecDeque` 。当新任务生成后,这些任务中会携带上消息通道的发送端,当任务中的资源准备就绪时,会使用该发送端将该任务放入消息通道的队列中,等待执行器 `poll`。 458 | 459 | `Task::poll()` 函数使用 `futures` 包提供的 `ArcWake` 创建了一个 `waker`,后者可以用来创建 `task::Context`,最终该 `Context` 会被传给执行器调用的 `poll` 函数。 460 | 461 | > 注意,Task::poll 和执行器调用的 poll 是完全不同的,大家别搞混了 462 | 463 | 464 | ## 一些遗留问题 465 | 至此,我们的程序已经差不多完成,还剩几个遗留问题需要解决下。 466 | 467 | #### 在异步函数中生成异步任务 468 | 之前实现 `Delay Future` 时,我们提到有几个问题需要解决。Rust 的异步模型允许一个 Future 在执行过程中可以跨任务迁移: 469 | ```rust 470 | use futures::future::poll_fn; 471 | use std::future::Future; 472 | use std::pin::Pin; 473 | 474 | #[tokio::main] 475 | async fn main() { 476 | let when = Instant::now() + Duration::from_millis(10); 477 | let mut delay = Some(Delay { when }); 478 | 479 | poll_fn(move |cx| { 480 | let mut delay = delay.take().unwrap(); 481 | let res = Pin::new(&mut delay).poll(cx); 482 | assert!(res.is_pending()); 483 | tokio::spawn(async move { 484 | delay.await; 485 | }); 486 | 487 | Poll::Ready(()) 488 | }).await; 489 | } 490 | ``` 491 | 492 | 493 | 首先,`poll_fn` 函数使用闭包创建了一个 `Future`,其次,上面代码还创建一个 `Delay` 实例,然后在闭包中,对其进行了一次 `poll` ,接着再将该 `Delay` 实例发送到一个新的任务,在此任务中使用 `.await` 进行了执行。 494 | 495 | 在例子中,`Delay:poll` 被调用了不止一次,且使用了不同的 `Waker` 实例,在这种场景下,你必须确保调用最近一次 `poll` 函数中的 `Waker` 参数中的`wake`方法。也就是调用最内层 `poll` 函数参数( `Waker` )上的 `wake` 方法。 496 | 497 | 当实现一个 `Future` 时,很关键的一点就是要假设每次 `poll` 调用都会应用到一个不同的 `Waker` 实例上。因此 `poll` 函数必须要使用一个新的 `waker` 去更新替代之前的 `waker`。 498 | 499 | 我们之前的 `Delay` 实现中,会在每一次 `poll` 调用时都生成一个新的线程。这么做问题不大,但是当 `poll` 调用较多时会出现明显的性能问题!一个解决方法就是记录你是否已经生成了一个线程,然后只有在没有生成时才去创建一个新的线程。但是一旦这么做,就必须确保线程的 `Waker` 在后续 `poll` 调用中被正确更新,否则你无法唤醒最近的 `Waker` ! 500 | 501 | 502 | 这一段大家可能会看得云里雾里的,没办法,原文就饶来绕去,好在终于可以看代码了。。我们可以通过代码来解决疑惑: 503 | ```rust 504 | use std::future::Future; 505 | use std::pin::Pin; 506 | use std::sync::{Arc, Mutex}; 507 | use std::task::{Context, Poll, Waker}; 508 | use std::thread; 509 | use std::time::{Duration, Instant}; 510 | 511 | struct Delay { 512 | when: Instant, 513 | // 用于说明是否已经生成一个线程 514 | // Some 代表已经生成, None 代表还没有 515 | waker: Option>>, 516 | } 517 | 518 | impl Future for Delay { 519 | type Output = (); 520 | 521 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { 522 | // 若这是 Future 第一次被调用,那么需要先生成一个计时器线程。 523 | // 若不是第一次调用(该线程已在运行),那要确保已存储的 `Waker` 跟当前任务的 `waker` 匹配 524 | if let Some(waker) = &self.waker { 525 | let mut waker = waker.lock().unwrap(); 526 | 527 | // 检查之前存储的 `waker` 是否跟当前任务的 `waker` 相匹配. 528 | // 这是必要的,原因是 `Delay Future` 的实例可能会在两次 `poll` 之间被转移到另一个任务中,然后 529 | // 存储的 waker 被该任务进行了更新。 530 | // 这种情况一旦发生,`Context` 包含的 `waker` 将不同于存储的 `waker`。 531 | // 因此我们必须对存储的 `waker` 进行更新 532 | if !waker.will_wake(cx.waker()) { 533 | *waker = cx.waker().clone(); 534 | } 535 | } else { 536 | let when = self.when; 537 | let waker = Arc::new(Mutex::new(cx.waker().clone())); 538 | self.waker = Some(waker.clone()); 539 | 540 | // 第一次调用 `poll`,生成计时器线程 541 | thread::spawn(move || { 542 | let now = Instant::now(); 543 | 544 | if now < when { 545 | thread::sleep(when - now); 546 | } 547 | 548 | // 计时结束,通过调用 `waker` 来通知执行器 549 | let waker = waker.lock().unwrap(); 550 | waker.wake_by_ref(); 551 | }); 552 | } 553 | 554 | // 一旦 waker 被存储且计时器线程已经开始,我们就需要检查 `delay` 是否已经完成 555 | // 若计时已完成,则当前 Future 就可以完成并返回 `Poll::Ready` 556 | if Instant::now() >= self.when { 557 | Poll::Ready(()) 558 | } else { 559 | // 计时尚未结束,Future 还未完成,因此返回 `Poll::Pending`. 560 | // 561 | // `Future` 特征要求当 `Pending` 被返回时,那我们要确保当资源准备好时,必须调用 `waker` 以通/// 知执行器。 在我们的例子中,会通过生成的计时线程来保证 562 | // 563 | // 如果忘记调用 waker, 那等待我们的将是深渊:该任务将被永远的挂起,无法再执行 564 | Poll::Pending 565 | } 566 | } 567 | } 568 | ``` 569 | 570 | 这着实有些复杂(原文。。),但是简单来看就是:在每次 `poll` 调用时,都会检查 `Context` 中提供的 `waker` 和我们之前记录的 `waker` 是否匹配。若匹配,就什么都不用做,若不匹配,那之前存储的就必须进行更新。 571 | 572 | #### Notify 573 | 我们之前证明了如何用手动编写的 `waker` 来实现 `Delay Future`。 `Waker` 是Rust异步编程的基石,因此绝大多数时候,我们并不需要直接去使用它。例如,在 `Delay` 的例子中, 可以使用 [`tokio::sync::Notify`](https://docs.rs/tokio/1.16.0/tokio/sync/struct.Notify.html) 去实现。 574 | 575 | 该 `Notify` 提供了一个基础的任务通知机制,它会处理这些 `waker` 的细节,包括确保两次 `waker` 的匹配: 576 | ```rust 577 | use tokio::sync::Notify; 578 | use std::sync::Arc; 579 | use std::time::{Duration, Instant}; 580 | use std::thread; 581 | 582 | async fn delay(dur: Duration) { 583 | let when = Instant::now() + dur; 584 | let notify = Arc::new(Notify::new()); 585 | let notify2 = notify.clone(); 586 | 587 | thread::spawn(move || { 588 | let now = Instant::now(); 589 | 590 | if now < when { 591 | thread::sleep(when - now); 592 | } 593 | 594 | notify2.notify_one(); 595 | }); 596 | 597 | 598 | notify.notified().await; 599 | } 600 | ``` 601 | 602 | 当使用 `Notify` 后,我们就可以轻松的实现如上的 `delay` 函数。 603 | 604 | ## 总结 605 | 在看完这么长的文章后,我们来总结下,否则大家可能还会遗忘: 606 | 607 | - 在 Rust 中,`async` 是惰性的,直到执行器 `poll` 它们时,才会开始执行 608 | - `Waker` 是 `Future` 被执行的关键,它可以链接起 `Future` 任务和执行器 609 | - 当资源没有准备时,会返回一个 `Poll::Pending` 610 | - 当资源准备好时,会通过 `waker.wake` 发出通知 611 | - 执行器会收到通知,然后调度该任务继续执行,此时由于资源已经准备好,因此任务可以顺利往前推进了 612 | -------------------------------------------------------------------------------- /src/bridging-with-sync.md: -------------------------------------------------------------------------------- 1 | # 异步跟同步共存 2 | 一些异步程序例如 tokio指南 章节中的绝大多数例子,它们整个程序都是异步的,包括程序入口 `main` 函数: 3 | ```rust 4 | #[tokio::main] 5 | async fn main() { 6 | println!("Hello world"); 7 | } 8 | ``` 9 | 10 | 在一些场景中,你可能只想在异步程序中运行一小部分同步代码,这种需求可以考虑下 [`spawn_blocking`](https://docs.rs/tokio/1.16.1/tokio/task/fn.spawn_blocking.html)。 11 | 12 | 但是在很多场景中,我们只想让程序的某一个部分成为异步的,也许是因为同步代码更好实现,又或许是同步代码可读性、兼容性都更好。例如一个 `GUI` 应用可能想要让 `UI` 相关的代码在主线程中,然后通过另一个线程使用 `tokio` 的运行时来处理一些异步任务。 13 | 14 | 因此本章节的目标很纯粹:如何在同步代码中使用一小部分异步代码。 15 | 16 | ## `#[tokio::main]` 的展开 17 | 在 Rust 中, `main` 函数不能是异步的,有同学肯定不愿意了,我们在之前章节..不对,就在开头,你还用到了 `async fn main` 的声明方式,怎么就不能异步了呢? 18 | 19 | 其实,`#[tokio::main]` 该宏仅仅是提供语法糖,目的是让大家可以更简单、更一致的去写异步代码,它会将你写下的`async fn main` 函数替换为: 20 | ```rust 21 | fn main() { 22 | tokio::runtime::Builder::new_multi_thread() 23 | .enable_all() 24 | .build() 25 | .unwrap() 26 | .block_on(async { 27 | println!("Hello world"); 28 | }) 29 | } 30 | ``` 31 | 32 | 注意到上面的 `block_on` 方法了嘛?在我们自己的同步代码中,可以使用它开启一个 `async/await` 世界。 33 | 34 | ## mini-redis的同步接口 35 | 在下面,我们将一起构建一个同步的 `mini-redis` ,为了实现这一点,需要将 `Runtime` 对象存储起来,然后利用上面提到的 `block_on` 方法。 36 | 37 | 38 | 首先,创建一个文件 `src/blocking_client.rs`,然后使用下面代码将异步的 `Clien` 结构体包裹起来: 39 | ```rust 40 | use tokio::net::ToSocketAddrs; 41 | use tokio::runtime::Runtime; 42 | 43 | pub use crate::client::Message; 44 | 45 | /// 建立到 redis 服务端的连接 46 | pub struct BlockingClient { 47 | /// 之前实现的异步客户端 `Client` 48 | inner: crate::client::Client, 49 | 50 | /// 一个 `current_thread` 模式的 `tokio` 运行时, 51 | /// 使用阻塞的方式来执行异步客户端 `Client` 上的操作 52 | rt: Runtime, 53 | } 54 | 55 | pub fn connect(addr: T) -> crate::Result { 56 | // 构建一个 tokio 运行时: Runtime 57 | let rt = tokio::runtime::Builder::new_current_thread() 58 | .enable_all() 59 | .build()?; 60 | 61 | // 使用运行时来调用异步的连接方法 62 | let inner = rt.block_on(crate::client::connect(addr))?; 63 | 64 | Ok(BlockingClient { inner, rt }) 65 | } 66 | ``` 67 | 68 | 在这里,我们使用了一个构造器函数用于在同步代码中执行异步的方法:使用 `Runtime` 上的 `block_on` 方法来执行一个异步方法并返回结果。 69 | 70 | 有一个很重要的点,就是我们还使用了 [`current_thread`](https://docs.rs/tokio/1.16.1/tokio/runtime/struct.Builder.html#method.new_current_thread) 模式的运行时。这个可不常见,原因是异步程序往往要利用多线程的威力来实现更高的吞吐性能,相对应的模式就是 [`multi_thread`](https://docs.rs/tokio/1.16.1/tokio/runtime/struct.Builder.html#method.new_multi_thread),该模式会生成多个运行在后台的线程,它们可以高效的实现多个任务的同时并行处理。 71 | 72 | 但是对于我们的使用场景来说,在同一时间点只需要做一件事,无需并行处理,多个线程并不能帮助到任何事情,因此 `current_thread` 此时成为了最佳的选择。 73 | 74 | 在构建 `Runtime` 的过程中还有一个 [`enable_all`](https://docs.rs/tokio/1.16.1/tokio/runtime/struct.Builder.html#method.enable_all) 方法调用,它可以开启 `Tokio` 运行时提供的 IO 和定时器服务。 75 | 76 | 77 | > 由于 `current_thread` 运行时并不生成新的线程,只是运行在已有的主线程上,因此只有当 `block_on` 被调用后,该运行时才能执行相应的操作。一旦 `block_on` 返回,那运行时上所有生成的任务将再次冻结,直到 `block_on` 的再次调用。 78 | > 79 | > 如果这种模式不符合使用场景的需求,那大家还是需要用 `multi_thread` 运行时来代替。事实上,在 tokio 之前的章节中,我们默认使用的就是 `multi_thread` 模式。 80 | 81 | ```rust 82 | use bytes::Bytes; 83 | use std::time::Duration; 84 | 85 | impl BlockingClient { 86 | pub fn get(&mut self, key: &str) -> crate::Result> { 87 | self.rt.block_on(self.inner.get(key)) 88 | } 89 | 90 | pub fn set(&mut self, key: &str, value: Bytes) -> crate::Result<()> { 91 | self.rt.block_on(self.inner.set(key, value)) 92 | } 93 | 94 | pub fn set_expires( 95 | &mut self, 96 | key: &str, 97 | value: Bytes, 98 | expiration: Duration, 99 | ) -> crate::Result<()> { 100 | self.rt.block_on(self.inner.set_expires(key, value, expiration)) 101 | } 102 | 103 | pub fn publish(&mut self, channel: &str, message: Bytes) -> crate::Result { 104 | self.rt.block_on(self.inner.publish(channel, message)) 105 | } 106 | } 107 | ``` 108 | 109 | 这代码看上去挺长,实际上很简单,通过 `block_on` 将异步形式的 `Client` 的法变成同步调用的形式。例如 `BlockingClient` 的 `get` 方法实际上是对内部的异步 `get` 方法的同步调用。 110 | 111 | 与上面的平平无奇相比,下面的代码将更有趣,因为它将 `Client` 转变成一个 `Subscriber` 对象: 112 | ```rust 113 | /// 下面的客户端可以进入 pub/sub (发布/订阅) 模式 114 | /// 115 | /// 一旦客户端订阅了某个消息通道,那就只能执行 pub/sub 相关的命令。 116 | /// 将`BlockingClient` 类型转换成 `BlockingSubscriber` 是为了防止非 `pub/sub` 方法被调用 117 | pub struct BlockingSubscriber { 118 | /// 异步版本的 `Subscriber` 119 | inner: crate::client::Subscriber, 120 | 121 | /// 一个 `current_thread` 模式的 `tokio` 运行时, 122 | /// 使用阻塞的方式来执行异步客户端 `Client` 上的操作 123 | rt: Runtime, 124 | } 125 | 126 | impl BlockingClient { 127 | pub fn subscribe(self, channels: Vec) -> crate::Result { 128 | let subscriber = self.rt.block_on(self.inner.subscribe(channels))?; 129 | Ok(BlockingSubscriber { 130 | inner: subscriber, 131 | rt: self.rt, 132 | }) 133 | } 134 | } 135 | 136 | impl BlockingSubscriber { 137 | pub fn get_subscribed(&self) -> &[String] { 138 | self.inner.get_subscribed() 139 | } 140 | 141 | pub fn next_message(&mut self) -> crate::Result> { 142 | self.rt.block_on(self.inner.next_message()) 143 | } 144 | 145 | pub fn subscribe(&mut self, channels: &[String]) -> crate::Result<()> { 146 | self.rt.block_on(self.inner.subscribe(channels)) 147 | } 148 | 149 | pub fn unsubscribe(&mut self, channels: &[String]) -> crate::Result<()> { 150 | self.rt.block_on(self.inner.unsubscribe(channels)) 151 | } 152 | } 153 | ``` 154 | 155 | 由上可知,`subscribe` 方法会使用运行时将一个异步的 `Client` 转变成一个异步的 `Subscriber`,此外,`Subscriber` 结构体有一个非异步的方法 `get_subscribed`,对于这种方法,只需直接调用即可,而无需使用运行时。 156 | 157 | ## 其它方法 158 | 上面介绍的是最简单的方法,但是,如果只有这一种, tokio 也不会成为今天这个大名鼎鼎的自己。 159 | 160 | #### runtime.spawn 161 | 可以通过 `Runtime` 的 `spawn` 方法来创建一个基于该运行时的后台任务: 162 | ```rust 163 | use tokio::runtime::Builder; 164 | use tokio::time::{sleep, Duration}; 165 | 166 | fn main() { 167 | let runtime = Builder::new_multi_thread() 168 | .worker_threads(1) 169 | .enable_all() 170 | .build() 171 | .unwrap(); 172 | 173 | let mut handles = Vec::with_capacity(10); 174 | for i in 0..10 { 175 | handles.push(runtime.spawn(my_bg_task(i))); 176 | } 177 | 178 | // 在后台任务运行的同时做一些耗费时间的事情 179 | std::thread::sleep(Duration::from_millis(750)); 180 | println!("Finished time-consuming task."); 181 | 182 | // 等待这些后台任务的完成 183 | for handle in handles { 184 | // `spawn` 方法返回一个 `JoinHandle`,它是一个 `Future`,因此可以通过 `block_on` 来等待它完成 185 | runtime.block_on(handle).unwrap(); 186 | } 187 | } 188 | 189 | async fn my_bg_task(i: u64) { 190 | let millis = 1000 - 50 * i; 191 | println!("Task {} sleeping for {} ms.", i, millis); 192 | 193 | sleep(Duration::from_millis(millis)).await; 194 | 195 | println!("Task {} stopping.", i); 196 | } 197 | ``` 198 | 199 | 运行该程序,输出如下: 200 | ```console 201 | Task 0 sleeping for 1000 ms. 202 | Task 1 sleeping for 950 ms. 203 | Task 2 sleeping for 900 ms. 204 | Task 3 sleeping for 850 ms. 205 | Task 4 sleeping for 800 ms. 206 | Task 5 sleeping for 750 ms. 207 | Task 6 sleeping for 700 ms. 208 | Task 7 sleeping for 650 ms. 209 | Task 8 sleeping for 600 ms. 210 | Task 9 sleeping for 550 ms. 211 | Task 9 stopping. 212 | Task 8 stopping. 213 | Task 7 stopping. 214 | Task 6 stopping. 215 | Finished time-consuming task. 216 | Task 5 stopping. 217 | Task 4 stopping. 218 | Task 3 stopping. 219 | Task 2 stopping. 220 | Task 1 stopping. 221 | Task 0 stopping. 222 | ``` 223 | 224 | 在此例中,我们生成了10个后台任务在运行时中运行,然后等待它们的完成。作为一个例子,想象一下在图形渲染应用( GUI )中,有时候需要通过网络访问远程服务来获取一些数据,那上面的这种模式就非常适合,因为这些网络访问比较耗时,而且不会影响图形的主体渲染,因此可以在主线程中渲染图形,然后使用其它线程来运行 Tokio 的运行时,并通过该运行时使用异步的方式完成网络访问,最后将这些网络访问的结果发送到 GUI 进行数据渲染,例如一个进度条。 225 | 226 | 还有一点很重要,在本例子中只能使用 `multi_thread` 运行时。如果我们使用了 `current_thread`,你会发现主线程的耗时任务会在后台任务开始之前就完成了。因为在 `multi_thread` 模式下,生成的任务只会在 `block_on` 期间才执行。 227 | 228 | 在 `multi_thread` 模式下,我们并不需要通过 `block_on` 来触发任务的运行,这里是仅仅是用来阻塞并等待最终的结果。而除了通过 `block_on` 等待结果外,你还可以: 229 | 230 | - 使用消息传递的方式,例如 `tokio::sync::mpsc`,让异步任务将结果发送到主线程,然后主线程通过 `.recv`方法等待这些结果 231 | - 通过共享变量的方式,例如 `Mutex`,这种方式非常适合实现 GUI 的进度条: GUI 在每个渲染帧读取该变量即可。 232 | 233 | #### 发送消息 234 | 在同步代码中使用异步的另一个方法就是生成一个运行时,然后使用消息传递的方式跟它进行交互。这个方法虽然更啰嗦一些,但是相对于之前的两种方法更加灵活: 235 | ```rust 236 | use tokio::runtime::Builder; 237 | use tokio::sync::mpsc; 238 | 239 | pub struct Task { 240 | name: String, 241 | // 一些信息用于描述该任务 242 | } 243 | 244 | async fn handle_task(task: Task) { 245 | println!("Got task {}", task.name); 246 | } 247 | 248 | #[derive(Clone)] 249 | pub struct TaskSpawner { 250 | spawn: mpsc::Sender, 251 | } 252 | 253 | impl TaskSpawner { 254 | pub fn new() -> TaskSpawner { 255 | // 创建一个消息通道用于通信 256 | let (send, mut recv) = mpsc::channel(16); 257 | 258 | let rt = Builder::new_current_thread() 259 | .enable_all() 260 | .build() 261 | .unwrap(); 262 | 263 | std::thread::spawn(move || { 264 | rt.block_on(async move { 265 | while let Some(task) = recv.recv().await { 266 | tokio::spawn(handle_task(task)); 267 | } 268 | 269 | // 一旦所有的发送端超出作用域被 drop 后,`.recv()` 方法会返回 None,同时 while 循环会退出,然后线程结束 270 | }); 271 | }); 272 | 273 | TaskSpawner { 274 | spawn: send, 275 | } 276 | } 277 | 278 | pub fn spawn_task(&self, task: Task) { 279 | match self.spawn.blocking_send(task) { 280 | Ok(()) => {}, 281 | Err(_) => panic!("The shared runtime has shut down."), 282 | } 283 | } 284 | } 285 | ``` 286 | 287 | 为何说这种方法比较灵活呢?以上面代码为例,它可以在很多方面进行配置。例如,可以使用信号量 [`Semaphore`](https://docs.rs/tokio/1.16.1/tokio/sync/struct.Semaphore.html)来限制当前正在进行的任务数,或者你还可以使用一个消息通道将消息反向发送回任务生成器 `spawner`。 288 | 289 | 抛开细节,抽象来看,这是不是很像一个 Actor ? 290 | -------------------------------------------------------------------------------- /src/channels.md: -------------------------------------------------------------------------------- 1 | # 消息传递 2 | 迄今为止,你已经学了不少关于 Tokio 的并发编程的内容,是时候见识下真正的挑战了,接下来,我们一起来实现下客户端这块儿的功能。 3 | 4 | 首先,将之前实现的 `src/main.rs `文件中的[服务器端代码](https://github.com/tokio-rs/website/blob/master/tutorial-code/shared-state/src/main.rs)放入到一个 bin 文件中,等下可以直接通过该文件来运行我们的服务器: 5 | ```console 6 | mkdir src/bin 7 | mv src/main.rs src/bin/server.rs 8 | ``` 9 | 10 | 接着创建一个新的 bin 文件,用于包含我们即将实现的客户端代码: 11 | ```console 12 | touch src/bin/client.rs 13 | ``` 14 | 15 | 由于不再使用 `main.rs` 作为程序入口,我们需要使用以下命令来运行指定的 bin 文件: 16 | ```rust 17 | cargo run --bin server 18 | ``` 19 | 20 | 此时,服务器已经成功运行起来。 同样的,可以用 `cargo run --bin client` 这种方式运行即将实现的客户端。 21 | 22 | 万事俱备,只欠代码,一起来看看客户端该如何实现。 23 | 24 | ## 错误的实现 25 | 如果想要同时运行两个 redis 命令,我们可能会为每一个命令生成一个任务,例如: 26 | ```rust 27 | use mini_redis::client; 28 | 29 | #[tokio::main] 30 | async fn main() { 31 | // 创建到服务器的连接 32 | let mut client = client::connect("127.0.0.1:6379").await.unwrap(); 33 | 34 | // 生成两个任务,一个用于获取 key, 一个用于设置 key 35 | let t1 = tokio::spawn(async { 36 | let res = client.get("hello").await; 37 | }); 38 | 39 | let t2 = tokio::spawn(async { 40 | client.set("foo", "bar".into()).await; 41 | }); 42 | 43 | t1.await.unwrap(); 44 | t2.await.unwrap(); 45 | } 46 | ``` 47 | 48 | 这段代码不会编译,因为两个任务都需要去访问 `client`,但是 `client` 并没有实现 `Copy` 特征,再加上我们并没有实现相应的共享代码,因此自然会报错。还有一个问题,方法 `set` 和 `get` 都使用了 `client` 的可变引用 `&mut self`,由此还会造成同时借用两个可变引用的错误。 49 | 50 | 在上一节中,我们介绍了几个解决方法,但是它们大部分都不太适用于此时的情况,例如: 51 | 52 | - `std::sync::Mutex` 无法被使用,这个问题在之前章节有详解介绍过,同步锁无法跨越 `.await` 调用时使用 53 | - 那么你可能会想,是不是可以使用 `tokio::sync:Mutex` ,答案是可以用,但是同时就只能运行一个请求。若客户端实现了 redis 的 [pipelining](https://redis.io/topics/pipelining), 那这个异步锁就会导致连接利用率不足 54 | 55 | 这个不行,那个也不行,是不是没有办法解决了?还记得我们上一章节提到过几次的消息传递,但是一直没有看到它的庐山真面目吗?现在可以来看看了。 56 | 57 | ## 消息传递 58 | 之前章节我们提到可以创建一个专门的任务 `C1` (消费者 Consumer) 和通过消息传递来管理共享的资源,这里的共享资源就是 `client` 。若任务 `P1` (生产者 Producer) 想要发出 Redis 请求,首先需要发送信息给 `C1`,然后 `C1` 会发出请求给服务器,在获取到结果后,再将结果返回给 `P1`。 59 | 60 | 在这种模式下,只需要建立一条连接,然后由一个统一的任务来管理 `client` 和该连接,这样之前的 `get` 和 `set` 请求也将不存在资源共享的问题。 61 | 62 | 同时,`P1` 和 `C1` 进行通信的消息通道是有缓冲的,当大量的消息发送给 `C1` 时,首先会放入消息通道的缓冲区中,当 `C1` 处理完一条消息后,再从该缓冲区中取出下一条消息进行处理,这种方式跟消息队列( mq ) 非常类似,可以实现更高的吞吐。而且这种方式还有利于实现连接池,例如不止一个 `P` 和 `C` 时,多个 `P` 可以往消息通道中发送消息,同时多个 `C`,其中每个 `C` 都维护一条连接,并从消息通道获取消息。 63 | 64 | ## Tokio的消息通道( channel ) 65 | Tokio 提供了多种消息通道,可以满足不同场景的需求: 66 | 67 | - [`mpsc`](https://docs.rs/tokio/1.15.0/tokio/sync/mpsc/index.html), 多生产者,单消费者模式 68 | - [`oneshot`](https://docs.rs/tokio/1.15.0/tokio/sync/oneshot/index.html), 单生产者单消费,一次只能发送一条消息 69 | - [`broadcast`](https://docs.rs/tokio/1/tokio/sync/broadcast/index.html),多生产者,多消费者,其中每一条发送的消息都可以被所有接收者收到,因此是广播 70 | - [`watch`](https://docs.rs/tokio/1/tokio/sync/watch/index.html),单生产者,多消费者,只保存一条最新的消息,因此接收者只能看到最近的一条消息,例如,这种模式适用于配置文件变化的监听 71 | 72 | 细心的同学可能会发现,这里还少了一种类型:多生产者、多消费者,且每一条消息只能被其中一个消费者接收,如果有这种需求,可以使用 [`async-channel`](https://docs.rs/async-channel/latest/async_channel/) 包。 73 | 74 | 以上这些消息通道都有一个共同点:适用于 `async` 编程,对于其它场景,你可以使用在[多线程章节](https://course.rs/advance/concurrency-with-threads/message-passing.html)中提到过的 `std::sync::mpsc` 和 `crossbeam::channel`, 这些通道在等待消息时会阻塞当前的线程,因此不适用于 `async` 编程。 75 | 76 | 在下面的代码中,我们将使用 `mpsc` 和 `oneshot`, 本章节完整的代码见[这里](https://github.com/tokio-rs/website/blob/master/tutorial-code/channels/src/main.rs)。 77 | 78 | ## 定义消息类型 79 | 在大多数场景中使用消息传递时,都是多个发送者向一个任务发送消息,该任务在处理完后,需要将响应内容返回给相应的发送者。例如我们的例子中,任务需要将 `GET` 和 `SET` 命令处理的结果返回。首先,我们需要定一个 `Command` 枚举用于代表命令: 80 | ```rust 81 | use bytes::Bytes; 82 | 83 | #[derive(Debug)] 84 | enum Command { 85 | Get { 86 | key: String, 87 | }, 88 | Set { 89 | key: String, 90 | val: Bytes, 91 | } 92 | } 93 | ``` 94 | 95 | ## 创建消息通道 96 | 在 `src/bin/client.rs` 的 `main` 函数中,创建一个 `mpsc` 消息通道: 97 | ```rust 98 | use tokio::sync::mpsc; 99 | 100 | #[tokio::main] 101 | async fn main() { 102 | // 创建一个新通道,缓冲队列长度是 32 103 | let (tx, mut rx) = mpsc::channel(32); 104 | 105 | // ... 其它代码 106 | } 107 | ``` 108 | 109 | 一个任务可以通过此通道将命令发送给管理 redis 连接的任务,同时由于通道支持多个生产者,因此多个任务可以同时发送命令。创建该通道会返回一个发送和接收句柄,这两个句柄可以分别被使用,例如它们可以被移动到不同的任务中。 110 | 111 | 通道的缓冲队列长度是 32,意味着如果消息发送的比接收的快,这些消息将被存储在缓冲队列中,一旦存满了 32 条消息,使用`send(...).await`的发送者会**进入睡眠**,直到缓冲队列可以放入新的消息(被接收者消费了)。 112 | 113 | ```rust 114 | use tokio::sync::mpsc; 115 | 116 | #[tokio::main] 117 | async fn main() { 118 | let (tx, mut rx) = mpsc::channel(32); 119 | let tx2 = tx.clone(); 120 | 121 | tokio::spawn(async move { 122 | tx.send("sending from first handle").await; 123 | }); 124 | 125 | tokio::spawn(async move { 126 | tx2.send("sending from second handle").await; 127 | }); 128 | 129 | while let Some(message) = rx.recv().await { 130 | println!("GOT = {}", message); 131 | } 132 | } 133 | ``` 134 | 135 | 你可以使用 `clone` 方法克隆多个发送者,但是接收者无法被克隆,因为我们的通道是 `mpsc` 类型。 136 | 137 | 当所有的发送者都被 `Drop` 掉后(超出作用域或被 `drop(...)` 函数主动释放),就不再会有任何消息发送给该通道,此时 `recv` 方法将返回 `None`,也意味着该通道已经**被关闭**。 138 | 139 | 在我们的例子中,接收者是在管理 redis 连接的任务中,当该任务发现所有发送者都关闭时,它知道它的使命可以完成了,因此它会关闭 redis 连接。 140 | 141 | ## 生成管理任务 142 | 下面,我们来一起创建一个管理任务,它会管理 redis 的连接,当然,首先需要创建一条到 redis 的连接: 143 | ```rust 144 | use mini_redis::client; 145 | // 将消息通道接收者 rx 的所有权转移到管理任务中 146 | let manager = tokio::spawn(async move { 147 | // Establish a connection to the server 148 | // 建立到 redis 服务器的连接 149 | let mut client = client::connect("127.0.0.1:6379").await.unwrap(); 150 | 151 | // 开始接收消息 152 | while let Some(cmd) = rx.recv().await { 153 | use Command::*; 154 | 155 | match cmd { 156 | Get { key } => { 157 | client.get(&key).await; 158 | } 159 | Set { key, val } => { 160 | client.set(&key, val).await; 161 | } 162 | } 163 | } 164 | }); 165 | ``` 166 | 167 | 如上所示,当从消息通道接收到一个命令时,该管理任务会将此命令通过 redis 连接发送到服务器。 168 | 169 | 现在,让两个任务发送命令到消息通道,而不是像最开始报错的那样,直接发送命令到各自的 redis 连接: 170 | ```rust 171 | // 由于有两个任务,因此我们需要两个发送者 172 | let tx2 = tx.clone(); 173 | 174 | // 生成两个任务,一个用于获取 key,一个用于设置 key 175 | let t1 = tokio::spawn(async move { 176 | let cmd = Command::Get { 177 | key: "hello".to_string(), 178 | }; 179 | 180 | tx.send(cmd).await.unwrap(); 181 | }); 182 | 183 | let t2 = tokio::spawn(async move { 184 | let cmd = Command::Set { 185 | key: "foo".to_string(), 186 | val: "bar".into(), 187 | }; 188 | 189 | tx2.send(cmd).await.unwrap(); 190 | }); 191 | ``` 192 | 193 | 在 `main` 函数的末尾,我们让 3 个任务,按照需要的顺序开始运行: 194 | ```rust 195 | t1.await.unwrap(); 196 | t2.await.unwrap(); 197 | manager.await.unwrap(); 198 | ``` 199 | 200 | ## 接收响应消息 201 | 最后一步,就是让发出命令的任务从管理任务那里获取命令执行的结果。为了完成这个目标,我们将使用 `oneshot` 消息通道,因为它针对一发一收的使用类型做过特别优化,且特别适用于此时的场景:接收一条从管理任务发送的结果消息。 202 | 203 | ```rust 204 | use tokio::sync::oneshot; 205 | 206 | let (tx, rx) = oneshot::channel(); 207 | ``` 208 | 209 | 使用方式跟 `mpsc` 很像,但是它并没有缓存长度,因为只能发送一条,接收一条,还有一点不同:你无法对返回的两个句柄进行 `clone`。 210 | 211 | 为了让管理任务将结果准确的返回到发送者手中,这个管道的发送端必须要随着命令一起发送, 然后发出命令的任务保留管道的发送端。一个比较好的实现就是将管道的发送端放入 `Command` 的数据结构中,同时使用一个别名来代表该发送端: 212 | ```rust 213 | use tokio::sync::oneshot; 214 | use bytes::Bytes; 215 | 216 | #[derive(Debug)] 217 | enum Command { 218 | Get { 219 | key: String, 220 | resp: Responder>, 221 | }, 222 | Set { 223 | key: String, 224 | val: Bytes, 225 | resp: Responder<()>, 226 | }, 227 | } 228 | 229 | 230 | /// 管理任务可以使用该发送端将命令执行的结果传回给发出命令的任务 231 | type Responder = oneshot::Sender>; 232 | ``` 233 | 234 | 下面,更新发送命令的代码: 235 | ```rust 236 | let t1 = tokio::spawn(async move { 237 | let (resp_tx, resp_rx) = oneshot::channel(); 238 | let cmd = Command::Get { 239 | key: "hello".to_string(), 240 | resp: resp_tx, 241 | }; 242 | 243 | // 发送 GET 请求 244 | tx.send(cmd).await.unwrap(); 245 | 246 | // 等待回复 247 | let res = resp_rx.await; 248 | println!("GOT = {:?}", res); 249 | }); 250 | 251 | let t2 = tokio::spawn(async move { 252 | let (resp_tx, resp_rx) = oneshot::channel(); 253 | let cmd = Command::Set { 254 | key: "foo".to_string(), 255 | val: "bar".into(), 256 | resp: resp_tx, 257 | }; 258 | 259 | // 发送 SET 请求 260 | tx2.send(cmd).await.unwrap(); 261 | 262 | // 等待回复 263 | let res = resp_rx.await; 264 | println!("GOT = {:?}", res); 265 | }); 266 | ``` 267 | 268 | 最后,更新管理任务: 269 | ```rust 270 | while let Some(cmd) = rx.recv().await { 271 | match cmd { 272 | Command::Get { key, resp } => { 273 | let res = client.get(&key).await; 274 | // 忽略错误 275 | let _ = resp.send(res); 276 | } 277 | Command::Set { key, val, resp } => { 278 | let res = client.set(&key, val).await; 279 | // 忽略错误 280 | let _ = resp.send(res); 281 | } 282 | } 283 | } 284 | ``` 285 | 286 | 有一点值得注意,往 `oneshot` 中发送消息时,并没有使用 `.await`,原因是该发送操作要么直接成功、要么失败,并不需要等待。 287 | 288 | 当 `oneshot` 的接受端被 `drop` 后,继续发送消息会直接返回 `Err` 错误,它表示接收者已经不感兴趣了。对于我们的场景,接收者不感兴趣是非常合理的操作,并不是一种错误,因此可以直接忽略。 289 | 290 | 本章的完整代码见[这里](https://github.com/tokio-rs/website/blob/master/tutorial-code/channels/src/main.rs)。 291 | 292 | ## 对消息通道进行限制 293 | 无论何时使用消息通道,我们都需要对缓存队列的长度进行限制,这样系统才能优雅的处理各种负载状况。如果不限制,假设接收端无法及时处理消息,那消息就会迅速堆积,最终可能会导致内存消耗殆尽,就算内存没有消耗完,也可能会导致整体性能的大幅下降。 294 | 295 | Tokio 在设计时就考虑了这种状况,例如 `async` 操作在 Tokio 中是惰性的: 296 | ```rust 297 | loop { 298 | async_op(); 299 | } 300 | ``` 301 | 302 | 如果上面代码中,`async_op` 不是惰性的,而是在每次循环时立即执行,那该循环会立即将一个 `async_op` 发送到缓冲队列中,然后开始执行下一个循环,因为无需等待任务执行完成,这种发送速度是非常恐怖的,一秒钟可能会有几十万、上百万的消息发送到消息队列中。在其它语言编程中,相信大家也或多或少遇到过这种情况。 303 | 304 | 然后在 `Async Rust` 和 Tokio 中,上面的代码 `async_op` 根本就不会运行,也就不会往消息队列中写入消息。原因是我们没有调用 `.await`,就算使用了 `.await` 上面的代码也不会有问题,因为只有等当前循环的任务结束后,才会开始下一次循环。 305 | 306 | ```rust 307 | loop { 308 | // 当前 `async_op` 完成后,才会开始下一次循环 309 | async_op().await; 310 | } 311 | ``` 312 | 313 | 总之,在 Tokio 中我们必须要显式地引入并发和队列: 314 | 315 | - `tokio::spawn` 316 | - `select!` 317 | - `join!` 318 | - `mpsc::channel` 319 | 320 | 当这么做时,我们需要小心的控制并发度来确保系统的安全。例如,当使用一个循环去接收 TCP 连接时,你要确保当前打开的 `socket` 数量在可控范围内,而不是毫无原则的接收连接。 再比如,当使用 `mpsc::channel` 时,要设置一个缓冲值。 321 | 322 | 挑选一个合适的限制值是 `Tokio` 编程中很重要的一部分,可以帮助我们的系统更加安全、可靠的运行。 323 | -------------------------------------------------------------------------------- /src/frame.md: -------------------------------------------------------------------------------- 1 | # 解析数据帧 2 | 现在,鉴于大家已经掌握了 Tokio 的基本 I/O 用法,我们可以开始实现 `mini-redis` 的帧 `frame`。通过帧可以将字节流转换成帧组成的流。每个帧就是一个数据单元,例如客户端发送的一次请求就是一个帧。 3 | ```rust 4 | use bytes::Bytes; 5 | 6 | enum Frame { 7 | Simple(String), 8 | Error(String), 9 | Integer(u64), 10 | Bulk(Bytes), 11 | Null, 12 | Array(Vec), 13 | } 14 | ``` 15 | 16 | 可以看到帧除了数据之外,并不具备任何语义。命令解析和实现会在更高的层次进行(相比帧解析层)。我们再来通过 HTTP 的帧来帮大家加深下相关的理解: 17 | ```rust 18 | enum HttpFrame { 19 | RequestHead { 20 | method: Method, 21 | uri: Uri, 22 | version: Version, 23 | headers: HeaderMap, 24 | }, 25 | ResponseHead { 26 | status: StatusCode, 27 | version: Version, 28 | headers: HeaderMap, 29 | }, 30 | BodyChunk { 31 | chunk: Bytes, 32 | }, 33 | } 34 | ``` 35 | 36 | 为了实现 `mini-redis` 的帧,我们需要一个 `Connection` 结构体,里面包含了一个 `TcpStream` 以及对帧进行读写的方法: 37 | ```rust 38 | use tokio::net::TcpStream; 39 | use mini_redis::{Frame, Result}; 40 | 41 | struct Connection { 42 | stream: TcpStream, 43 | // ... 这里定义其它字段 44 | } 45 | 46 | impl Connection { 47 | /// 从连接读取一个帧 48 | /// 49 | /// 如果遇到EOF,则返回 None 50 | pub async fn read_frame(&mut self) 51 | -> Result> 52 | { 53 | // 具体实现 54 | } 55 | 56 | /// 将帧写入到连接中 57 | pub async fn write_frame(&mut self, frame: &Frame) 58 | -> Result<()> 59 | { 60 | // 具体实现 61 | } 62 | } 63 | ``` 64 | 65 | 关于Redis协议的说明,可以看看[官方文档](https://redis.io/topics/protocol),`Connection` 代码的完整实现见[这里](https://github.com/tokio-rs/mini-redis/blob/tutorial/src/connection.rs). 66 | 67 | ## 缓冲读取(Buffered Reads) 68 | `read_frame` 方法会等到一个完整的帧都读取完毕后才返回,与之相比,它底层调用的`TcpStream::read` 只会返回任意多的数据(填满传入的缓冲区 buffer ),它可能返回帧的一部分、一个帧、多个帧,总之这种读取行为是不确定的。 69 | 70 | 当 `read_frame` 的底层调用 `TcpStream::read` 读取到部分帧时,会将数据先缓冲起来,接着继续等待并读取数据。如果读到多个帧,那第一个帧会被返回,然后剩下的数据依然被缓冲起来,等待下一次 `read_frame` 被调用。 71 | 72 | 为了实现这种功能,我们需要为 `Connection` 增加一个读取缓冲区。数据首先从 `socket` 中读取到缓冲区中,接着这些数据会被解析为帧,当一个帧被解析后,该帧对应的数据会从缓冲区被移除。 73 | 74 | 这里使用 [`BytesMut`](https://docs.rs/bytes/1/bytes/struct.BytesMut.html) 作为缓冲区类型,它是 [`Bytes`](https://docs.rs/bytes/1/bytes/struct.Bytes.html) 的可变版本。 75 | 76 | ```rust 77 | use bytes::BytesMut; 78 | use tokio::net::TcpStream; 79 | 80 | pub struct Connection { 81 | stream: TcpStream, 82 | buffer: BytesMut, 83 | } 84 | 85 | impl Connection { 86 | pub fn new(stream: TcpStream) -> Connection { 87 | Connection { 88 | stream, 89 | // 分配一个缓冲区,具有4kb的缓冲长度 90 | buffer: BytesMut::with_capacity(4096), 91 | } 92 | } 93 | } 94 | ``` 95 | 96 | 接下来,实现 `read_frame` 方法: 97 | ```rust 98 | use tokio::io::AsyncReadExt; 99 | use bytes::Buf; 100 | use mini_redis::Result; 101 | 102 | pub async fn read_frame(&mut self) 103 | -> Result> 104 | { 105 | loop { 106 | // 尝试从缓冲区的数据中解析出一个数据帧, 107 | // 只有当数据足够被解析时,才返回对应的帧 108 | if let Some(frame) = self.parse_frame()? { 109 | return Ok(Some(frame)); 110 | } 111 | 112 | // 如果缓冲区中的数据还不足以被解析为一个数据帧, 113 | // 那么我们需要从 socket 中读取更多的数据 114 | // 115 | // 读取成功时,会返回读取到的字节数,0 代表着读到了数据流的末尾 116 | if 0 == self.stream.read_buf(&mut self.buffer).await? { 117 | // 代码能执行到这里,说明了对端关闭了连接, 118 | // 需要看看缓冲区是否还有数据,若没有数据,说明所有数据成功被处理, 119 | // 若还有数据,说明对端在发送帧的过程中断开了连接,导致只发送了部分数据 120 | if self.buffer.is_empty() { 121 | return Ok(None); 122 | } else { 123 | return Err("connection reset by peer".into()); 124 | } 125 | } 126 | } 127 | } 128 | ``` 129 | 130 | `read_frame` 内部使用循环的方式读取数据,直到一个完整的帧被读取到时,才会返回。当然,当远程的对端关闭了连接后,也会返回。 131 | 132 | #### `Buf` 特征 133 | 在上面的 `read_frame` 方法中,我们使用了 `read_buf` 来读取 socket 中的数据,该方法的参数是来自 [`bytes`](https://docs.rs/bytes/) 包的 `BufMut`。 134 | 135 | 可以先来考虑下该如何使用 `read()` 和 `Vec` 来实现同样的功能 : 136 | ```rust 137 | use tokio::net::TcpStream; 138 | 139 | pub struct Connection { 140 | stream: TcpStream, 141 | buffer: Vec, 142 | cursor: usize, 143 | } 144 | 145 | impl Connection { 146 | pub fn new(stream: TcpStream) -> Connection { 147 | Connection { 148 | stream, 149 | // 4kb 大小的缓冲区 150 | buffer: vec![0; 4096], 151 | cursor: 0, 152 | } 153 | } 154 | } 155 | ``` 156 | 157 | 下面是相应的 `read_frame` 方法: 158 | ```rust 159 | use mini_redis::{Frame, Result}; 160 | 161 | pub async fn read_frame(&mut self) 162 | -> Result> 163 | { 164 | loop { 165 | if let Some(frame) = self.parse_frame()? { 166 | return Ok(Some(frame)); 167 | } 168 | 169 | // 确保缓冲区长度足够 170 | if self.buffer.len() == self.cursor { 171 | // 若不够,需要增加缓冲区长度 172 | self.buffer.resize(self.cursor * 2, 0); 173 | } 174 | 175 | // 从游标位置开始将数据读入缓冲区 176 | let n = self.stream.read( 177 | &mut self.buffer[self.cursor..]).await?; 178 | 179 | if 0 == n { 180 | if self.cursor == 0 { 181 | return Ok(None); 182 | } else { 183 | return Err("connection reset by peer".into()); 184 | } 185 | } else { 186 | // 更新游标位置 187 | self.cursor += n; 188 | } 189 | } 190 | } 191 | ``` 192 | 193 | 在这段代码中,我们使用了非常重要的技术:通过游标( cursor )跟踪已经读取的数据,并将下次读取的数据写入到游标之后的缓冲区中,只有这样才不会让新读取的数据将之前读取的数据覆盖掉。 194 | 195 | 一旦缓冲区满了,还需要增加缓冲区的长度,这样才能继续写入数据。还有一点值得注意,在 `parse_frame` 方法的内部实现中,也需要通过游标来解析数据: `self.buffer[..self.cursor]`,通过这种方式,我们可以准确获取到目前已经读取的全部数据。 196 | 197 | 在网络编程中,通过字节数组和游标的方式读取数据是非常普遍的,因此 `bytes` 包提供了一个 `Buf` 特征,如果一个类型可以被读取数据,那么该类型需要实现 `Buf` 特征。与之对应,当一个类型可以被写入数据时,它需要实现 `BufMut` 。 198 | 199 | 当 `T: BufMut` ( 特征约束,说明类型 `T` 实现了 `BufMut` 特征 ) 被传给 `read_buf()` 方法时,缓冲区 `T` 的内部游标会自动进行更新。正因为如此,在使用了 `BufMut` 版本的 `read_frame` 中,我们并不需要管理自己的游标。 200 | 201 | 除了游标之外,`Vec` 的使用也值得关注,该缓冲区在使用时必须要被初始化: `vec![0; 4096]`,该初始化会创建一个 4096 字节长度的数组,然后将数组的每个元素都填充上 0 。当缓冲区长度不足时,新创建的缓冲区数组依然会使用 0 被重新填充一遍。 事实上,这种初始化过程会存在一定的性能开销。 202 | 203 | 与 `Vec` 相反, `BytesMut` 和 `BufMut` 就没有这个问题,它们无需被初始化,而且 `BytesMut` 还会阻止我们读取未初始化的内存。 204 | 205 | ## 帧解析 206 | 在理解了该如何读取数据后, 再来看看该如何通过两个部分解析出一个帧: 207 | 208 | - 确保有一个完整的帧已经被写入了缓冲区,找到该帧的最后一个字节所在的位置 209 | - 解析帧 210 | 211 | ```rust 212 | use mini_redis::{Frame, Result}; 213 | use mini_redis::frame::Error::Incomplete; 214 | use bytes::Buf; 215 | use std::io::Cursor; 216 | 217 | fn parse_frame(&mut self) 218 | -> Result> 219 | { 220 | // 创建 `T: Buf` 类型 221 | let mut buf = Cursor::new(&self.buffer[..]); 222 | 223 | // 检查是否读取了足够解析出一个帧的数据 224 | match Frame::check(&mut buf) { 225 | Ok(_) => { 226 | // 获取组成该帧的字节数 227 | let len = buf.position() as usize; 228 | 229 | // 在解析开始之前,重置内部的游标位置 230 | buf.set_position(0); 231 | 232 | // 解析帧 233 | let frame = Frame::parse(&mut buf)?; 234 | 235 | // 解析完成,将缓冲区该帧的数据移除 236 | self.buffer.advance(len); 237 | 238 | // 返回解析出的帧 239 | Ok(Some(frame)) 240 | } 241 | // 缓冲区的数据不足以解析出一个完整的帧 242 | Err(Incomplete) => Ok(None), 243 | // 遇到一个错误 244 | Err(e) => Err(e.into()), 245 | } 246 | } 247 | ``` 248 | 249 | 完整的 `Frame::check` 函数实现在[这里](https://github.com/tokio-rs/mini-redis/blob/tutorial/src/frame.rs#L63-L100),感兴趣的同学可以看看,在这里我们不会对它进行完整的介绍。 250 | 251 | 值得一提的是, `Frame::check` 使用了 `Buf` 的字节迭代风格的 API。例如,为了解析一个帧,首先需要检查它的第一个字节,该字节用于说明帧的类型。这种首字节检查是通过 `Buf::get_u8` 函数完成的,该函数会获取游标所在位置的字节,然后将游标位置向右移动一个字节。 252 | 253 | ## 缓冲写入(Buffered writes) 254 | 关于帧操作的另一个 API 是 `write_frame(frame)` 函数,它会将一个完整的帧写入到 socket 中。 每一次写入,都会触发一次或数次系统调用,当程序中有大量的连接和写入时,系统调用的开销将变得非常高昂,具体可以看看 SyllaDB 团队写过的一篇[性能调优文章](https://www.scylladb.com/2022/01/12/async-rust-in-practice-performance-pitfalls-profiling/)。 255 | 256 | 为了降低系统调用的次数,我们需要使用一个写入缓冲区,当写入一个帧时,首先会写入该缓冲区,然后等缓冲区数据足够多时,再集中将其中的数据写入到 socket 中,这样就将多次系统调用优化减少到一次。 257 | 258 | 还有,缓冲区也不总是能提升性能。 例如,考虑一个 `bulk` 帧(多个帧放在一起组成一个bulk,通过批量发送提升效率),该帧的特点就是:由于由多个帧组合而成,因此帧体数据可能会很大。所以我们不能将其帧体数据写入到缓冲区中,因为数据较大时,先写入缓冲区再写入 socket 会有较大的性能开销(实际上缓冲区就是为了批量写入,既然 bulk 已经是批量了,因此不使用缓冲区也很正常)。 259 | 260 | 261 | 为了实现缓冲写,我们将使用 [`BufWriter`](https://docs.rs/tokio/1/tokio/io/struct.BufWriter.html) 结构体。该结构体实现了 `AsyncWrite` 特征,当 `write` 方法被调用时,不会直接写入到 socket 中,而是先写入到缓冲区中。当缓冲区被填满时,其中的内容会自动刷到(写入到)内部的 socket 中,然后再将缓冲区清空。当然,其中还存在某些优化,通过这些优化可以绕过缓冲区直接访问 socket。 262 | 263 | 由于篇幅有限,我们不会实现完整的 `write_frame` 函数,想要看完整代码可以访问[这里](https://github.com/tokio-rs/mini-redis/blob/tutorial/src/connection.rs#L159-L184)。 264 | 265 | 首先,更新下 `Connection` 的结构体: 266 | ```rust 267 | use tokio::io::BufWriter; 268 | use tokio::net::TcpStream; 269 | use bytes::BytesMut; 270 | 271 | pub struct Connection { 272 | stream: BufWriter, 273 | buffer: BytesMut, 274 | } 275 | 276 | impl Connection { 277 | pub fn new(stream: TcpStream) -> Connection { 278 | Connection { 279 | stream: BufWriter::new(stream), 280 | buffer: BytesMut::with_capacity(4096), 281 | } 282 | } 283 | } 284 | ``` 285 | 286 | 接着来实现 `write_frame` 函数: 287 | ```rust 288 | use tokio::io::{self, AsyncWriteExt}; 289 | use mini_redis::Frame; 290 | 291 | async fn write_frame(&mut self, frame: &Frame) 292 | -> io::Result<()> 293 | { 294 | match frame { 295 | Frame::Simple(val) => { 296 | self.stream.write_u8(b'+').await?; 297 | self.stream.write_all(val.as_bytes()).await?; 298 | self.stream.write_all(b"\r\n").await?; 299 | } 300 | Frame::Error(val) => { 301 | self.stream.write_u8(b'-').await?; 302 | self.stream.write_all(val.as_bytes()).await?; 303 | self.stream.write_all(b"\r\n").await?; 304 | } 305 | Frame::Integer(val) => { 306 | self.stream.write_u8(b':').await?; 307 | self.write_decimal(*val).await?; 308 | } 309 | Frame::Null => { 310 | self.stream.write_all(b"$-1\r\n").await?; 311 | } 312 | Frame::Bulk(val) => { 313 | let len = val.len(); 314 | 315 | self.stream.write_u8(b'$').await?; 316 | self.write_decimal(len as u64).await?; 317 | self.stream.write_all(val).await?; 318 | self.stream.write_all(b"\r\n").await?; 319 | } 320 | Frame::Array(_val) => unimplemented!(), 321 | } 322 | 323 | self.stream.flush().await; 324 | 325 | Ok(()) 326 | } 327 | ``` 328 | 329 | 这里使用的方法由 `AsyncWriteExt` 提供,它们在 `TcpStream` 中也有对应的函数。但是在没有缓冲区的情况下最好避免使用这种逐字节的写入方式!不然,每写入几个字节就会触发一次系统调用,写完整个数据帧可能需要几十次系统调用,可以说是丧心病狂! 330 | 331 | - `write_u8` 写入一个字节 332 | - `write_all` 写入所有数据 333 | - `write_decimal`由 mini-redis 提供 334 | 335 | 在函数结束前,我们还额外的调用了一次 `self.stream.flush().await`,原因是缓冲区可能还存在数据,因此需要手动刷一次数据:`flush` 的调用会将缓冲区中剩余的数据立刻写入到 socket 中。 336 | 337 | 当然,当帧比较小的时候,每写一次帧就 flush 一次的模式性能开销会比较大,此时我们可以选择在 `Connection` 中实现 `flush` 函数,然后将等帧积累多个后,再一次性在 `Connection` 中进行 flush。当然,对于我们的例子来说,简洁性是非常重要的,因此选了将 `flush` 放入到 `write_frame` 中。 -------------------------------------------------------------------------------- /src/getting-startted.md: -------------------------------------------------------------------------------- 1 | # tokio初印象 2 | 又到了喜闻乐见的初印象环节,这个环节决定了你心中的那24盏灯最终是全绿还是全灭。 3 | 4 | 在本文中,我们将看看本专题的学习目标、`tokio`该怎么引入以及如何实现一个 `Hello Tokio` 项目,最终留灯还是灭灯的决定权留给各位看官。但我提前说好,如果你全灭了,但却找不到更好的,未来还是得回来真香 :P 5 | 6 | ## 专题目标 7 | 通过 API 学项目无疑是无聊的,因此我们采用一个与众不同的方式:边学边练,在本专题的最后你将拥有一个 `redis` 客户端和服务端,当然不会实现一个完整版本的 `redis` ,只会提供基本的功能和部分常用的命令。 8 | 9 | #### mini-redis 10 | `redis` 的项目源码可以在[这里访问](https://github.com/sunface/rust-course/tree/main/pratice/mini-redis),本项目是从[官方地址](https://github.com/tokio-rs/mini-redis) `fork` 而来,在未来会提供注释和文档汉化。 11 | 12 | 再次声明:该项目仅仅用于学习目的,因此它的文档注释非常全,但是它完全无法作为 `redis` 的替代品。 13 | 14 | ## 环境配置 15 | 首先,我们假定你已经安装了 Rust 和相关的工具链,例如 `cargo`。其中 Rust 版本的最低要求是 `1.45.0`,建议使用最新版 `1.58`: 16 | ```shell 17 | sunfei@sunface $ rustc --version 18 | rustc 1.58.0 (02072b482 2022-01-11) 19 | ``` 20 | 21 | 接下来,安装 `mini-redis` 的服务器端,它可以用来测试我们后面将要实现的 `redis` 客户端: 22 | ```shell 23 | $ cargo install mini-redis 24 | ``` 25 | 26 | > 如果下载失败,也可以通过[这个地址](https://github.com/sunface/rust-course/tree/main/pratice/mini-redis)下载源码,然后在本地通过 `cargo run`运行。 27 | 28 | 下载成功后,启动服务端: 29 | ```shell 30 | $ mini-redis-server 31 | ``` 32 | 33 | 然后,再使用客户端测试下刚启动的服务端: 34 | ```shell 35 | $ mini-redis-cli set foo 1 36 | OK 37 | $ mini-redis-cli get foo 38 | "1" 39 | ``` 40 | 41 | 不得不说,还挺好用的,先自我陶醉下 :) 此时,万事俱备,只欠东风,接下来是时候亮"箭"了:实现我们的 `Hello Tokio` 项目。 42 | 43 | ## Hello Tokio 44 | 与简单无比的 `Hello World` 有所不同(简单?还记得本书开头时,湖畔边的那个多国语言版本的`你好,世界`嘛~~),`Hello Tokio` 它承载着"非常艰巨"的任务,那就是向刚启动的 `redis` 服务器写入一个 `key=hello, value=world` ,然后再读取出来,嗯,使用 `mini-redis` 客户端 :) 45 | 46 | #### 分析未到,代码先行 47 | 在详细讲解之前,我们先来看看完整的代码,让大家有一个直观的印象。首先,创建一个新的 `Rust` 项目: 48 | ```shell 49 | $ cargo new my-redis 50 | $ cd my-redis 51 | ``` 52 | 53 | 然后在 `Cargo.toml` 中添加相关的依赖: 54 | ```toml 55 | [dependencies] 56 | tokio = { version = "1", features = ["full"] } 57 | mini-redis = "0.4" 58 | ``` 59 | 60 | 接下来,使用以下代码替换 `main.rs` 中的内容: 61 | ```rust 62 | use mini_redis::{client, Result}; 63 | 64 | #[tokio::main] 65 | async fn main() -> Result<()> { 66 | // 建立与mini-redis服务器的连接 67 | let mut client = client::connect("127.0.0.1:6379").await?; 68 | 69 | // 设置 key: "hello" 和 值: "world" 70 | client.set("hello", "world".into()).await?; 71 | 72 | // 获取"key=hello"的值 73 | let result = client.get("hello").await?; 74 | 75 | println!("从服务器端获取到结果={:?}", result); 76 | 77 | Ok(()) 78 | } 79 | ``` 80 | 81 | 不知道你之前启动的 `mini-redis-server` 关闭没有,如果关了,记得重新启动下,否则我们的代码就是意大利空气炮。 82 | 83 | 最后,运行这个项目: 84 | ```shell 85 | $ cargo run 86 | 从服务器端获取到结果=Some(b"world") 87 | ``` 88 | 89 | Perfect, 代码成功运行,是时候来解释下其中蕴藏的至高奥秘了。 90 | 91 | ## 原理解释 92 | 代码篇幅虽然不长,但是还是有不少值得关注的地方,接下来我们一起来看看。 93 | 94 | ```rust 95 | let mut client = client::connect("127.0.0.1:6379").await?; 96 | ``` 97 | 98 | [`client::connect`](https://docs.rs/mini-redis/0.4.1/mini_redis/client/fn.connect.html) 函数由`mini-redis` 包提供,它使用异步的方式跟指定的远程 `IP` 地址建立 TCP 长连接,一旦连接建立成功,那 `client` 的赋值初始化也将完成。 99 | 100 | 特别值得注意的是:虽然该连接是异步建立的,但是从代码本身来看,完全是**同步的代码编写方式**,唯一能说明异步的点就是 `.await`。 101 | 102 | #### 什么是异步编程 103 | 大部分计算机程序都是按照代码编写的顺序来执行的:先执行第一行,然后第二行,以此类推(当然,还要考虑流程控制,例如循环)。当进行同步编程时,一旦程序遇到一个操作无法被立即完成,它就会进入阻塞状态,直到该操作完成为止。 104 | 105 | 因此同步编程非常符合我们人类的思维习惯,是一个顺其自然的过程,被几乎每一个程序员所喜欢(本来想说所有,但我不敢打包票,毕竟总有特立独行之士)。例如,当建立 TCP 连接时,当前线程会被阻塞,直到等待该连接建立完成,然后才往下继续进行。 106 | 107 | 而使用异步编程,无法立即完成的操作会被切到后台去等待,因此当前线程不会被阻塞,它会接着执行其它的操作。一旦之前的操作准备好可以继续执行后,它会通知执行器,然后执行器会调度它并从上次离开的点继续执行。但是大家想象下,如果没有使用 `await`,而是按照这个异步的流程使用通知 -> 回调的方式实现,代码该多么的难写和难读! 108 | 109 | 好在 Rust 为我们提供了 `async/await` 的异步编程特性,让我们可以像写同步代码那样去写异步的代码,也让这个世界美好依旧。 110 | 111 | #### 编译时绿色线程 112 | 一个函数可以通过`async fn`的方式被标记为异步函数: 113 | ```rust 114 | use mini_redis::Result; 115 | use mini_redis::client::Client; 116 | use tokio::net::ToSocketAddrs; 117 | 118 | pub async fn connect(addr: T) -> Result { 119 | // ... 120 | } 121 | ``` 122 | 123 | 在上例中,`redis` 的连接函数 `connect` 实现如上,它看上去很像是一个同步函数,但是 `async fn` 出卖了它。 124 | `async fn` 异步函数并不会直接返回值,而是返回一个 `Future`,顾名思义,该 `Future` 会在未来某个时间点被执行,然后最终获取到真实的返回值 `Result`。 125 | 126 | > async/await 的原理就算大家不理解,也不妨碍使用 `tokio` 写出能用的服务,但是如果想要更深入的用好,强烈建议认真读下本书的 [`async/await` 异步编程章节](https://course.rs/async/intro.html),你会对 Rust 的异步编程有一个全新且深刻的认识。 127 | 128 | 由于 `async` 会返回一个 `Future`,因此我们还需要配合使用 `.await` 来让该 `Future` 运行起来,最终获得返回值: 129 | ```rust 130 | async fn say_to_world() -> String { 131 | String::from("world") 132 | } 133 | 134 | #[tokio::main] 135 | async fn main() { 136 | // 此处的函数调用是惰性的,并不会执行 `say_to_world()` 函数体中的代码 137 | let op = say_to_world(); 138 | 139 | // 首先打印出 "hello" 140 | println!("hello"); 141 | 142 | // 使用 `.await` 让 `say_to_world` 开始运行起来 143 | println!("{}", op.await); 144 | } 145 | ``` 146 | 147 | 上面代码输出如下: 148 | ```shell 149 | hello 150 | world 151 | ``` 152 | 153 | 而大家可能很好奇 `async fn` 到底返回什么吧?它实际上返回的是一个实现了 `Future` 特征的匿名类型: `impl Future`。 154 | 155 | #### async main 156 | 在代码中,使用了一个与众不同的 `main` 函数 : `async fn main` ,而且是用 `#[tokio::main]` 属性进行了标记。异步 `main` 函数有以下意义: 157 | 158 | - `.await` 只能在 `async` 函数中使用,如果是以前的 `fn main`,那它内部是无法直接使用 `async` 函数的!这个会极大的限制了我们的使用场景 159 | - 异步运行时本身需要初始化 160 | 161 | 因此 `#[tokio::main]` 宏在将 `async fn main` 隐式的转换为 `fn main` 的同时还对整个异步运行时进行了初始化。例如以下代码: 162 | ```rust 163 | #[tokio::main] 164 | async fn main() { 165 | println!("hello"); 166 | } 167 | ``` 168 | 169 | 将被转换成: 170 | ```rust 171 | fn main() { 172 | let mut rt = tokio::runtime::Runtime::new().unwrap(); 173 | rt.block_on(async { 174 | println!("hello"); 175 | }) 176 | } 177 | ``` 178 | 179 | 最终,Rust 编译器就愉快地执行这段代码了。 180 | 181 | ## cargo feature 182 | 在引入 `tokio` 包时,我们在 `Cargo.toml` 文件中添加了这么一行: 183 | ```toml 184 | tokio = { version = "1", features = ["full"] } 185 | ``` 186 | 187 | 里面有个 `features = ["full"]` 可能大家会比较迷惑,当然,关于它的具体解释在本书的 [Cargo详解专题](https://course.rs/cargo/intro.html) 有介绍,这里就简单进行说明, 188 | 189 | `Tokio` 有很多功能和特性,例如 `TCP`,`UDP`,`Unix sockets`,同步工具,多调度类型等等,不是每个应用都需要所有的这些特性。为了优化编译时间和最终生成可执行文件大小、内存占用大小,应用可以对这些特性进行可选引入。 190 | 191 | 而这里为了演示的方便,我们使用 `full` ,表示直接引入所有的特性。 192 | 193 | ## 总结 194 | 大家对 `tokio` 的初印象如何?可否24灯全绿通过? 195 | 196 | 总之,`tokio` 做的事情其实是细雨润无声的,在大多数时候,我们并不能感觉到它的存在,但是它确实是异步编程中最重要的一环(或者之一),深入了解它对我们的未来之路会有莫大的帮助。 197 | 198 | 接下来,正式开始 `tokio` 的学习之旅。 199 | -------------------------------------------------------------------------------- /src/graceful-shutdown.md: -------------------------------------------------------------------------------- 1 | # 优雅的关闭 2 | 如果你的服务是一个小说阅读网站,那大概率用不到优雅关闭的,简单粗暴的关闭服务器,然后用户再次请求时获取一个错误就是了。但如果是一个web服务或数据库服务呢?当前的连接很可能在做着重要的事情,一旦关闭会导致数据的丢失甚至错误,此时,我们就需要优雅的关闭(graceful shutdown)了。 3 | 4 | 要让一个异步应用优雅的关闭往往需要做到3点: 5 | 6 | - 找出合适的关闭时机 7 | - 通知程序的每一个子部分开始关闭 8 | - 在主线程等待各个部分的关闭结果 9 | 10 | 在本文的下面部分,我们一起来看看该如何做到这三点。如果想要进一步了解在真实项目中该如何使用,大家可以看看 mini-redis 的完整代码实现,特别是 [`src/server.rs`](https://lucumr.pocoo.org/2022/1/30/unsafe-rust/) 和 [`src/shutdown.rs`](https://github.com/tokio-rs/mini-redis/blob/master/src/shutdown.rs)。 11 | 12 | 13 | ## 找出合适的关闭时机 14 | 一般来说,何时关闭是取决于应用自身的,但是一个常用的关闭准则就是当应用收到来自于操作系统的关闭信号时。例如通过 `ctrl + c` 来关闭正在运行的命令行程序。 15 | 16 | 为了检测来自操作系统的关闭信号,`Tokio` 提供了一个 `tokio::signal::ctrl_c` 函数,它将一直睡眠直到收到对应的信号: 17 | ```rust 18 | use tokio::signal; 19 | 20 | #[tokio::main] 21 | async fn main() { 22 | // ... spawn application as separate task ... 23 | // 在一个单独的任务中处理应用逻辑 24 | 25 | match signal::ctrl_c().await { 26 | Ok(()) => {}, 27 | Err(err) => { 28 | eprintln!("Unable to listen for shutdown signal: {}", err); 29 | }, 30 | } 31 | 32 | // 发送关闭信号给应用所在的任务,然后等待 33 | } 34 | ``` 35 | 36 | ## 通知程序的每一个部分开始关闭 37 | 大家看到这个标题,不知道会想到用什么技术来解决问题,反正我首先想到的是,真的很像广播哎。。 38 | 39 | 事实上也是如此,最常见的通知程序各个部分关闭的方式就是使用一个广播消息通道。关于如何实现,其实也不复杂:应用中的每个任务都持有一个广播消息通道的接收端,当消息被广播到该通道时,每个任务都可以收到该消息,并关闭自己: 40 | ```rust 41 | let next_frame = tokio::select! { 42 | res = self.connection.read_frame() => res?, 43 | _ = self.shutdown.recv() => { 44 | // 当收到关闭信号后,直接从 `select!` 返回,此时 `select!` 中的另一个分支会自动释放,其中的任务也会结束 45 | return Ok(()); 46 | } 47 | }; 48 | ``` 49 | 50 | 51 | 在 `mini-redis` 中,当收到关闭消息时,任务会立即结束,但在实际项目中,这种方式可能会过于理想,例如当我们向文件或数据库写入数据时,立刻终止任务可能会导致一些无法预料的错误,因此,在结束前做一些收尾工作会是非常好的选择。 52 | 53 | 除此之外,还有两点值得注意: 54 | 55 | - 将广播消息通道作为结构体的一个字段是相当不错的选择, 例如[这个例子](https://github.com/tokio-rs/mini-redis/blob/master/src/shutdown.rs) 56 | - 还可以使用 [`watch channel`](https://docs.rs/tokio/1.16.1/tokio/sync/watch/index.html) 实现同样的效果,与之前的方式相比,这两种方法并没有太大的区别 57 | 58 | ## 等待各个部分的结束 59 | 在之前章节,我们讲到过一个 [`mpsc`](https://docs.rs/tokio/1/tokio/sync/mpsc/index.html) 消息通道有一个重要特性:当所有发送端都 `drop` 时,消息通道会自动关闭,此时继续接收消息就会报错。 60 | 61 | 大家发现没?这个特性特别适合优雅关闭的场景:主线程持有消息通道的接收端,然后每个代码部分拿走一个发送端,当该部分结束时,就 `drop` 掉发送端,因此所有发送端被 `drop` 也就意味着所有的部分都已关闭,此时主线程的接收端就会收到错误,进而结束。 62 | 63 | ```rust 64 | use tokio::sync::mpsc::{channel, Sender}; 65 | use tokio::time::{sleep, Duration}; 66 | 67 | #[tokio::main] 68 | async fn main() { 69 | let (send, mut recv) = channel(1); 70 | 71 | for i in 0..10 { 72 | tokio::spawn(some_operation(i, send.clone())); 73 | } 74 | 75 | // 等待各个任务的完成 76 | // 77 | // 我们需要 drop 自己的发送端,因为等下的 `recv()` 调用会阻塞, 如果不 `drop` ,那发送端就无法被全部关闭 78 | // `recv` 也将永远无法结束,这将陷入一个类似死锁的困境 79 | drop(send); 80 | 81 | // 当所有发送端都超出作用域被 `drop` 时 (当前的发送端并不是因为超出作用域被 `drop` 而是手动 `drop` 的) 82 | // `recv` 调用会返回一个错误 83 | let _ = recv.recv().await; 84 | } 85 | 86 | async fn some_operation(i: u64, _sender: Sender<()>) { 87 | sleep(Duration::from_millis(100 * i)).await; 88 | println!("Task {} shutting down.", i); 89 | 90 | // 发送端超出作用域,然后被 `drop` 91 | } 92 | ``` 93 | 94 | 关于忘记 `drop` 本身持有的发送端进而导致 bug 的问题,大家可以看看[这篇文章](https://course.rs/pitfalls/main-with-channel-blocked.html)。 95 | -------------------------------------------------------------------------------- /src/io.md: -------------------------------------------------------------------------------- 1 | # I/O 2 | 本章节中我们将深入学习 Tokio 中的 I/O 操作,了解它的原理以及该如何使用。 3 | 4 | Tokio 中的 I/O 操作和 `std` 在使用方式上几无区别,最大的区别就是前者是异步的,例如 Tokio 的读写特征分别是 `AsyncRead` 和 `AsyncWrite`: 5 | 6 | - 有部分类型按照自己的所需实现了它们: `TcpStream`,`File`,`Stdout` 7 | - 还有数据结构也实现了它们:`Vec`、`&[u8]`,这样就可以直接使用这些数据结构作为读写器( reader / writer) 8 | 9 | ## AsyncRead 和 AsyncWrite 10 | 这两个特征为字节流的异步读写提供了便利,通常我们会使用 `AsyncReadExt` 和 `AsyncWriteExt` 提供的工具方法,这些方法都使用 `async` 声明,且需要通过 `.await` 进行调用, 11 | 12 | #### async fn read 13 | `AsyncReadExt::read` 是一个异步方法可以将数据读入缓冲区( `buffer` )中,然后返回读取的字节数。 14 | ```rust 15 | use tokio::fs::File; 16 | use tokio::io::{self, AsyncReadExt}; 17 | 18 | #[tokio::main] 19 | async fn main() -> io::Result<()> { 20 | let mut f = File::open("foo.txt").await?; 21 | let mut buffer = [0; 10]; 22 | 23 | // 由于 buffer 的长度限制,当次的 `read` 调用最多可以从文件中读取 10 个字节的数据 24 | let n = f.read(&mut buffer[..]).await?; 25 | 26 | println!("The bytes: {:?}", &buffer[..n]); 27 | Ok(()) 28 | } 29 | ``` 30 | 31 | 需要注意的是:当 `read` 返回 `Ok(0)` 时,意味着字节流( stream )已经关闭,在这之后继续调用 `read` 会立刻完成,依然获取到返回值 `Ok(0)`。 例如,字节流如果是 `TcpStream` 类型,那 `Ok(0)` 说明该**连接的读取端已经被关闭**(写入端关闭,会报其它的错误)。 32 | 33 | #### async fn read_to_end 34 | `AsyncReadExt::read_to_end` 方法会从字节流中读取所有的字节,直到遇到 `EOF` : 35 | ```rust 36 | use tokio::io::{self, AsyncReadExt}; 37 | use tokio::fs::File; 38 | 39 | #[tokio::main] 40 | async fn main() -> io::Result<()> { 41 | let mut f = File::open("foo.txt").await?; 42 | let mut buffer = Vec::new(); 43 | 44 | // 读取整个文件的内容 45 | f.read_to_end(&mut buffer).await?; 46 | Ok(()) 47 | } 48 | ``` 49 | 50 | #### async fn write 51 | `AsyncWriteExt::write` 异步方法会尝试将缓冲区的内容写入到写入器( `writer` )中,同时返回写入的字节数: 52 | ```rust 53 | use tokio::io::{self, AsyncWriteExt}; 54 | use tokio::fs::File; 55 | 56 | #[tokio::main] 57 | async fn main() -> io::Result<()> { 58 | let mut file = File::create("foo.txt").await?; 59 | 60 | let n = file.write(b"some bytes").await?; 61 | 62 | println!("Wrote the first {} bytes of 'some bytes'.", n); 63 | Ok(()) 64 | } 65 | ``` 66 | 67 | 上面代码很清晰,但是大家可能会疑惑 `b"some bytes"` 是什么意思。这种写法可以将一个 `&str` 字符串转变成一个字节数组:`&[u8;10]`,然后 `write` 方法又会将这个 `&[u8;10]` 的数组类型隐式强转为数组切片: `&[u8]`。 68 | 69 | #### async fn write_all 70 | `AsyncWriteExt::write_all` 将缓冲区的内容全部写入到写入器中: 71 | ```rust 72 | use tokio::io::{self, AsyncWriteExt}; 73 | use tokio::fs::File; 74 | 75 | #[tokio::main] 76 | async fn main() -> io::Result<()> { 77 | let mut file = File::create("foo.txt").await?; 78 | 79 | file.write_all(b"some bytes").await?; 80 | Ok(()) 81 | } 82 | ``` 83 | 84 | 以上只是部分方法,实际上还有一些实用的方法由于篇幅有限无法列出,大家可以通过 [API 文档](https://docs.rs/tokio/latest/tokio/io/index.html) 查看完整的列表。 85 | 86 | ## 实用函数 87 | 另外,和标准库一样, `tokio::io` 模块包含了多个实用的函数或API,可以用于处理标准输入/输出/错误等。 88 | 89 | 例如,`tokio::io::copy` 异步的将读取器( `reader` )中的内容拷贝到写入器( `writer` )中。 90 | ```rust 91 | use tokio::fs::File; 92 | use tokio::io; 93 | 94 | #[tokio::main] 95 | async fn main() -> io::Result<()> { 96 | let mut reader: &[u8] = b"hello"; 97 | let mut file = File::create("foo.txt").await?; 98 | 99 | io::copy(&mut reader, &mut file).await?; 100 | Ok(()) 101 | } 102 | ``` 103 | 104 | 还记得我们之前提到的字节数组 `&[u8]` 实现了 `AsyncRead` 吗?正因为这个原因,所以这里可以直接将 `&u8` 用作读取器。 105 | 106 | ## 回声服务( Echo ) 107 | 就如同写代码必写 `hello, world`,实现 web 服务器,往往会选择实现一个回声服务。该服务会将用户的输入内容直接返回给用户,就像回声壁一样。 108 | 109 | 具体来说,就是从用户建立的 TCP 连接的 socket 中读取到数据,然后立刻将同样的数据写回到该 socket 中。因此客户端会收到和自己发送的数据一模一样的回复。 110 | 111 | 下面我们将使用两种稍有不同的方法实现该回声服务。 112 | 113 | #### 使用 `io::copy()` 114 | 先来创建一个新的 bin 文件,用于运行我们的回声服务: 115 | ```console 116 | touch src/bin/echo-server-copy.rs 117 | ``` 118 | 119 | 然后可以通过以下命令运行它(跟上一章节的方式相同): 120 | ```console 121 | cargo run --bin echo-server-copy 122 | ``` 123 | 124 | 至于客户端,可以简单的使用 `telnet` 的方式来连接,或者也可以使用 `tokio::net::TcpStream`,它的[文档示例](https://docs.rs/tokio/1/tokio/net/struct.TcpStream.html#examples)非常适合大家进行参考。 125 | 126 | 先来实现一下基本的服务器框架:通过 loop 循环接收 TCP 连接,然后为每一条连接创建一个单独的任务去处理。 127 | 128 | ```rust 129 | use tokio::io; 130 | use tokio::net::TcpListener; 131 | 132 | #[tokio::main] 133 | async fn main() -> io::Result<()> { 134 | let listener = TcpListener::bind("127.0.0.1:6142").await?; 135 | 136 | loop { 137 | let (mut socket, _) = listener.accept().await?; 138 | 139 | tokio::spawn(async move { 140 | // 在这里拷贝数据 141 | }); 142 | } 143 | } 144 | ``` 145 | 146 | 下面,来看看重头戏 `io::copy` ,它有两个参数:一个读取器,一个写入器,然后将读取器中的数据直接拷贝到写入器中,类似的实现代码如下: 147 | ```rust 148 | io::copy(&mut socket, &mut socket).await 149 | ``` 150 | 151 | 这段代码相信大家一眼就能看出问题,由于我们的读取器和写入器都是同一个 socket,因此需要对其进行两次可变借用,这明显违背了 Rust 的借用规则。 152 | 153 | ##### 分离读写器 154 | 显然,使用同一个 socket 是不行的,为了实现目标功能,必须将 `socket` 分离成一个读取器和写入器。 155 | 156 | 任何一个读写器( reader + writer )都可以使用 `io::split` 方法进行分离,最终返回一个读取器和写入器,这两者可以独自的使用,例如可以放入不同的任务中。 157 | 158 | 例如,我们的回声客户端可以这样实现,以实现同时并发读写: 159 | ```rust 160 | use tokio::io::{self, AsyncReadExt, AsyncWriteExt}; 161 | use tokio::net::TcpStream; 162 | 163 | #[tokio::main] 164 | async fn main() -> io::Result<()> { 165 | let socket = TcpStream::connect("127.0.0.1:6142").await?; 166 | let (mut rd, mut wr) = io::split(socket); 167 | 168 | // 创建异步任务,在后台写入数据 169 | tokio::spawn(async move { 170 | wr.write_all(b"hello\r\n").await?; 171 | wr.write_all(b"world\r\n").await?; 172 | 173 | // 有时,我们需要给予 Rust 一些类型暗示,它才能正确的推导出类型 174 | Ok::<_, io::Error>(()) 175 | }); 176 | 177 | let mut buf = vec![0; 128]; 178 | 179 | loop { 180 | let n = rd.read(&mut buf).await?; 181 | 182 | if n == 0 { 183 | break; 184 | } 185 | 186 | println!("GOT {:?}", &buf[..n]); 187 | } 188 | 189 | Ok(()) 190 | } 191 | ``` 192 | 193 | 实际上,`io::split` 可以用于任何同时实现了 `AsyncRead` 和 `AsyncWrite` 的值,它的内部使用了 `Arc` 和 `Mutex` 来实现相应的功能。如果大家觉得这种实现有些重,可以使用 Tokio 提供的 `TcpStream`,它提供了两种方式进行分离: 194 | 195 | - [`TcpStream::split`](https://docs.rs/tokio/1.15.0/tokio/net/struct.TcpStream.html#method.split)会获取字节流的引用,然后将其分离成一个读取器和写入器。但由于使用了引用的方式,它们俩必须和 `split` 在同一个任务中。 优点就是,这种实现没有性能开销,因为无需 `Arc` 和 `Mutex`。 196 | - [`TcpStream::into_split`](https://docs.rs/tokio/1.15.0/tokio/net/struct.TcpStream.html#method.into_split)还提供了一种分离实现,分离出来的结果可以在任务间移动,内部是通过 `Arc` 实现 197 | 198 | 199 | 再来分析下我们的使用场景,由于 `io::copy()` 调用时所在的任务和 `split` 所在的任务是同一个,因此可以使用性能最高的 `TcpStream::split`: 200 | ```rust 201 | tokio::spawn(async move { 202 | let (mut rd, mut wr) = socket.split(); 203 | 204 | if io::copy(&mut rd, &mut wr).await.is_err() { 205 | eprintln!("failed to copy"); 206 | } 207 | }); 208 | ``` 209 | 210 | 使用 `io::copy` 实现的完整代码见[此处](https://github.com/tokio-rs/website/blob/master/tutorial-code/io/src/echo-server-copy.rs)。 211 | 212 | #### 手动拷贝 213 | 程序员往往拥有一颗手动干翻一切的心,因此如果你不想用 `io::copy` 来简单实现,还可以自己手动去拷贝数据: 214 | ```rust 215 | use tokio::io::{self, AsyncReadExt, AsyncWriteExt}; 216 | use tokio::net::TcpListener; 217 | 218 | #[tokio::main] 219 | async fn main() -> io::Result<()> { 220 | let listener = TcpListener::bind("127.0.0.1:6142").await?; 221 | 222 | loop { 223 | let (mut socket, _) = listener.accept().await?; 224 | 225 | tokio::spawn(async move { 226 | let mut buf = vec![0; 1024]; 227 | 228 | loop { 229 | match socket.read(&mut buf).await { 230 | // 返回值 `Ok(0)` 说明对端已经关闭 231 | Ok(0) => return, 232 | Ok(n) => { 233 | // Copy the data back to socket 234 | // 将数据拷贝回 socket 中 235 | if socket.write_all(&buf[..n]).await.is_err() { 236 | // 非预期错误,由于我们这里无需再做什么,因此直接停止处理 237 | return; 238 | } 239 | } 240 | Err(_) => { 241 | // 非预期错误,由于我们无需再做什么,因此直接停止处理 242 | return; 243 | } 244 | } 245 | } 246 | }); 247 | } 248 | } 249 | ``` 250 | 251 | 建议这段代码放入一个和之前 `io::copy` 不同的文件中 `src/bin/echo-server.rs` , 然后使用 `cargo run --bin echo-server` 运行。 252 | 253 | 下面一起来看看这段代码有哪些值得注意的地方。首先,由于使用了 `write_all` 和 `read` 方法,需要先将对应的特征引入到当前作用域内: 254 | ```rust 255 | use tokio::io::{self, AsyncReadExt, AsyncWriteExt}; 256 | ``` 257 | 258 | ##### 在堆上分配缓冲区 259 | 在上面代码中,我们需要将数据从 `socket` 中读取到一个缓冲区 `buffer` 中: 260 | ```rust 261 | let mut buf = vec![0; 1024]; 262 | ``` 263 | 264 | 可以看到,此处的缓冲区是一个 `Vec` 动态数组,它的数据是存储在堆上,而不是栈上(若改成 `let mut buf = [0; 1024];`,则存储在栈上)。 265 | 266 | 在之前,我们提到过一个数据如果想在 `.await` 调用过程中存在,那它必须存储在当前任务内。在我们的代码中,`buf` 会在 `.await` 调用过程中被使用,因此它必须要存储在任务内。 267 | 268 | 若该缓冲区数组创建在栈上,那每条连接所对应的任务的内部数据结构看上去可能如下所示: 269 | ```rust 270 | struct Task { 271 | task: enum { 272 | AwaitingRead { 273 | socket: TcpStream, 274 | buf: [BufferType], 275 | }, 276 | AwaitingWriteAll { 277 | socket: TcpStream, 278 | buf: [BufferType], 279 | } 280 | 281 | } 282 | } 283 | ``` 284 | 285 | 可以看到,栈数组要被使用,就必须存储在相应的结构体内,其中两个结构体分别持有了不同的栈数组 `[BufferType]`,这种方式会导致任务结构变得很大。特别地,我们选择缓冲区长度往往会使用分页长度(page size),因此使用栈数组会导致任务的内存大小变得很奇怪甚至糟糕:`$page-size + 一些额外的字节`。 286 | 287 | 当然,编译器会帮助我们做一些优化。例如,会进一步优化 `async` 语句块的布局,而不是像上面一样简单的使用 `enum`。在实践中,变量也不会在枚举成员间移动。 288 | 289 | 但是再怎么优化,任务的结构体至少也会跟其中的栈数组一样大,因此通常情况下,使用堆上的缓冲区会高效实用的多。 290 | 291 | > 当任务因为调度在线程间移动时,存储在栈上的数据需要进行保存和恢复,过大的栈上变量会带来不小的数据拷贝开销 292 | > 293 | > 因此,存储大量数据的变量最好放到堆上 294 | 295 | ##### 处理EOF 296 | 当 TCP 连接的读取端关闭后,再调用 `read` 方法会返回 `Ok(0)`。此时,再继续下去已经没有意义,因此我们需要退出循环。忘记在 EOF 时退出读取循环,是网络编程中一个常见的 bug : 297 | ```rust 298 | loop { 299 | match socket.read(&mut buf).await { 300 | Ok(0) => return, 301 | // ... 其余错误处理 302 | } 303 | } 304 | ``` 305 | 306 | 大家不妨深入思考下,如果没有退出循环会怎么样?之前我们提到过,一旦读取端关闭后,那后面的 `read` 调用就会立即返回 `Ok(0)`,而不会阻塞等待,因此这种无阻塞循环会最终导致 CPU 立刻跑到 100% ,并将一直持续下去,直到程序关闭。 -------------------------------------------------------------------------------- /src/overview.md: -------------------------------------------------------------------------------- 1 | # tokio概览 2 | 3 | > 本教程在内容上大量借鉴和翻译了tokio官方文档[Tokio Tutorial](https://tokio.rs/tokio/tutorial), 但是重新组织了内容形式并融入了很多自己的见解和感悟,给大家提供更好的可读性和知识扩展性 4 | 5 | 对于 Async Rust,最最重要的莫过于底层的异步运行时,它提供了执行器、任务调度、异步API等核心服务。简单来说,使用 Rust 提供的 `async/.await` 特性编写的异步代码要运行起来,就必须依赖于异步运行时,否则这些代码将毫无用处。 6 | 7 | ## 异步运行时 8 | Rust 语言本身只提供了异步编程所需的基本特性,例如 `async/.await` 关键字,标准库中的 `Future` 特征,官方提供的 `futures` 实用库,这些特性单独使用没有任何用处,因此我们需要一个运行时来将这些特性实现的代码运行起来。 9 | 10 | 异步运行时是由 Rust 社区提供的,它们的核心是一个 `reactor` 和一个或多个 `executor`(执行器): 11 | 12 | - `reactor` 用于提供外部事件的订阅机制,例如 `I/O` 、进程间通信、定时器等 13 | - `executor` 在上一章我们有过深入介绍,它用于调度和执行相应的任务( `Future` ) 14 | 15 | 目前最受欢迎的几个运行时有: 16 | 17 | - [`tokio`](https://github.com/tokio-rs/tokio),目前最受欢迎的异步运行时,功能强大,还提供了异步所需的各种工具(例如 tracing )、网络协议框架(例如 HTTP,gRPC )等等 18 | - [`async-std`](https://github.com/async-rs/async-std),最大的优点就是跟标准库兼容性较强 19 | - [`smol`](https://github.com/smol-rs/smol), 一个小巧的异步运行时 20 | 21 | 但是,大浪淘沙,留下的才是金子,随着时间的流逝,`tokio`越来越亮眼,无论是性能、功能还是社区、文档,它在各个方面都异常优秀,时至今日,可以说已成为事实上的标准。 22 | 23 | #### 异步运行时的兼容性 24 | 为何选择异步运行时这么重要?不仅仅是它们在功能、性能上存在区别,更重要的是当你选择了一个,往往就无法切换到另外一个,除非异步代码很少。 25 | 26 | 使用异步运行时,往往伴随着对它相关的生态系统的深入使用,因此耦合性会越来越强,直至最后你很难切换到另一个运行时,例如 `tokio` 和 `async-std` ,就存在这种问题。 27 | 28 | 如果你实在有这种需求,可以考虑使用 [`async-compat`](https://github.com/smol-rs/async-compat),该包提供了一个中间层,用于兼容 `tokio` 和其它运行时。 29 | 30 | #### 结论 31 | 相信大家看到现在,心中应该有一个结论了。首先,运行时之间的不兼容性,让我们必须提前选择一个运行时,并且在未来坚持用下去,那这个运行时就应该是最优秀、最成熟的那个,`tokio` 几乎成了不二选择,当然 `tokio` 也有自己的问题:更难上手和运行时之间的兼容性。 32 | 33 | 如果你只用 `tokio` ,那兼容性自然不是问题,至于难以上手,Rust 这么难,我们都学到现在了,何况区区一个异步运行时,在本书的帮忙下,这些都不再是个问题:) 34 | 35 | ## tokio简介 36 | tokio是一个纸醉金迷之地,只要有钱就可以为所欲为,哦,抱歉,走错片场了。`tokio` 是 Rust 最优秀的异步运行时框架,它提供了写异步网络服务所需的几乎所有功能,不仅仅适用于大型服务器,还适用于小型嵌入式设备,它主要由以下组件构成: 37 | 38 | - 多线程版本的异步运行时,可以运行使用 `async/.await` 编写的代码 39 | - 标准库中阻塞API的异步版本,例如`thread::sleep`会阻塞当前线程,`tokio`中就提供了相应的异步实现版本 40 | - 构建异步编程所需的生态,甚至还提供了 [`tracing`](https://github.com/tokio-rs/tracing) 用于日志和分布式追踪, 提供 [`console`](https://github.com/tokio-rs/console) 用于 Debug 异步编程 41 | 42 | ### 优势 43 | 下面一起来看看使用 `tokio` 能给你提供哪些优势。 44 | 45 | **高性能** 46 | 47 | 因为快所以快,前者是 Rust 快,后者是 `tokio` 快。 `tokio` 在编写时充分利用了 Rust 提供的各种零成本抽象和高性能特性,而且贯彻了 Rust 的牛逼思想:如果你选择手写代码,那么最好的结果就是跟 `tokio` 一样快! 48 | 49 | 以下是一张官方提供的性能参考图,大致能体现出 `tokio` 的性能之恐怖: 50 | tokio performance 51 | 52 | **高可靠** 53 | 54 | Rust 语言的安全可靠性顺理成章的影响了 `tokio` 的可靠性,曾经有一个调查给出了令人乍舌的[结论](https://www.zdnet.com/article/microsoft-70-percent-of-all-security-bugs-are-memory-safety-issues/):软件系统70%的高危漏洞都是由内存不安全性导致的。 55 | 56 | 在 Rust 提供的安全性之外,`tokio` 还致力于提供一致性的行为表现:无论你何时运行系统,它的预期表现和性能都是一致的,例如不会出现莫名其妙的请求延迟或响应时间大幅增加。 57 | 58 | **简单易用** 59 | 60 | 通过 Rust 提供的 `async/await` 特性,编写异步程序的复杂性相比当初已经大幅降低,同时 `tokio` 还为我们提供了丰富的生态,进一步大幅降低了其复杂性。 61 | 62 | 同时 `tokio` 遵循了标准库的命名规则,让熟悉标准库的用户可以很快习惯于 `tokio` 的语法,再借助于 Rust 强大的类型系统,用户可以轻松地编写和交付正确的代码。 63 | 64 | **使用灵活性** 65 | 66 | `tokio` 支持你灵活的定制自己想要的运行时,例如你可以选择多线程 + 任务盗取模式的复杂运行时,也可以选择单线程的轻量级运行时。总之,几乎你的每一种需求在 `tokio` 中都能寻找到支持(画外音:强大的灵活性需要一定的复杂性来换取,并不是免费的午餐)。 67 | 68 | ### 劣势 69 | 虽然 `tokio` 对于大多数需要并发的项目都是非常适合的,但是确实有一些场景它并不适合使用: 70 | 71 | - 并行运行CPU密集型的任务,`tokio` 非常适合于IO密集型任务,这些IO任务的绝大多数时间都用于阻塞等待IO的结果,而不是刷刷刷的单烤CPU。如果你的应用是CPU密集型(例如并行计算),建议使用 [`rayon`](https://github.com/rayon-rs/rayon),当然,对于其中的IO任务部分,你依然可以混用 `tokio` 72 | - 读取大量的文件, 读取文件的瓶颈主要在于操作系统,因为OS没有提供异步文件读取接口,大量的并发并不会提升文件读取的并行性能,反而可能会造成不可忽视的性能损耗,因此建议使用线程(或线程池)的方式 73 | - 发送HTTP请求,`tokio` 的优势是给予你并发处理大量任务的能力,对于这种轻量级 HTTP 请求场景,`tokio` 除了增加你的代码复杂性,并无法带来什么额外的优势。因此,对于这种场景,你可以使用 [`reqwest`](https://github.com/seanmonstar/reqwest) 库,它会更加简单易用。 74 | 75 | 76 | ## 总结 77 | 离开三方开源社区提供的异步运行时, `async/await` 什么都不是,甚至还不如一堆破铜烂铁,除非你选择根据自己的需求手撸一个。 78 | 79 | 而 `tokio` 就是那颗皇冠上的夜明珠,也是值得我们投入时间去深入学习的开源库,它的设计原理和代码实现都异常优秀,在之后的章节中,我们将对其进行深入学习和剖析,敬请期待。 -------------------------------------------------------------------------------- /src/select.md: -------------------------------------------------------------------------------- 1 | # select! 2 | 在实际使用时,一个重要的场景就是同时等待多个异步操作的结果,并且对其结果进行进一步处理,在本章节,我们来看看,强大的 `select!` 是如何帮助咱们更好的控制多个异步操作并发执行的。 3 | 4 | ## tokio::select! 5 | `select!` 允许同时等待多个计算操作,然后当其中一个操作完成时就退出等待: 6 | ```rust 7 | use tokio::sync::oneshot; 8 | 9 | #[tokio::main] 10 | async fn main() { 11 | let (tx1, rx1) = oneshot::channel(); 12 | let (tx2, rx2) = oneshot::channel(); 13 | 14 | tokio::spawn(async { 15 | let _ = tx1.send("one"); 16 | }); 17 | 18 | tokio::spawn(async { 19 | let _ = tx2.send("two"); 20 | }); 21 | 22 | tokio::select! { 23 | val = rx1 => { 24 | println!("rx1 completed first with {:?}", val); 25 | } 26 | val = rx2 => { 27 | println!("rx2 completed first with {:?}", val); 28 | } 29 | } 30 | 31 | // 任何一个 select 分支结束后,都会继续执行接下来的代码 32 | } 33 | ``` 34 | 35 | 这里用到了两个 `oneshot` 消息通道,虽然两个操作的创建在代码上有先后顺序,但在实际执行时却不这样。因此, `select` 在从两个通道**阻塞等待**接收消息时,`rx1` 和 `rx2` 都有可能被先打印出来。 36 | 37 | 需要注意,任何一个 `select` 分支完成后,都会继续执行后面的代码,没被执行的分支会被丢弃( `dropped` )。 38 | 39 | #### 取消 40 | 对于 `Async Rust` 来说,释放( drop )掉一个 `Future` 就意味着取消任务。从上一章节可以得知, `async` 操作会返回一个 `Future`,而后者是惰性的,直到被 `poll` 调用时,才会被执行。一旦 `Future` 被释放,那操作将无法继续,因为所有相关的状态都被释放。 41 | 42 | 对于 Tokio 的 `oneshot` 的接收端来说,它在被释放时会发送一个关闭通知到发送端,因此发送端可以通过释放任务的方式来终止正在执行的任务。 43 | 44 | ```rust 45 | use tokio::sync::oneshot; 46 | 47 | async fn some_operation() -> String { 48 | // 在这里执行一些操作... 49 | } 50 | 51 | #[tokio::main] 52 | async fn main() { 53 | let (mut tx1, rx1) = oneshot::channel(); 54 | let (tx2, rx2) = oneshot::channel(); 55 | 56 | tokio::spawn(async { 57 | // 等待 `some_operation` 的完成 58 | // 或者处理 `oneshot` 的关闭通知 59 | tokio::select! { 60 | val = some_operation() => { 61 | let _ = tx1.send(val); 62 | } 63 | _ = tx1.closed() => { 64 | // 收到了发送端发来的关闭信号 65 | // `select` 即将结束,此时,正在进行的 `some_operation()` 任务会被取消,任务自动完成, 66 | // tx1 被释放 67 | } 68 | } 69 | }); 70 | 71 | tokio::spawn(async { 72 | let _ = tx2.send("two"); 73 | }); 74 | 75 | tokio::select! { 76 | val = rx1 => { 77 | println!("rx1 completed first with {:?}", val); 78 | } 79 | val = rx2 => { 80 | println!("rx2 completed first with {:?}", val); 81 | } 82 | } 83 | } 84 | ``` 85 | 86 | 上面代码的重点就在于 `tx1.closed` 所在的分支,一旦发送端被关闭,那该分支就会被执行,然后 `select` 会退出,并清理掉还没执行的第一个分支 `val = some_operation()` ,这其中 `some_operation` 返回的 `Future` 也会被清理,根据之前的内容,`Future` 被清理那相应的任务会立即取消,因此 `some_operation` 会被取消,不再执行。 87 | 88 | #### Future 的实现 89 | 为了更好的理解 `select` 的工作原理,我们来看看如果使用 `Future` 该如何实现。当然,这里是一个简化版本,在实际中,`select!` 会包含一些额外的功能,例如一开始会随机选择一个分支进行 `poll`。 90 | 91 | ```rust 92 | use tokio::sync::oneshot; 93 | use std::future::Future; 94 | use std::pin::Pin; 95 | use std::task::{Context, Poll}; 96 | 97 | struct MySelect { 98 | rx1: oneshot::Receiver<&'static str>, 99 | rx2: oneshot::Receiver<&'static str>, 100 | } 101 | 102 | impl Future for MySelect { 103 | type Output = (); 104 | 105 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { 106 | if let Poll::Ready(val) = Pin::new(&mut self.rx1).poll(cx) { 107 | println!("rx1 completed first with {:?}", val); 108 | return Poll::Ready(()); 109 | } 110 | 111 | if let Poll::Ready(val) = Pin::new(&mut self.rx2).poll(cx) { 112 | println!("rx2 completed first with {:?}", val); 113 | return Poll::Ready(()); 114 | } 115 | 116 | Poll::Pending 117 | } 118 | } 119 | 120 | #[tokio::main] 121 | async fn main() { 122 | let (tx1, rx1) = oneshot::channel(); 123 | let (tx2, rx2) = oneshot::channel(); 124 | 125 | // 使用 tx1 和 tx2 126 | 127 | MySelect { 128 | rx1, 129 | rx2, 130 | }.await; 131 | } 132 | ``` 133 | 134 | `MySelect` 包含了两个分支中的 `Future`,当它被 `poll` 时,第一个分支会先执行。如果执行完成,那取出的值会被使用,然后 `MySelect` 也随之结束。而另一个分支对应的 `Future` 会被释放掉,对应的操作也会被取消。 135 | 136 | 还记得上一章节中很重要的一段话吗? 137 | 138 | > 当一个 `Future` 返回 `Poll::Pending` 时,它必须确保会在某一个时刻通过 `Waker` 来唤醒,不然该 `Future` 将永远地被挂起 139 | 140 | 但是仔细观察我们之前的代码,里面并没有任何的 `wake` 调用!事实上,这是因为参数 `cx` 被传入了内层的 `poll` 调用。 只要内部的 `Future` 实现了唤醒并且返回了 `Poll::Pending`,那 `MySelect` 也等于实现了唤醒! 141 | 142 | 143 | ## 语法 144 | 目前来说,`select!` 最多可以支持 64 个分支,每个分支形式如下: 145 | ```rust 146 | <模式> = => <结果处理>, 147 | ``` 148 | 149 | 当 `select` 宏开始执行后,所有的分支会开始并发的执行。当任何一个**表达式**完成时,会将结果跟**模式**进行匹配。若匹配成功,则剩下的表达式会被释放。 150 | 151 | 最常用的**模式**就是用变量名去匹配表达式返回的值,然后该变量就可以在**结果处理**环节使用。 152 | 153 | 如果当前的模式不能匹配,剩余的 `async` 表达式将继续并发的执行,直到下一个完成。 154 | 155 | 由于 `select!` 使用的是一个 `async` 表达式,因此我们可以定义一些更复杂的计算。 156 | 157 | 例如从在分支中进行 TCP 连接: 158 | ```rust 159 | use tokio::net::TcpStream; 160 | use tokio::sync::oneshot; 161 | 162 | #[tokio::main] 163 | async fn main() { 164 | let (tx, rx) = oneshot::channel(); 165 | 166 | // 生成一个任务,用于向 oneshot 发送一条消息 167 | tokio::spawn(async move { 168 | tx.send("done").unwrap(); 169 | }); 170 | 171 | tokio::select! { 172 | socket = TcpStream::connect("localhost:3465") => { 173 | println!("Socket connected {:?}", socket); 174 | } 175 | msg = rx => { 176 | println!("received message first {:?}", msg); 177 | } 178 | } 179 | } 180 | ``` 181 | 182 | 再比如,在分支中进行 TCP 监听: 183 | ```rust 184 | use tokio::net::TcpListener; 185 | use tokio::sync::oneshot; 186 | use std::io; 187 | 188 | #[tokio::main] 189 | async fn main() -> io::Result<()> { 190 | let (tx, rx) = oneshot::channel(); 191 | 192 | tokio::spawn(async move { 193 | tx.send(()).unwrap(); 194 | }); 195 | 196 | let mut listener = TcpListener::bind("localhost:3465").await?; 197 | 198 | tokio::select! { 199 | _ = async { 200 | loop { 201 | let (socket, _) = listener.accept().await?; 202 | tokio::spawn(async move { process(socket) }); 203 | } 204 | 205 | // 给予 Rust 类型暗示 206 | Ok::<_, io::Error>(()) 207 | } => {} 208 | _ = rx => { 209 | println!("terminating accept loop"); 210 | } 211 | } 212 | 213 | Ok(()) 214 | } 215 | ``` 216 | 217 | 分支中接收连接的循环会一直运行,直到遇到错误才停止,或者当 `rx` 中有值时,也会停止。 `_` 表示我们并不关心这个值,这样使用唯一的目的就是为了结束第一分支中的循环。 218 | 219 | ## 返回值 220 | `select!` 还能返回一个值: 221 | ```rust 222 | async fn computation1() -> String { 223 | // .. 计算 224 | } 225 | 226 | async fn computation2() -> String { 227 | // .. 计算 228 | } 229 | 230 | #[tokio::main] 231 | async fn main() { 232 | let out = tokio::select! { 233 | res1 = computation1() => res1, 234 | res2 = computation2() => res2, 235 | }; 236 | 237 | println!("Got = {}", out); 238 | } 239 | ``` 240 | 241 | 需要注意的是,此时 `select!` 的所有分支必须返回一样的类型,否则编译器会报错! 242 | 243 | ## 错误传播 244 | 在 Rust 中使用 `?` 可以对错误进行传播,但是在 `select!` 中,`?` 如何工作取决于它是在分支中的 `async` 表达式使用还是在结果处理的代码中使用: 245 | 246 | - 在分支中 `async` 表达式使用会将该表达式的结果变成一个 `Result` 247 | - 在结果处理中使用,会将错误直接传播到 `select!` 之外 248 | 249 | ```rust 250 | use tokio::net::TcpListener; 251 | use tokio::sync::oneshot; 252 | use std::io; 253 | 254 | #[tokio::main] 255 | async fn main() -> io::Result<()> { 256 | // [设置 `rx` oneshot 消息通道] 257 | 258 | let listener = TcpListener::bind("localhost:3465").await?; 259 | 260 | tokio::select! { 261 | res = async { 262 | loop { 263 | let (socket, _) = listener.accept().await?; 264 | tokio::spawn(async move { process(socket) }); 265 | } 266 | 267 | Ok::<_, io::Error>(()) 268 | } => { 269 | res?; 270 | } 271 | _ = rx => { 272 | println!("terminating accept loop"); 273 | } 274 | } 275 | 276 | Ok(()) 277 | } 278 | ``` 279 | 280 | `listener.accept().await?` 是分支表达式中的 `?`,因此它会将表达式的返回值变成 `Result` 类型,然后赋予给 `res` 变量。 281 | 282 | 与之不同的是,结果处理中的 `res?;` 会让 `main` 函数直接结束并返回一个 `Result`,可以看出,这里 `?` 的用法跟我们平时的用法并无区别。 283 | 284 | 285 | ## 模式匹配 286 | 既然是模式匹配,我们需要再来回忆下 `select!` 的分支语法形式: 287 | ```rust 288 | <模式> = => <结果处理>, 289 | ``` 290 | 291 | 迄今为止,我们只用了变量绑定的模式,事实上,[任何 Rust 模式](https://course.rs/basic/match-pattern/all-patterns.html)都可以在此处使用。 292 | 293 | ```rust 294 | use tokio::sync::mpsc; 295 | 296 | #[tokio::main] 297 | async fn main() { 298 | let (mut tx1, mut rx1) = mpsc::channel(128); 299 | let (mut tx2, mut rx2) = mpsc::channel(128); 300 | 301 | tokio::spawn(async move { 302 | // 用 tx1 和 tx2 干一些不为人知的事 303 | }); 304 | 305 | tokio::select! { 306 | Some(v) = rx1.recv() => { 307 | println!("Got {:?} from rx1", v); 308 | } 309 | Some(v) = rx2.recv() => { 310 | println!("Got {:?} from rx2", v); 311 | } 312 | else => { 313 | println!("Both channels closed"); 314 | } 315 | } 316 | } 317 | ``` 318 | 319 | 上面代码中,`rx` 通道关闭后,`recv()` 方法会返回一个 `None`,可以看到没有任何模式能够匹配这个 `None`,那为何不会报错?秘密就在于 `else` 上:当使用模式去匹配分支时,若之前的所有分支都无法被匹配,那 `else` 分支将被执行。 320 | 321 | ## 借用 322 | 当在 Tokio 中生成( spawn )任务时,其 async 语句块必须拥有其中数据的所有权。而 `select!` 并没有这个限制,它的每个分支表达式可以直接借用数据,然后进行并发操作。只要遵循 Rust 的借用规则,多个分支表达式可以不可变的借用同一个数据,或者在一个表达式可变的借用某个数据。 323 | 324 | 来看个例子,在这里我们同时向两个 TCP 目标发送同样的数据: 325 | ```rust 326 | use tokio::io::AsyncWriteExt; 327 | use tokio::net::TcpStream; 328 | use std::io; 329 | use std::net::SocketAddr; 330 | 331 | async fn race( 332 | data: &[u8], 333 | addr1: SocketAddr, 334 | addr2: SocketAddr 335 | ) -> io::Result<()> { 336 | tokio::select! { 337 | Ok(_) = async { 338 | let mut socket = TcpStream::connect(addr1).await?; 339 | socket.write_all(data).await?; 340 | Ok::<_, io::Error>(()) 341 | } => {} 342 | Ok(_) = async { 343 | let mut socket = TcpStream::connect(addr2).await?; 344 | socket.write_all(data).await?; 345 | Ok::<_, io::Error>(()) 346 | } => {} 347 | else => {} 348 | }; 349 | 350 | Ok(()) 351 | } 352 | ``` 353 | 354 | 这里其实有一个很有趣的题外话,由于 TCP 连接过程是在模式中发生的,因此当某一个连接过程失败后,它通过 `?` 返回的 `Err` 类型并无法匹配 `Ok`,因此另一个分支会继续被执行,继续连接。 355 | 356 | 如果你把连接过程放在了结果处理中,那连接失败会直接从 `race` 函数中返回,而不是继续执行另一个分支中的连接! 357 | 358 | 还有一个非常重要的点,**借用规则在分支表达式和结果处理中存在很大的不同**。例如上面代码中,我们在两个分支表达式中分别对 `data` 做了不可变借用,这当然ok,但是若是两次可变借用,那编译器会立即进行报错。但是转折来了:当在结果处理中进行两次可变借用时,却不会报错,大家可以思考下为什么,提示下:思考下分支在执行完成后会发生什么? 359 | 360 | ```rust 361 | use tokio::sync::oneshot; 362 | 363 | #[tokio::main] 364 | async fn main() { 365 | let (tx1, rx1) = oneshot::channel(); 366 | let (tx2, rx2) = oneshot::channel(); 367 | 368 | let mut out = String::new(); 369 | 370 | tokio::spawn(async move { 371 | }); 372 | 373 | tokio::select! { 374 | _ = rx1 => { 375 | out.push_str("rx1 completed"); 376 | } 377 | _ = rx2 => { 378 | out.push_str("rx2 completed"); 379 | } 380 | } 381 | 382 | println!("{}", out); 383 | } 384 | ``` 385 | 386 | 例如以上代码,就在两个分支的结果处理中分别进行了可变借用,并不会报错。原因就在于:`select!`会保证只有一个分支的结果处理会被运行,然后在运行结束后,另一个分支会被直接丢弃。 387 | 388 | ## 循环 389 | 来看看该如何在循环中使用 `select!`,顺便说一句,跟循环一起使用是最常见的使用方式。 390 | ```rust 391 | use tokio::sync::mpsc; 392 | 393 | #[tokio::main] 394 | async fn main() { 395 | let (tx1, mut rx1) = mpsc::channel(128); 396 | let (tx2, mut rx2) = mpsc::channel(128); 397 | let (tx3, mut rx3) = mpsc::channel(128); 398 | 399 | loop { 400 | let msg = tokio::select! { 401 | Some(msg) = rx1.recv() => msg, 402 | Some(msg) = rx2.recv() => msg, 403 | Some(msg) = rx3.recv() => msg, 404 | else => { break } 405 | }; 406 | 407 | println!("Got {}", msg); 408 | } 409 | 410 | println!("All channels have been closed."); 411 | } 412 | ``` 413 | 414 | 在循环中使用 `select!` 最大的不同就是,当某一个分支执行完成后,`select!` 会继续循环等待并执行下一个分支,直到所有分支最终都完成,最终匹配到 `else` 分支,然后通过 `break` 跳出循环。 415 | 416 | 老生常谈的一句话:`select!` 中哪个分支先被执行是无法确定的,因此不要依赖于分支执行的顺序!想象一下,在异步编程场景,若 `select!` 按照分支的顺序来执行会如何:若 `rx1` 中总是有数据,那每次循环都只会去处理第一个分支,后面两个分支永远不会被执行。 417 | 418 | #### 恢复之前的异步操作 419 | ```rust 420 | async fn action() { 421 | // 一些异步逻辑 422 | } 423 | 424 | #[tokio::main] 425 | async fn main() { 426 | let (mut tx, mut rx) = tokio::sync::mpsc::channel(128); 427 | 428 | let operation = action(); 429 | tokio::pin!(operation); 430 | 431 | loop { 432 | tokio::select! { 433 | _ = &mut operation => break, 434 | Some(v) = rx.recv() => { 435 | if v % 2 == 0 { 436 | break; 437 | } 438 | } 439 | } 440 | } 441 | } 442 | ``` 443 | 444 | 在上面代码中,我们没有直接在 `select!` 分支中调用 `action()` ,而是在 `loop` 循环外面先将 `action()` 赋值给 `operation`,因此 `operation` 是一个 `Future`。 445 | 446 | **重点来了**,在 `select!` 循环中,我们使用了一个奇怪的语法 `&mut operation`,大家想象一下,如果不加 `&mut` 会如何?答案是,每一次循环调用的都是一次全新的 `action()`调用,但是当加了 `&mut operatoion` 后,每一次循环调用就变成了对同一次 `action()` 的调用。也就是我们实现了在每次循环中恢复了之前的异步操作! 447 | 448 | `select!` 的另一个分支从消息通道收取消息,一旦收到值是偶数,就跳出循环,否则就继续循环。 449 | 450 | 还有一个就是我们使用了 `tokio::pin!`,具体的细节这里先不介绍,值得注意的点是:如果要在一个引用上使用 `.await`,那么引用的值就必须是不能移动的或者实现了 `Unpin`,关于 `Pin` 和 `Unpin` 可以参见[这里](https://course.rs/async/pin-unpin.html)。 451 | 452 | 一旦移除 `tokio::pin!` 所在行的代码,然后试图编译,就会获得以下错误: 453 | ```console 454 | error[E0599]: no method named `poll` found for struct 455 | `std::pin::Pin<&mut &mut impl std::future::Future>` 456 | in the current scope 457 | --> src/main.rs:16:9 458 | | 459 | 16 | / tokio::select! { 460 | 17 | | _ = &mut operation => break, 461 | 18 | | Some(v) = rx.recv() => { 462 | 19 | | if v % 2 == 0 { 463 | ... | 464 | 22 | | } 465 | 23 | | } 466 | | |_________^ method not found in 467 | | `std::pin::Pin<&mut &mut impl std::future::Future>` 468 | | 469 | = note: the method `poll` exists but the following trait bounds 470 | were not satisfied: 471 | `impl std::future::Future: std::marker::Unpin` 472 | which is required by 473 | `&mut impl std::future::Future: std::future::Future` 474 | ``` 475 | 476 | 虽然我们已经学了很多关于 `Future` 的知识,但是这个错误依然不太好理解。但是它不难解决:当你试图在**一个引用上调用 `.await` 然后遇到了 `Future 未实现` 这种错误时**,往往只需要将对应的 `Future` 进行固定即可: ` tokio::pin!(operation);`。 477 | 478 | #### 修改一个分支 479 | 下面一起来看看一个稍微复杂一些的 `loop` 循环,首先,我们拥有: 480 | 481 | - 一个消息通道可以传递 `i32` 类型的值 482 | - 定义在 `i32` 值上的一个异步操作 483 | 484 | 想要实现的逻辑是: 485 | 486 | - 在消息通道中等待一个偶数出现 487 | - 使用该偶数作为输入来启动一个异步操作 488 | - 等待异步操作完成,与此同时监听消息通道以获取更多的偶数 489 | - 若在异步操作完成前一个新的偶数到来了,终止当前的异步操作,然后接着使用新的偶数开始异步操作 490 | 491 | ```rust 492 | async fn action(input: Option) -> Option { 493 | // 若 input(输入)是None,则返回 None 494 | // 事实上也可以这么写: `let i = input?;` 495 | let i = match input { 496 | Some(input) => input, 497 | None => return None, 498 | }; 499 | 500 | // 这里定义一些逻辑 501 | } 502 | 503 | #[tokio::main] 504 | async fn main() { 505 | let (mut tx, mut rx) = tokio::sync::mpsc::channel(128); 506 | 507 | let mut done = false; 508 | let operation = action(None); 509 | tokio::pin!(operation); 510 | 511 | tokio::spawn(async move { 512 | let _ = tx.send(1).await; 513 | let _ = tx.send(3).await; 514 | let _ = tx.send(2).await; 515 | }); 516 | 517 | loop { 518 | tokio::select! { 519 | res = &mut operation, if !done => { 520 | done = true; 521 | 522 | if let Some(v) = res { 523 | println!("GOT = {}", v); 524 | return; 525 | } 526 | } 527 | Some(v) = rx.recv() => { 528 | if v % 2 == 0 { 529 | // `.set` 是 `Pin` 上定义的方法 530 | operation.set(action(Some(v))); 531 | done = false; 532 | } 533 | } 534 | } 535 | } 536 | } 537 | ``` 538 | 539 | 当第一次循环开始时, 第一个分支会立即完成,因为 `operation` 的参数是 `None`。当第一个分支执行完成时,`done` 会变成 `true`,此时第一个分支的条件将无法被满足,开始执行第二个分支。 540 | 541 | 当第二个分支收到一个偶数时,`done` 会被修改为 `false`,且 `operation` 被设置了值。 此后再一次循环时,第一个分支会被执行,且 `operation` 返回一个 `Some(2)`,因此会触发 `return` ,最终结束循环并返回。 542 | 543 | 这段代码引入了一个新的语法: `if !done`,在解释之前,先看看去掉后会如何: 544 | ```console 545 | thread 'main' panicked at '`async fn` resumed after completion', src/main.rs:1:55 546 | note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 547 | ``` 548 | 549 | `async fn resumed after completion'` 错误的含义是:`async fn` 异步函数在完成后,依然被恢复了(继续使用)。 550 | 551 | 回到例子中来,这个错误是由于 `operation` 在它已经调用完成后依然被使用。通常来说,当使用 `.await` 后,调用 `.await` 的值会被消耗掉,因此并不存在这个问题。但是在这例子中,我们在引用上调用 `.await`,因此之后该引用依然可以被使用。 552 | 553 | 为了避免这个问题,需要在第一个分支的 `operation` 完成后禁止再使用该分支。这里的 `done` 的引入就很好的解决了问题。对于 `select!` 来说 `if !done` 的语法被称为预条件( **precondition** ),该条件会在分支被 `.await` 执行前进行检查。 554 | 555 | 那大家肯定有疑问了,既然 `operation` 不能再被调用了,我们该如何在有偶数值时,再回到第一个分支对其进行调用呢?答案就是 `operation.set(action(Some(v)));`,该操作会重新使用新的参数设置 `operation`。 556 | 557 | ## spawn 和 select! 的一些不同 558 | 学到现在,相信大家对于 `tokio::spawn` 和 `select!` 已经非常熟悉,它们的共同点就是都可以并发的运行异步操作。 559 | 然而它们使用的策略大相径庭。 560 | 561 | `tokio::spawn` 函数会启动新的任务来运行一个异步操作,每个任务都是一个独立的对象可以单独被 Tokio 调度运行,因此两个不同的任务的调度都是独立进行的,甚至于它们可能会运行在两个不同的操作系统线程上。鉴于此,生成的任务和生成的线程有一个相同的限制:不允许对外部环境中的值进行借用。 562 | 563 | 而 `select!` 宏就不一样了,它在同一个任务中并发运行所有的分支。正是因为这样,在同一个任务中,这些分支无法被同时运行。 `select!` 宏在单个任务中实现了多路复用的功能。 564 | -------------------------------------------------------------------------------- /src/shared-state.md: -------------------------------------------------------------------------------- 1 | # 共享状态 2 | 上一章节中,咱们搭建了一个异步的 redis 服务器,并成功的提供了服务,但是其隐藏了一个巨大的问题:状态(数据)无法在多个连接之间共享,下面一起来看看该如何解决。 3 | 4 | ## 解决方法 5 | 好在 Tokio 十分强大,上面问题对应的解决方法也不止一种: 6 | 7 | - 使用 `Mutex` 来保护数据的共享访问 8 | - 生成一个异步任务去管理状态,然后各个连接使用消息传递的方式与其进行交互 9 | 10 | 其中,第一种方法适合比较简单的数据,而第二种方法适用于需要异步工作的,例如 I/O 原语。由于我们使用的数据存储类型是 `HashMap`,使用到的相关操作是 `insert` 和 `get` ,又因为这两个操作都不是异步的,因此只要使用 `Mutex` 即可解决问题。 11 | 12 | 在上面的描述中,说实话第二种方法及其适用的场景并不是很好理解,但没关系,在后面章节会进行详细介绍。 13 | 14 | ## 添加 `bytes` 依赖包 15 | 在上一节中,我们使用 `Vec` 来保存目标数据,但是它有一个问题,对它进行克隆时会将底层数据也整个复制一份,效率很低,但是克隆操作对于我们在多连接间共享数据又是必不可少的。 16 | 17 | 因此这里咱们新引入一个 `bytes` 包,它包含一个 `Bytes` 类型,当对该类型的值进行克隆时,就不再会克隆底层数据。事实上,`Bytes` 是一个引用计数类型,跟 `Arc` 非常类似,或者准确的说,`Bytes` 就是基于 `Arc` 实现的,但相比后者`Bytes` 提供了一些额外的能力。 18 | 19 | 在 `Cargo.toml` 的 `[dependencies]` 中引入 `bytes` : 20 | ```console 21 | bytes = "1" 22 | ``` 23 | 24 | ## 初始化 HashMap 25 | 由于 `HashMap` 会在多个任务甚至多个线程间共享,再结合之前的选择,最终我们决定使用 `>>` 的方式对其进行包裹。 26 | 27 | 但是,大家先来畅想一下使用它进行包裹后的类型长什么样? 大概,可能,长这样:`Arc>>`,天哪噜,一不小心,你就遇到了 Rust 的阴暗面:类型大串烧。可以想象,如果要在代码中到处使用这样的类型,可读性会极速下降,因此我们需要一个[类型别名](https://course.rs/advance/custom-type.html#类型别名type-alias)( type alias )来简化下: 28 | ```rust 29 | use bytes::Bytes; 30 | use std::collections::HashMap; 31 | use std::sync::{Arc, Mutex}; 32 | 33 | type Db = Arc>>; 34 | ``` 35 | 36 | 此时,`Db` 就是一个类型别名,使用它就可以替代那一大串的东东,等下你就能看到功效。 37 | 38 | 接着,我们需要在 `main` 函数中对 `HashMap` 进行初始化,然后使用 `Arc` 克隆一份它的所有权并将其传入到生成的异步任务中。事实上在 Tokio 中,这里的 `Arc` 被称为 **handle**,或者更宽泛的说,`handle` 在 Tokio 中可以用来访问某个共享状态。 39 | 40 | ```rust 41 | use tokio::net::TcpListener; 42 | use std::collections::HashMap; 43 | use std::sync::{Arc, Mutex}; 44 | 45 | #[tokio::main] 46 | async fn main() { 47 | let listener = TcpListener::bind("127.0.0.1:6379").await.unwrap(); 48 | 49 | println!("Listening"); 50 | 51 | let db = Arc::new(Mutex::new(HashMap::new())); 52 | 53 | loop { 54 | let (socket, _) = listener.accept().await.unwrap(); 55 | // 将 handle 克隆一份 56 | let db = db.clone(); 57 | 58 | println!("Accepted"); 59 | tokio::spawn(async move { 60 | process(socket, db).await; 61 | }); 62 | } 63 | } 64 | ``` 65 | 66 | #### 为何使用 `std::sync::Mutex` 67 | 上面代码还有一点非常重要,那就是我们使用了 `std::sync::Mutex` 来保护 `HashMap`,而不是使用 `tokio::sync::Mutex`。 68 | 69 | 在使用 Tokio 编写异步代码时,一个常见的错误无条件地使用 `tokio::sync::Mutex` ,而真相是:Tokio 提供的异步锁只应该在跨多个 `.await`调用时使用,而且 Tokio 的 `Mutex` 实际上内部使用的也是 `std::sync::Mutex`。 70 | 71 | 多补充几句,在异步代码中,关于锁的使用有以下经验之谈: 72 | 73 | - 锁如果在多个 `.await` 过程中持有,应该使用 Tokio 提供的锁,原因是 `.await`的过程中锁可能在线程间转移,若使用标准库的同步锁存在死锁的可能性,例如某个任务刚获取完锁,还没使用完就因为 `.await` 让出了当前线程的所有权,结果下个任务又去获取了锁,造成死锁 74 | - 锁竞争不多的情况下,使用 `std::sync::Mutex` 75 | - 锁竞争多,可以考虑使用三方库提供的性能更高的锁,例如 [`parking_lot::Mutex`](https://docs.rs/parking_lot/0.10.2/parking_lot/type.Mutex.html) 76 | 77 | 78 | ## 更新 `process()` 79 | `process()` 函数不再初始化 `HashMap`,取而代之的是它使用了 `HashMap` 的一个 `handle` 作为参数: 80 | ```rust 81 | use tokio::net::TcpStream; 82 | use mini_redis::{Connection, Frame}; 83 | 84 | async fn process(socket: TcpStream, db: Db) { 85 | use mini_redis::Command::{self, Get, Set}; 86 | 87 | let mut connection = Connection::new(socket); 88 | 89 | while let Some(frame) = connection.read_frame().await.unwrap() { 90 | let response = match Command::from_frame(frame).unwrap() { 91 | Set(cmd) => { 92 | let mut db = db.lock().unwrap(); 93 | db.insert(cmd.key().to_string(), cmd.value().clone()); 94 | Frame::Simple("OK".to_string()) 95 | } 96 | Get(cmd) => { 97 | let db = db.lock().unwrap(); 98 | if let Some(value) = db.get(cmd.key()) { 99 | Frame::Bulk(value.clone()) 100 | } else { 101 | Frame::Null 102 | } 103 | } 104 | cmd => panic!("unimplemented {:?}", cmd), 105 | }; 106 | 107 | connection.write_frame(&response).await.unwrap(); 108 | } 109 | } 110 | ``` 111 | 112 | ## 任务、线程和锁竞争 113 | 当竞争不多的时候,使用阻塞性的锁去保护共享数据是一个正确的选择。当一个锁竞争触发后,当前正在执行任务(请求锁)的线程会被阻塞,并等待锁被前一个使用者释放。这里的关键就是:**锁竞争不仅仅会导致当前的任务被阻塞,还会导致执行任务的线程被阻塞,因此该线程准备执行的其它任务也会因此被阻塞!** 114 | 115 | 默认情况下,Tokio 调度器使用了多线程模式,此时如果有大量的任务都需要访问同一个锁,那么锁竞争将变得激烈起来。当然,你也可以使用 [**current_thread**](https://docs.rs/tokio/1.15.0/tokio/runtime/index.html#current-thread-scheduler) 运行时设置,在该设置下会使用一个单线程的调度器(执行器),所有的任务都会创建并执行在当前线程上,因此不再会有锁竞争。 116 | 117 | > current_thread 是一个轻量级、单线程的运行时,当任务数不多或连接数不多时是一个很好的选择。例如你想在一个异步客户端库的基础上提供给用户同步的API访问时,该模式就很适用 118 | 119 | 当同步锁的竞争变成一个问题时,使用 Tokio 提供的异步锁几乎并不能帮你解决问题,此时可以考虑如下选项: 120 | 121 | - 创建专门的任务并使用消息传递的方式来管理状态 122 | - 将锁进行分片 123 | - 重构代码以避免锁 124 | 125 | 在我们的例子中,由于每一个 `key` 都是独立的,因此对锁进行分片将成为一个不错的选择: 126 | ```rust 127 | type ShardedDb = Arc>>>>; 128 | 129 | fn new_sharded_db(num_shards: usize) -> ShardedDb { 130 | let mut db = Vec::with_capacity(num_shards); 131 | for _ in 0..num_shards { 132 | db.push(Mutex::new(HashMap::new())); 133 | } 134 | Arc::new(db) 135 | } 136 | ``` 137 | 138 | 在这里,我们创建了 N 个不同的存储实例,每个实例都会存储不同的分片数据,例如我们有`a-i`共9个不同的 `key`, 可以将存储分成3个实例,那么第一个实例可以存储 `a-c`,第二个`d-f`,以此类推。在这种情况下,访问 `b` 时,只需要锁住第一个实例,此时二、三实例依然可以正常访问,因此锁被成功的分片了。 139 | 140 | 在分片后,使用给定的 key 找到对应的值就变成了两个步骤:首先,使用 `key` 通过特定的算法寻找到对应的分片,然后再使用该 `key` 从分片中查询到值: 141 | ```rust 142 | let shard = db[hash(key) % db.len()].lock().unwrap(); 143 | shard.insert(key, value); 144 | ``` 145 | 146 | 这里我们使用 `hash` 算法来进行分片,但是该算法有个缺陷:分片的数量不能变,一旦变了后,那之前落入分片1 的`key`很可能将落入到其它分片中,最终全部乱掉。此时你可以考虑[dashmap](https://docs.rs/dashmap),它提供了更复杂、更精妙的支持分片的`hash map`。 147 | 148 | ## 在 `.await` 期间持有锁 149 | 在某些时候,你可能会不经意写下这种代码: 150 | ```rust 151 | use std::sync::{Mutex, MutexGuard}; 152 | 153 | async fn increment_and_do_stuff(mutex: &Mutex) { 154 | let mut lock: MutexGuard = mutex.lock().unwrap(); 155 | *lock += 1; 156 | 157 | do_something_async().await; 158 | } // 锁在这里超出作用域 159 | ``` 160 | 161 | 如果你要 `spawn` 一个任务来执行上面的函数的话,会报错: 162 | ```console 163 | error: future cannot be sent between threads safely 164 | --> src/lib.rs:13:5 165 | | 166 | 13 | tokio::spawn(async move { 167 | | ^^^^^^^^^^^^ future created by async block is not `Send` 168 | | 169 | ::: /playground/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-0.2.21/src/task/spawn.rs:127:21 170 | | 171 | 127 | T: Future + Send + 'static, 172 | | ---- required by this bound in `tokio::task::spawn::spawn` 173 | | 174 | = help: within `impl std::future::Future`, the trait `std::marker::Send` is not implemented for `std::sync::MutexGuard<'_, i32>` 175 | note: future is not `Send` as this value is used across an await 176 | --> src/lib.rs:7:5 177 | | 178 | 4 | let mut lock: MutexGuard = mutex.lock().unwrap(); 179 | | -------- has type `std::sync::MutexGuard<'_, i32>` which is not `Send` 180 | ... 181 | 7 | do_something_async().await; 182 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^ await occurs here, with `mut lock` maybe used later 183 | 8 | } 184 | | - `mut lock` is later dropped here 185 | ``` 186 | 187 | 错误的原因在于 `std::sync::MutexGuard` 类型并没有实现 `Send` 特征,这意味着你不能将一个 `Mutex` 锁发送到另一个线程,因为 `.await` 可能会让任务转移到另一个线程上执行,这个之前也介绍过。 188 | 189 | #### 提前释放锁 190 | 要解决这个问题,就必须重构代码,让 `Mutex` 锁在 `.await` 被调用前就被释放掉。 191 | ```rust 192 | // 下面的代码可以工作! 193 | async fn increment_and_do_stuff(mutex: &Mutex) { 194 | { 195 | let mut lock: MutexGuard = mutex.lock().unwrap(); 196 | *lock += 1; 197 | } // lock在这里超出作用域 (被释放) 198 | 199 | do_something_async().await; 200 | } 201 | ``` 202 | 203 | > 大家可能已经发现,很多错误都是因为 `.await` 引起的,其实你只要记住,在 `.await` 执行期间,任务可能会在线程间转移,那么这些错误将变得很好理解,不必去死记硬背 204 | 205 | 但是下面的代码不工作: 206 | ```rust 207 | use std::sync::{Mutex, MutexGuard}; 208 | 209 | async fn increment_and_do_stuff(mutex: &Mutex) { 210 | let mut lock: MutexGuard = mutex.lock().unwrap(); 211 | *lock += 1; 212 | drop(lock); 213 | 214 | do_something_async().await; 215 | } 216 | ``` 217 | 218 | 原因我们之前解释过,编译器在这里不够聪明,目前它只能根据作用域的范围来判断,`drop` 虽然释放了锁,但是锁的作用域依然会持续到函数的结束,未来也许编译器会改进,但是现在至少还是不行的。 219 | 220 | 聪明的读者此时的小脑袋已经飞速运转起来,既然锁没有实现 `Send`, 那我们主动给它实现如何?这样不就可以顺利运行了吗?答案依然是不可以,原因就是我们之前提到过的死锁,如果一个任务获取了锁,然后还没释放就在 `.await` 期间被挂起,接着开始执行另一个任务,这个任务又去获取锁,就会导致死锁。 221 | 222 | 再来看看其它解决方法: 223 | 224 | #### 重构代码:在 `.await` 期间不持有锁 225 | 之前的代码其实也是为了在 `.await` 期间不持有锁,但是我们还有更好的实现方式,例如,你可以把 `Mutex` 放入一个结构体中,并且只在该结构体的非异步方法中使用该锁: 226 | ```rust 227 | use std::sync::Mutex; 228 | 229 | struct CanIncrement { 230 | mutex: Mutex, 231 | } 232 | impl CanIncrement { 233 | // 该方法不是 `async` 234 | fn increment(&self) { 235 | let mut lock = self.mutex.lock().unwrap(); 236 | *lock += 1; 237 | } 238 | } 239 | 240 | async fn increment_and_do_stuff(can_incr: &CanIncrement) { 241 | can_incr.increment(); 242 | do_something_async().await; 243 | } 244 | ``` 245 | 246 | #### 使用异步任务和通过消息传递来管理状态 247 | 该方法常常用于共享的资源是 I/O 类型的资源时,我们在下一章节将详细介绍。 248 | 249 | #### 使用 Tokio 提供的异步锁 250 | Tokio 提供的锁最大的优点就是:它可以在 `.await` 执行期间被持有,而且不会有任何问题。但是代价就是,这种异步锁的性能开销会更高,因此如果可以,使用之前的两种方法来解决会更好。 251 | 252 | ```rust 253 | use tokio::sync::Mutex; // 注意,这里使用的是 Tokio 提供的锁 254 | 255 | // 下面的代码会编译 256 | // 但是就这个例子而言,之前的方式会更好 257 | async fn increment_and_do_stuff(mutex: &Mutex) { 258 | let mut lock = mutex.lock().await; 259 | *lock += 1; 260 | 261 | do_something_async().await; 262 | } // 锁在这里被释放 263 | ``` 264 | -------------------------------------------------------------------------------- /src/spawning.md: -------------------------------------------------------------------------------- 1 | # 创建异步任务 2 | 同志们,抓稳了,我们即将换挡提速,通向 `mini-redis` 服务端的高速之路已经开启。 3 | 4 | 不过在开始之前,先来做点收尾工作:上一章节中,我们实现了一个简易的 `mini-redis` 客户端并支持了 `SET`/`GET` 操作, 现在将该[代码](https://course.rs/tokio/getting-startted.html#分析未到代码先行)移动到 `example` 文件夹下,因为我们这个章节要实现的是服务器,后面可以用之前客户端示例对我们的服务器端进行测试: 5 | ```shell 6 | $ mkdir -p examples 7 | $ mv src/main.rs examples/hello-redis.rs 8 | ``` 9 | 10 | 然后再重新创建一个空的 `src/main.rs` 文件,至此换挡已经完成,提速正式开始。 11 | 12 | ## 接收 sockets 13 | 作为服务器端,最基础的工作无疑是接收外部进来的 TCP 连接,可以通过 `tokio::net::TcpListener` 来完成。 14 | 15 | > Tokio 中大多数类型的名称都和标准库中对应的同步类型名称相同,而且,如果没有特殊原因,Tokio 的 API 名称也和标准库保持一致,只不过用 `async fn` 取代 `fn` 来声明函数。 16 | 17 | `TcpListener` 监听 **6379** 端口,然后通过循环来接收外部进来的连接,每个连接在处理完后会被关闭。对于目前来说,我们的任务很简单:读取命令、打印到标准输出 `stdout`,最后回复给客户端一个错误。 18 | 19 | ```rust 20 | use tokio::net::{TcpListener, TcpStream}; 21 | use mini_redis::{Connection, Frame}; 22 | 23 | #[tokio::main] 24 | async fn main() { 25 | // Bind the listener to the address 26 | // 监听指定地址,等待 TCP 连接进来 27 | let listener = TcpListener::bind("127.0.0.1:6379").await.unwrap(); 28 | 29 | loop { 30 | // 第二个被忽略的项中包含有新连接的 `IP` 和端口信息 31 | let (socket, _) = listener.accept().await.unwrap(); 32 | process(socket).await; 33 | } 34 | } 35 | 36 | async fn process(socket: TcpStream) { 37 | // `Connection` 对于 redis 的读写进行了抽象封装,因此我们读到的是一个一个数据帧frame(数据帧 = redis命令 + 数据),而不是字节流 38 | // `Connection` 是在 mini-redis 中定义 39 | let mut connection = Connection::new(socket); 40 | 41 | if let Some(frame) = connection.read_frame().await.unwrap() { 42 | println!("GOT: {:?}", frame); 43 | 44 | // 回复一个错误 45 | let response = Frame::Error("unimplemented".to_string()); 46 | connection.write_frame(&response).await.unwrap(); 47 | } 48 | } 49 | ``` 50 | 51 | 现在运行我们的简单服务器 : 52 | ```shel 53 | cargo run 54 | ``` 55 | 56 | 此时服务器会处于循环等待以接收连接的状态,接下来在一个新的终端窗口中启动上一章节中的 `redis` 客户端,由于相关代码已经放入 `examples` 文件夹下,因此我们可以使用 `-- example` 来指定运行该客户端示例: 57 | ```shell 58 | $ cargo run --example hello-redis 59 | ``` 60 | 61 | 此时,客户端的输出是: `Error: "unimplemented"`, 同时服务器端打印出了客户端发来的由 **redis 命令和数据** 组成的数据帧: `GOT: Array([Bulk(b"set"), Bulk(b"hello"), Bulk(b"world")])`。 62 | 63 | ## 生成任务 64 | 上面的服务器,如果你仔细看,它其实一次只能接受和处理一条 TCP 连接,只有等当前的处理完并结束后,才能开始接收下一条连接。原因在于 `loop` 循环中的 `await` 会导致当前任务进入阻塞等待,也就是 `loop` 循环会被阻塞。 65 | 66 | 而这显然不是我们想要的,服务器能并发地处理多条连接的请求,才是正确的打开姿势,下面来看看如何实现真正的并发。 67 | 68 | > 关于并发和并行,在[多线程章节中](https://course.rs/advance/concurrency-with-threads/concurrency-parallelism.html)有详细的解释 69 | 70 | 为了并发的处理连接,需要为每一条进来的连接都生成( `spawn` )一个新的任务, 然后在该任务中处理连接: 71 | ```rust 72 | use tokio::net::TcpListener; 73 | 74 | #[tokio::main] 75 | async fn main() { 76 | let listener = TcpListener::bind("127.0.0.1:6379").await.unwrap(); 77 | 78 | loop { 79 | let (socket, _) = listener.accept().await.unwrap(); 80 | // 为每一条连接都生成一个新的任务, 81 | // `socket` 的所有权将被移动到新的任务中,并在那里进行处理 82 | tokio::spawn(async move { 83 | process(socket).await; 84 | }); 85 | } 86 | } 87 | ``` 88 | 89 | #### 任务 90 | 一个 Tokio 任务是一个异步的绿色线程,它们通过 `tokio::spawn` 进行创建,该函数会返回一个 `JoinHandle` 类型的句柄,调用者可以使用该句柄跟创建的任务进行交互。 91 | 92 | `spawn` 函数的参数是一个 `async` 语句块,该语句块甚至可以返回一个值,然后调用者可以通过 `JoinHandle` 句柄获取该值: 93 | ```rust 94 | #[tokio::main] 95 | async fn main() { 96 | let handle = tokio::spawn(async { 97 | 10086 98 | }); 99 | 100 | let out = handle.await.unwrap(); 101 | println!("GOT {}", out); 102 | } 103 | ``` 104 | 105 | 以上代码会打印出`GOT 10086`。实际上,上面代码中`.await` 会返回一个 `Result` ,若 `spawn` 创建的任务正常运行结束,则返回一个 `Ok(T)`的值,否则会返回一个错误 `Err`:例如任务内部发生了 `panic` 或任务因为运行时关闭被强制取消时。 106 | 107 | 任务是调度器管理的执行单元。`spawn`生成的任务会首先提交给调度器,然后由它负责调度执行。需要注意的是,执行任务的线程未必是创建任务的线程,任务完全有可能运行在另一个不同的线程上,而且任务在生成后,它还可能会在线程间被移动。 108 | 109 | 任务在 Tokio 中远比看上去要更轻量,例如创建一个任务仅仅需要一次64字节大小的内存分配。因此应用程序在生成任务上,完全不应该有任何心理负担,除非你在一台没那么好的机器上疯狂生成了几百万个任务。。。 110 | 111 | #### `'static` 约束 112 | 当使用 Tokio 创建一个任务时,该任务类型的生命周期必须是 `'static`。意味着,在任务中不能使用外部数据的引用: 113 | ```rust 114 | use tokio::task; 115 | 116 | #[tokio::main] 117 | async fn main() { 118 | let v = vec![1, 2, 3]; 119 | 120 | task::spawn(async { 121 | println!("Here's a vec: {:?}", v); 122 | }); 123 | } 124 | ``` 125 | 126 | 上面代码中,`spawn` 出的任务引用了外部环境中的变量 `v` ,导致以下报错: 127 | ```console 128 | error[E0373]: async block may outlive the current function, but 129 | it borrows `v`, which is owned by the current function 130 | --> src/main.rs:7:23 131 | | 132 | 7 | task::spawn(async { 133 | | _______________________^ 134 | 8 | | println!("Here's a vec: {:?}", v); 135 | | | - `v` is borrowed here 136 | 9 | | }); 137 | | |_____^ may outlive borrowed value `v` 138 | | 139 | note: function requires argument type to outlive `'static` 140 | --> src/main.rs:7:17 141 | | 142 | 7 | task::spawn(async { 143 | | _________________^ 144 | 8 | | println!("Here's a vector: {:?}", v); 145 | 9 | | }); 146 | | |_____^ 147 | help: to force the async block to take ownership of `v` (and any other 148 | referenced variables), use the `move` keyword 149 | | 150 | 7 | task::spawn(async move { 151 | 8 | println!("Here's a vec: {:?}", v); 152 | 9 | }); 153 | | 154 | ``` 155 | 156 | 原因在于:默认情况下,变量并不是通过 `move` 的方式转移进 `async` 语句块的, `v` 变量的所有权依然属于 `main` 函数,因为任务内部的 `println!` 是通过借用的方式使用了 `v`,但是这种借用并不能满足 `'static` 生命周期的要求。 157 | 158 | 在报错的同时,Rust编译器还给出了相当有帮助的提示:为 `async` 语句块使用 `move` 关键字,这样就能将 `v` 的所有权从 `main` 函数转移到新创建的任务中。 159 | 160 | 但是 `move` 有一个问题,一个数据只能被一个任务使用,如果想要多个任务使用一个数据,就有些强人所难。不知道还有多少同学记得 [`Arc`](../advance/smart-pointer/rc-arc.md),它可以轻松解决该问题,还是线程安全的。 161 | 162 | 在上面的报错中,还有一句很奇怪的信息`function requires argument type to outlive 'static`, 函数要求参数类型的生命周期必须比 `'static` 长,问题是 `'static` 已经活得跟整个程序一样久了,难道函数的参数还能活得更久?大家可能会觉得编译器秀逗了,毕竟其它语言编译器也有秀逗的时候:) 163 | 164 | 先别急着给它扣帽子,虽然我有时候也想这么做。。原因是它说的是类型必须活得比 `'static` 长,而不是值。当我们说一个值是 `'static` 时,意味着它将永远存活。这个很重要,因为编译器无法知道新创建的任务将存活多久,所以唯一的办法就是让任务永远存活。 165 | 166 | 实际上编译的这个报错在告诉我们函数的参数要满足以下约束:`T: 'static`。 167 | 168 | 我们一般用 **T 被 'static 所约束** 来描述 `T: 'static`,当然你也可以说**T 类型比 `'static` 活得更久** 或 **T的值是 `'static`** ,事实上这三个是一个意思,都是说只要 `T: 'static`, 那它将永远活下去。 169 | 170 | 反而是 `&'static T` 它拥有一个不同的含义:使用`'static` 进行标注,但这个标注并不能让 `T` 永远活下去,仅仅是一个标注。 171 | 172 | 也就是说,一个类型 `T` 必须活得比 `'static` 更长,意味着它满足以下约束 `T: 'static` 173 | 174 | PS: 这一段内容,Tokio 的原文有些逻辑混乱,我已经尽可能的理清了作者想表达的含义, 同时补充了一些自己的理解,以让内容阅读起来更流畅 :/ 175 | 176 | > 一个关于 `'static` 生命周期的常见误解就是它将永远存活(跟整个程序活得一样久),实际上并不是这样的。同样的,一个变量是 `'static` 并不意味着它存在内存泄漏的可能性。 177 | 178 | #### Send 约束 179 | `tokio::spawn` 生成的任务必须实现 `Send` 特征,因为当这些任务在 `.await` 执行过程中发生阻塞时,Tokio 调度器会将任务在线程间移动。 180 | 181 | **一个任务要实现 `Send` 特征,那它在 `.await` 调用的过程中所持有的全部数据都必须实现 `Send` 特征**。当 `.await` 调用发生阻塞时,任务会让出当前线程所有权给调度器,然后当任务准备好后,调度器会从上一次暂停的位置继续执行该任务。该流程能正确的工作,任务必须将`.await`之后使用的所有状态保存起来,这样才能在中断后恢复现场并继续执行。若这些状态实现了 `Send` 特征(可以在线程间安全地移动),那任务自然也就可以在线程间安全地移动。 182 | 183 | 例如以下代码可以工作: 184 | ```rust 185 | use tokio::task::yield_now; 186 | use std::rc::Rc; 187 | 188 | #[tokio::main] 189 | async fn main() { 190 | tokio::spawn(async { 191 | // 语句块的使用强制了 `rc` 会在 `.await` 被调用前就被释放, 192 | // 因此 `rc` 并不会影响 `.await`的安全性 193 | { 194 | let rc = Rc::new("hello"); 195 | println!("{}", rc); 196 | } 197 | 198 | // `rc` 的作用范围已经失效,因此当任务让出所有权给当前线程时,它无需作为状态被保存起来 199 | yield_now().await; 200 | }); 201 | } 202 | ``` 203 | 204 | 但是下面代码就不行: 205 | ```rust 206 | use tokio::task::yield_now; 207 | use std::rc::Rc; 208 | 209 | #[tokio::main] 210 | async fn main() { 211 | tokio::spawn(async { 212 | let rc = Rc::new("hello"); 213 | 214 | 215 | // `rc` 在 `.await` 后还被继续使用,因此它必须被作为任务的状态保存起来 216 | yield_now().await; 217 | 218 | 219 | // 事实上,注释掉下面一行代码,依然会报错 220 | // 原因是:是否保存,不取决于 `rc` 是否被使用,而是取决于 `.await`在调用时是否仍然处于 `rc` 的作用域中 221 | println!("{}", rc); 222 | 223 | // rc 作用域在这里结束 224 | }); 225 | } 226 | ``` 227 | 228 | 这里有一个很重要的点,代码注释里有讲到,但是我们再重复一次: `rc` 是否会保存到任务状态中,取决于 `.await` 的调用是否处于它的作用域中,上面代码中,就算你注释掉 `println!` 函数,该报错依然会报错,因为 `rc` 的作用域直到 `async` 的末尾才结束! 229 | 230 | 下面是相应的报错,在下一章节,我们还会继续深入讨论该错误: 231 | ```shell 232 | error: future cannot be sent between threads safely 233 | --> src/main.rs:6:5 234 | | 235 | 6 | tokio::spawn(async { 236 | | ^^^^^^^^^^^^ future created by async block is not `Send` 237 | | 238 | ::: [..]spawn.rs:127:21 239 | | 240 | 127 | T: Future + Send + 'static, 241 | | ---- required by this bound in 242 | | `tokio::task::spawn::spawn` 243 | | 244 | = help: within `impl std::future::Future`, the trait 245 | | `std::marker::Send` is not implemented for 246 | | `std::rc::Rc<&str>` 247 | note: future is not `Send` as this value is used across an await 248 | --> src/main.rs:10:9 249 | | 250 | 7 | let rc = Rc::new("hello"); 251 | | -- has type `std::rc::Rc<&str>` which is not `Send` 252 | ... 253 | 10 | yield_now().await; 254 | | ^^^^^^^^^^^^^^^^^ await occurs here, with `rc` maybe 255 | | used later 256 | 11 | println!("{}", rc); 257 | 12 | }); 258 | | - `rc` is later dropped here 259 | ``` 260 | 261 | ## 使用HashMap存储数据 262 | 现在,我们可以继续前进了,下面来实现 `process` 函数,它用于处理进入的命令。相应的值将被存储在 `HashMap` 中: 通过 `SET` 命令存值,通过 `GET` 命令来取值。 263 | 264 | 同时,我们将使用循环的方式在同一个客户端连接中处理多次连续的请求: 265 | ```rust 266 | use tokio::net::TcpStream; 267 | use mini_redis::{Connection, Frame}; 268 | 269 | async fn process(socket: TcpStream) { 270 | use mini_redis::Command::{self, Get, Set}; 271 | use std::collections::HashMap; 272 | 273 | // 使用 hashmap 来存储 redis 的数据 274 | let mut db = HashMap::new(); 275 | 276 | // `mini-redis` 提供的便利函数,使用返回的 `connection` 可以用于从 socket 中读取数据并解析为数据帧 277 | let mut connection = Connection::new(socket); 278 | 279 | // 使用 `read_frame` 方法从连接获取一个数据帧:一条redis命令 + 相应的数据 280 | while let Some(frame) = connection.read_frame().await.unwrap() { 281 | let response = match Command::from_frame(frame).unwrap() { 282 | Set(cmd) => { 283 | // 值被存储为 `Vec` 的形式 284 | db.insert(cmd.key().to_string(), cmd.value().to_vec()); 285 | Frame::Simple("OK".to_string()) 286 | } 287 | Get(cmd) => { 288 | if let Some(value) = db.get(cmd.key()) { 289 | // `Frame::Bulk` 期待数据的类型是 `Bytes`, 该类型会在后面章节讲解, 290 | // 此时,你只要知道 `&Vec` 可以使用 `into()` 方法转换成 `Bytes` 类型 291 | Frame::Bulk(value.clone().into()) 292 | } else { 293 | Frame::Null 294 | } 295 | } 296 | cmd => panic!("unimplemented {:?}", cmd), 297 | }; 298 | 299 | // 将请求响应返回给客户端 300 | connection.write_frame(&response).await.unwrap(); 301 | } 302 | } 303 | 304 | // main 函数在之前已实现 305 | ``` 306 | 307 | 使用 `cargo run` 运行服务器,然后再打开另一个终端窗口,运行 `hello-redis` 客户端示例: `cargo run --example hello-redis`。 308 | 309 | Bingo,在看了这么多原理后,我们终于迈出了小小的第一步,并获取到了存在 `HashMap` 中的值: `got value from the server; result=Some(b"world")`。 310 | 311 | 但是问题又来了:这些值无法在 TCP 连接中共享,如果另外一个用户连接上来并试图同时获取 `hello` 这个 `key`,他将一无所获。 312 | -------------------------------------------------------------------------------- /src/stream.md: -------------------------------------------------------------------------------- 1 | # Stream 2 | 大家有没有想过, Rust 中的迭代器在迭代时能否异步进行?若不可以,是不是有相应的解决方案? 3 | 4 | 以上的问题其实很重要,因为在实际场景中,迭代一个集合,然后异步的去执行是很常见的需求,好在 Tokio 为我们提供了 `stream`,我们可以在异步函数中对其进行迭代,甚至和迭代器 `Iterator` 一样,`stream` 还能使用适配器,例如 `map` ! Tokio 在 [`StreamExt`](https://docs.rs/tokio-stream/0.1.8/tokio_stream/trait.StreamExt.html) 特征上定义了常用的适配器。 5 | 6 | 要使用 `stream` ,目前还需要手动引入对应的包: 7 | ```rust 8 | tokio-stream = "0.1" 9 | ``` 10 | 11 | > stream 没有放在 `tokio` 包的原因在于标准库中的 `Stream` 特征还没有稳定,一旦稳定后,`stream` 将移动到 `tokio` 中来 12 | 13 | ## 迭代 14 | 目前, Rust 语言还不支持异步的 `for` 循环,因此我们需要 `while let` 循环和 [`StreamExt::next()`](https://docs.rs/tokio-stream/0.1.8/tokio_stream/trait.StreamExt.html#method.next) 一起使用来实现迭代的目的: 15 | ```rust 16 | use tokio_stream::StreamExt; 17 | 18 | #[tokio::main] 19 | async fn main() { 20 | let mut stream = tokio_stream::iter(&[1, 2, 3]); 21 | 22 | while let Some(v) = stream.next().await { 23 | println!("GOT = {:?}", v); 24 | } 25 | } 26 | ``` 27 | 28 | 和迭代器 `Iterator` 类似,`next()` 方法返回一个 `Option`,其中 `T` 是从 `stream` 中获取的值的类型。若收到 `None` 则意味着 `stream` 迭代已经结束。 29 | 30 | #### mini-redis 广播 31 | 下面我们来实现一个复杂一些的 mini-redis 客户端,完整代码见[这里](https://github.com/tokio-rs/website/blob/master/tutorial-code/streams/src/main.rs)。 32 | 33 | 在开始之前,首先启动一下完整的 mini-redis 服务器端: 34 | ```console 35 | $ mini-redis-server 36 | ``` 37 | 38 | ```rust 39 | use tokio_stream::StreamExt; 40 | use mini_redis::client; 41 | 42 | async fn publish() -> mini_redis::Result<()> { 43 | let mut client = client::connect("127.0.0.1:6379").await?; 44 | 45 | // 发布一些数据 46 | client.publish("numbers", "1".into()).await?; 47 | client.publish("numbers", "two".into()).await?; 48 | client.publish("numbers", "3".into()).await?; 49 | client.publish("numbers", "four".into()).await?; 50 | client.publish("numbers", "five".into()).await?; 51 | client.publish("numbers", "6".into()).await?; 52 | Ok(()) 53 | } 54 | 55 | async fn subscribe() -> mini_redis::Result<()> { 56 | let client = client::connect("127.0.0.1:6379").await?; 57 | let subscriber = client.subscribe(vec!["numbers".to_string()]).await?; 58 | let messages = subscriber.into_stream(); 59 | 60 | tokio::pin!(messages); 61 | 62 | while let Some(msg) = messages.next().await { 63 | println!("got = {:?}", msg); 64 | } 65 | 66 | Ok(()) 67 | } 68 | 69 | #[tokio::main] 70 | async fn main() -> mini_redis::Result<()> { 71 | tokio::spawn(async { 72 | publish().await 73 | }); 74 | 75 | subscribe().await?; 76 | 77 | println!("DONE"); 78 | 79 | Ok(()) 80 | } 81 | ``` 82 | 83 | 上面生成了一个异步任务专门用于发布消息到 min-redis 服务器端的 `numbers` 消息通道中。然后,在 `main` 中,我们订阅了 `numbers` 消息通道,并且打印从中接收到的消息。 84 | 85 | 还有几点值得注意的: 86 | 87 | - [`into_stream`](https://docs.rs/mini-redis/0.4.1/mini_redis/client/struct.Subscriber.html#method.into_stream) 会将 `Subscriber` 变成一个 `stream` 88 | - 在 `stream` 上调用 `next` 方法要求该 `stream` 被固定住([`pinned`](https://doc.rust-lang.org/std/pin/index.html)),因此需要调用 `tokio::pin!` 89 | 90 | > 关于 Pin 的详细解读,可以阅读[这篇文章](https://course.rs/async/pin-unpin.html) 91 | 92 | 大家可以去掉 `pin!` 的调用,然后观察下报错,若以后你遇到这种错误,可以尝试使用下 `pin!`。 93 | 94 | 此时,可以运行下我们的客户端代码看看效果(别忘了先启动前面提到的 mini-redis 服务端): 95 | ```console 96 | got = Ok(Message { channel: "numbers", content: b"1" }) 97 | got = Ok(Message { channel: "numbers", content: b"two" }) 98 | got = Ok(Message { channel: "numbers", content: b"3" }) 99 | got = Ok(Message { channel: "numbers", content: b"four" }) 100 | got = Ok(Message { channel: "numbers", content: b"five" }) 101 | got = Ok(Message { channel: "numbers", content: b"6" }) 102 | ``` 103 | 104 | 在了解了 `stream` 的基本用法后,我们再来看看如何使用适配器来扩展它。 105 | 106 | ## 适配器 107 | 在前面章节中,我们了解了迭代器有[两种适配器](https://course.rs/advance/functional-programing/iterator.html#消费者与适配器): 108 | 109 | - 迭代器适配器,会将一个迭代器转变成另一个迭代器,例如 `map`,`filter` 等 110 | - 消费者适配器,会消费掉一个迭代器,最终生成一个值,例如 `collect` 可以将迭代器收集成一个集合 111 | 112 | 与迭代器类似,`stream` 也有适配器,例如一个 `stream` 适配器可以将一个 `stream` 转变成另一个 `stream` ,例如 `map`、`take` 和 `filter`。 113 | 114 | 在之前的客户端中,`subscribe` 订阅一直持续下去,直到程序被关闭。现在,让我们来升级下,让它在收到三条消息后就停止迭代,最终结束。 115 | ```rust 116 | let messages = subscriber 117 | .into_stream() 118 | .take(3); 119 | ``` 120 | 121 | 这里关键就在于 `take` 适配器,它会限制 `stream` 只能生成最多 `n` 条消息。运行下看看结果: 122 | ```console 123 | got = Ok(Message { channel: "numbers", content: b"1" }) 124 | got = Ok(Message { channel: "numbers", content: b"two" }) 125 | got = Ok(Message { channel: "numbers", content: b"3" }) 126 | ``` 127 | 128 | 程序终于可以正常结束了。现在,让我们过滤 `stream` 中的消息,只保留数字类型的值: 129 | ```rust 130 | let messages = subscriber 131 | .into_stream() 132 | .filter(|msg| match msg { 133 | Ok(msg) if msg.content.len() == 1 => true, 134 | _ => false, 135 | }) 136 | .take(3); 137 | ``` 138 | 139 | 运行后输出: 140 | ```console 141 | got = Ok(Message { channel: "numbers", content: b"1" }) 142 | got = Ok(Message { channel: "numbers", content: b"3" }) 143 | got = Ok(Message { channel: "numbers", content: b"6" }) 144 | ``` 145 | 146 | 需要注意的是,适配器的顺序非常重要,`.filter(...).take(3)` 和 `.take(3).filter(...)` 的结果可能大相径庭,大家可以自己尝试下。 147 | 148 | 现在,还有一件事要做,咱们的消息被不太好看的 `Ok(...)` 所包裹,现在通过 `map` 适配器来简化下: 149 | ```rust 150 | let messages = subscriber 151 | .into_stream() 152 | .filter(|msg| match msg { 153 | Ok(msg) if msg.content.len() == 1 => true, 154 | _ => false, 155 | }) 156 | .map(|msg| msg.unwrap().content) 157 | .take(3); 158 | ``` 159 | 160 | 注意到 `msg.unwrap` 了吗?大家可能会以为我们是出于示例的目的才这么用,实际上并不是,由于 `filter` 的先执行, `map` 中的 `msg` 只能是 `Ok(...)`,因此 `unwrap` 非常安全。 161 | 162 | ```console 163 | got = b"1" 164 | got = b"3" 165 | got = b"6" 166 | ``` 167 | 168 | 还有一点可以改进的地方:当 `filter` 和 `map` 一起使用时,你往往可以用一个统一的方法来实现 [`filter_map`](https://docs.rs/tokio-stream/0.1.8/tokio_stream/trait.StreamExt.html#method.filter_map)。 169 | 170 | 想要学习更多的适配器,可以看看 [`StreamExt`](https://docs.rs/tokio-stream/0.1.8/tokio_stream/trait.StreamExt.html) 特征。 171 | 172 | ## 实现 Stream 特征 173 | 如果大家还没忘记 `Future` 特征,那 `Stream` 特征相信你也会很快记住,因为它们非常类似: 174 | ```rust 175 | use std::pin::Pin; 176 | use std::task::{Context, Poll}; 177 | 178 | pub trait Stream { 179 | type Item; 180 | 181 | fn poll_next( 182 | self: Pin<&mut Self>, 183 | cx: &mut Context<'_> 184 | ) -> Poll>; 185 | 186 | fn size_hint(&self) -> (usize, Option) { 187 | (0, None) 188 | } 189 | } 190 | ``` 191 | 192 | `Stream::poll_next()` 函数跟 `Future::poll` 很相似,区别就是前者为了从 `stream` 收到多个值需要重复的进行调用。 就像在 [`深入async`](https://course.rs/tokio/async.html) 章节提到的那样,当一个 `stream` 没有做好返回一个值的准备时,它将返回一个 `Poll::Pending` ,同时将任务的 `waker` 进行注册。一旦 `stream` 准备好后, `waker` 将被调用。 193 | 194 | 通常来说,如果想要手动实现一个 `Stream`,需要组合 `Future` 和其它 `Stream`。下面,还记得在[`深入async`](https://course.rs/tokio/async.html) 中构建的 `Delay Future` 吗?现在让我们来更进一步,将它转换成一个 `stream`,每 10 毫秒生成一个值,总共生成 3 次: 195 | ```rust 196 | use tokio_stream::Stream; 197 | use std::pin::Pin; 198 | use std::task::{Context, Poll}; 199 | use std::time::Duration; 200 | 201 | struct Interval { 202 | rem: usize, 203 | delay: Delay, 204 | } 205 | 206 | impl Stream for Interval { 207 | type Item = (); 208 | 209 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) 210 | -> Poll> 211 | { 212 | if self.rem == 0 { 213 | // 去除计时器实现 214 | return Poll::Ready(None); 215 | } 216 | 217 | match Pin::new(&mut self.delay).poll(cx) { 218 | Poll::Ready(_) => { 219 | let when = self.delay.when + Duration::from_millis(10); 220 | self.delay = Delay { when }; 221 | self.rem -= 1; 222 | Poll::Ready(Some(())) 223 | } 224 | Poll::Pending => Poll::Pending, 225 | } 226 | } 227 | } 228 | ``` 229 | 230 | #### async-stream 231 | 手动实现 `Stream` 特征实际上是相当麻烦的事,不幸地是,Rust 语言的 `async/await` 语法目前还不能用于定义 `stream`,虽然相关的工作已经在进行中。 232 | 233 | 作为替代方案,[`async-stream`](https://docs.rs/async-stream/latest/async_stream/) 包提供了一个 `stream!` 宏,它可以将一个输入转换成 `stream`,使用这个包,上面的代码可以这样实现: 234 | ```rust 235 | use async_stream::stream; 236 | use std::time::{Duration, Instant}; 237 | 238 | stream! { 239 | let mut when = Instant::now(); 240 | for _ in 0..3 { 241 | let delay = Delay { when }; 242 | delay.await; 243 | yield (); 244 | when += Duration::from_millis(10); 245 | } 246 | } 247 | ``` 248 | 249 | 嗯,看上去还是相当不错的,代码可读性大幅提升! 250 | 251 | 252 | 253 | --------------------------------------------------------------------------------