并发服务器:第 7 部分 - Rust 语言实现
文章背景与核心概要
本文是编写并发网络服务器系列文章的第七部分。在前几篇文章中,作者探讨了从基础的单线程状态机、线程池到现代异步事件驱动架构的各种并发挑战。在本篇中,作者将目光转向 Rust 语言,展示了 Rust 如何通过其独特的语言特性、tokio 运行时以及 Redis 缓存集成来应对这些挑战。
文章首先回顾了顺序执行的状态机服务器,随后逐步演进到“每个客户端一个线程”的模型以及通过 crossbeam_channel 实现的固定线程池模型。接着,文章重点介绍了基于 tokio 的现代异步事件驱动服务器,并通过素数计算和 Redis 缓存集成案例,演示了 Rust 异步编程中的“async/await”语法以及对“函数颜色问题”的处理方式。
摘要 (Summary)
This is the seventh installment in a comprehensive series on writing concurrent network servers. In this part, we explore how Rust addresses the concurrency challenges discussed in earlier articles—ranging from foundational single-threaded state machines to thread pools, and finally to modern asynchronous event-driven architectures utilizing the
tokioruntime and Redis caching.
这是关于编写并发网络服务器系列文章的第七部分。在本部分中,我们探讨了 Rust 如何应对早期文章中讨论的并发挑战——从基础的单线程状态机到线程池,最后到利用 tokio 运行时和 Redis 缓存的现代异步事件驱动架构。
系列概述 (Series Overview)
- 第 1 部分 - 简介
- 第 2 部分 - 线程
- 第 3 部分 - 事件驱动
- 第 4 部分 - libuv
- 第 5 部分 - Redis 案例研究
- 第 6 部分 - 回调、Promise 与 async/await
- 第 7 部分 - Rust (本部分)
引言 (Introduction)
Several years have passed since the previous parts were published. I've recently gone over them to make sure the information presented is still relevant and all the code samples build and run using modern toolchains. I strongly recommend reviewing the previous parts before reading this one.
自从发布前几部分以来,已经过去了好几年。我最近重新审阅了它们,以确保所提供的信息仍然具有相关性,并且所有的代码示例都能在使用现代工具链的情况下构建和运行。我强烈建议在阅读本文之前先复习前面的部分。
This post assumes a basic familiarity with the Rust programming language. It will only explain Rust constructs when we encounter code that wouldn't appear in an introductory book or tutorial.
本文假定读者对 Rust 编程语言有基本的了解。只有当我们遇到不会出现在入门书籍或教程中的代码时,才会对 Rust 的结构进行解释。
建立基准:顺序状态机服务器 (Setting the Baseline: A Sequential State Machine Server)
The first few parts in the series focused on a socket server that implements a simple state machine protocol. See Part 1 for a complete description of the protocol. Let's start by showing how this protocol is implemented in a basic sequential Rust server:
本系列的前几部分重点关注实现简单状态机协议的套接字服务器。有关该协议的完整描述,请参见第 1 部分。让我们首先展示如何在基本的顺序 Rust 服务器中实现该协议:
use async_socket_server::serve_connection;
use std::net::TcpListener;
fn main() -> std::io::Result<()> {
let port = match std::env::args().nth(1) {
Some(s) => s,
None => "9090".to_string(),
};
let addr = format!("127.0.0.1:{port}");
let listener = TcpListener::bind(addr)?;
println!("Serving on port {port}");
loop {
let (stream, addr) = listener.accept()?;
println!("connection received from {}", addr);
if let Err(e) = serve_connection(stream) {
eprintln!("error serving connection: {}", e);
} else {
println!("peer done {addr}");
}
}
}
With the function
serve_connectiondefined as:
其中 serve_connection 函数定义如下:
pub enum ProcessingState {
WaitForMsg,
InMsg,
}
pub fn serve_connection(mut stream: TcpStream) -> std::io::Result<()> {
stream.write_all(b"*")?;
let mut state = ProcessingState::WaitForMsg;
let mut buf = [0u8; 1024];
loop {
let n = stream.read(&mut buf)?;
if n == 0 {
// Connection closed by the client.
break;
}
for byte in &buf[..n] {
match state {
ProcessingState::WaitForMsg => {
if *byte == b'^' {
state = ProcessingState::InMsg;
}
}
ProcessingState::InMsg => {
if *byte == b'$' {
state = ProcessingState::WaitForMsg;
} else {
let newbyte = byte.wrapping_add(1);
stream.write_all(&[newbyte])?;
}
}
}
}
}
Ok(())
}
As a reminder, this server version is sequential because it accepts clients one by one; the main loop blocks on
serve_connectionuntil it's done (the client closes the connection), and only then goes back to accept the next client.
提醒一下,这个版本的服务器是顺序执行的,因为它逐个接受客户端;主循环在 serve_connection 上阻塞,直到其完成(客户端关闭连接),然后才返回去接受下一个客户端。
每个客户端一个线程 (One Thread Per Client)
Clearly, handling clients one by one won't do. In Part 2, we've discussed approaches that use OS threads to handle clients concurrently. Let's start with the unbounded one-thread-per-client solution in Rust:
很明显,逐个处理客户端是行不通的。在第 2 部分中,我们讨论了使用操作系统线程来并发处理客户端的方法。让我们从 Rust 中无限制的“每个客户端一个线程”解决方案开始:
use async_socket_server::serve_connection;
use std::{net::TcpListener, thread};
fn main() -> std::io::Result<()> {
let port = match std::env::args().nth(1) {
Some(s) => s,
None => "9090".to_string(),
};
let addr = format!("127.0.0.1:{port}");
let listener = TcpListener::bind(addr)?;
println!("Serving on port {port}");
loop {
let (stream, addr) = listener.accept()?;
println!("connection received from {}", addr);
let res = thread::Builder::new().spawn(move || {
if let Err(e) = serve_connection(stream) {
eprintln!("error serving connection: {}", e);
} else {
println!("peer done {addr}");
}
});
if let Err(e) = res {
eprintln!("error spawning thread: {}", e);
}
}
}
The
spawnmethod returns aResult<JoinHandle<T>>; on success, we allow the handle to be dropped at the end of the loop iteration. In Rust, this detaches the thread; we don't actually wait for it to complete. This is reasonable for our code sample, because the loop is infinite; it never terminates anyway. The potential for runaway threads is just one of the issues with the unbounded threads approach discussed in part 2. The solution is to use a fixed thread pool.
spawn 方法返回一个 Result<JoinHandle<T>>;成功时,我们允许在循环迭代结束时丢弃该句柄。在 Rust 中,这会分离(detach)线程;我们实际上并不等待它完成。对于我们的代码示例来说,这是合理的,因为循环是无限的;无论如何它都不会终止。线程失控的潜力只是第 2 部分中讨论的无限制线程方法的其中一个问题。解决方案是使用固定的线程池。
线程池 (Thread Pool)
Before diving into the code, a quick note on the design: the thread pool is a fixed set of threads that await "jobs" and handle them to completion. In our case a "job" is
serve_connectionfor a specific client. There are many ways to implement a thread pool; for our use case, I went with a set of threads that all get a shared channel to which the main thread sends jobs. A worker thread picks up the next job from the channel, serves it to completion, and goes back to waiting for the next job. Here's how this looks in code:
在深入代码之前,先简要说明一下设计:线程池是一组固定的线程,它们等待“任务”并将其处理完成。在我们的例子中,“任务”是针对特定客户端的 serve_connection。实现线程池的方法有很多;对于我们的用例,我选择了一组线程,它们都获取一个共享的通道(channel),主线程向其中发送任务。工作线程从通道中拾取下一个任务,将其服务到完成,然后返回等待下一个任务。以下是代码中的实现形式:
struct Job {
stream: TcpStream,
addr: SocketAddr,
}
fn worker(receiver: Receiver<Job>) {
while let Ok(job) = receiver.recv() {
if let Err(e) = serve_connection(job.stream) {
eprintln!("error serving connection from {}: {}", job.addr, e);
} else {
println!("peer done {}", job.addr);
}
}
}
What is
Receiver? It's a type from thecrossbeam_channelcrate:
什么是 Receiver?它是 crossbeam_channel crate 中的一个类型:
use crossbeam_channel::{Receiver, bounded};
Rust's builtin channels in
stdare mpsc — multi producer, single consumer, but what we need for our job queue is a channel that supports multiple consumers (the worker threads). Whilestddoes have mpmc, this is an experimental API only available in nightly versions at the time of writing. Therefore, I've opted to include thecrossbeam_channelcrate that provides well-tested mpmc channels for this sample [1].
Rust std 中的内置通道是 mpsc(多生产者,单消费者),但我们任务队列需要的是支持多消费者(工作线程)的通道。虽然 std 确实有 mpmc,但在撰写本文时,这是一个仅在 nightly 版本中可用的实验性 API。因此,我选择引入 crossbeam_channel crate,它为本示例提供了经过充分测试的 mpmc 通道 [1]。
And here's the main function:
以下是 main 函数:
use async_socket_server::serve_connection;
use crossbeam_channel::{Receiver, bounded};
use std::{
io,
net::{SocketAddr, TcpListener, TcpStream},
thread,
};
const NUM_WORKERS: usize = 256;
const JOB_QUEUE_CAPACITY: usize = NUM_WORKERS;
fn main() -> io::Result<()> {
let port = match std::env::args().nth(1) {
Some(s) => s,
None => "9090".to_string(),
};
let addr = format!("127.0.0.1:{port}");
let listener = TcpListener::bind(addr)?;
println!("Serving on port {port}");
let (tx, rx) = bounded::<Job>(JOB_QUEUE_CAPACITY);
for _ in 0..NUM_WORKERS {
let receiver = rx.clone();
thread::Builder::new().spawn(move || worker(receiver))?;
}
drop(rx);
loop {
let (stream, addr) = listener.accept()?;
println!("connection received from {addr}");
// A full queue blocks this loop, preventing it from accepting more
// connections until a worker becomes available.
if tx.send(Job { stream, addr }).is_err() {
return Err(io::Error::other("all connection workers stopped"));
}
}
}
Note that our job channel is bounded — it has a fixed size. This helps naturally implement a backpressure mechanism — if too many clients connect, the following clients will have to wait — the main loop blocks on
tx.sendand won't accept additional clients on the socket until jobs are cleared from the channel.
请注意,我们的任务通道是有界的(bounded)——它具有固定的大小。这有助于自然地实现背压(backpressure)机制——如果有太多客户端连接,后续的客户端将不得不等待——主循环会在 tx.send 上阻塞,并且在通道中的任务被清理之前,不会接受套接字上的更多客户端。
异步、事件驱动服务器 (Asynchronous, Event-Driven Server)
In Parts 4, 5, and 6 of the series we've discussed event-driven, or asynchronous servers. Let's see how it's done in Rust. Specifically, Part 6 presented a gradation from callbacks to promises to async/await mechanisms; Rust supports all of these and — as you'd expect — modern code is usually written with async/await while hiding all the details of promises (called futures in Rust) underneath.
在本系列的第 4、5 和 6 部分中,我们讨论了事件驱动或异步服务器。让我们看看在 Rust 中是如何实现的。具体来说,第 6 部分展示了从回调到 Promise 再到 async/await 机制的演进;Rust 支持所有这些,并且——正如你所料——现代代码通常使用 async/await 编写,同时在底层隐藏了 Promise(在 Rust 中称为 futures)的所有细节。
Without further ado, here's our simple state machine protocol in asynchronous Rust:
废话不多说,这是我们在异步 Rust 中实现的简单状态机协议:
use async_socket_server::ProcessingState;
use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
#[tokio::main]
async fn main() -> io::Result<()> {
let port = match std::env::args().nth(1) {
Some(s) => s,
None => "9090".to_string(),
};
let addr = format!("127.0.0.1:{port}");
let listener = TcpListener::bind(addr).await?;
println!("Serving on port {port}");
loop {
let (socket, addr) = listener.accept().await?;
println!("connection received from {addr:?}");
tokio::spawn(async move {
if let Err(e) = serve_connection_async(socket).await {
eprintln!("error serving connection from {addr:?}: {e}");
} else {
println!("peer done {addr:?}");
}
});
}
}
Rust takes an interesting approach to async programming: it supports some of its fundamental building blocks (like futures and the
asyncandawaitkeywords) in the core language, but leaves the actual async engine implementation (the thing that implements the event loop) to external crates. By far the most popular crate for async programming in Rust is tokio, so that's what we're using here.
Rust 对异步编程采取了一种有趣的方法:它在核心语言中支持其某些基础构建块(如 futures 以及 async 和 await 关键字),但将实际的异步引擎实现(实现事件循环的东西)留给外部 crate。到目前为止,Rust 中最受欢迎的异步编程 crate 是 tokio,所以这就是我们在这里使用的工具。
After reading the JS code in part 6, the Rust snippet above should appear fairly familiar, except perhaps the explicit tokio task "spawn". Instead of enqueuing a callback on the connection returned by
listener.accept, the code spawns a tokio task, which can be seen as a green thread, and hence uses similar terminology [2]. These tasks must not issue blocking calls; therefore, they are supposed to use tokio's I/O utilities instead of the usual, blockingstdutilities. In fact, we have to implement an async version ofserve_connectionto make this work:
在阅读了第 6 部分中的 JS 代码后,上面这段 Rust 代码片段应该显得相当熟悉,除了显式的 tokio 任务“spawn”之外。代码没有在 listener.accept 返回的连接上排队回调,而是生成(spawn)了一个 tokio 任务,这可以被视为一个绿色线程(green thread),因此使用了类似的术语 [2]。这些任务绝不能发出阻塞调用;因此,它们应该使用 tokio 的 I/O 工具,而不是通常的阻塞 std 工具。事实上,我们必须实现一个异步版本的 serve_connection 才能使其工作:
async fn serve_connection_async(mut stream: tokio::net::TcpStream) -> std::io::Result<()> {
stream.write_all(b"*").await?;
let mut state = ProcessingState::WaitForMsg;
let mut buf = [0u8; 1024];
loop {
let n = stream.read(&mut buf).await?;
if n == 0 {
// Connection closed by the client.
break;
}
for byte in &buf[..n] {
match state {
ProcessingState::WaitForMsg => {
if *byte == b'^' {
state = ProcessingState::InMsg;
}
}
ProcessingState::InMsg => {
if *byte == b'$' {
state = ProcessingState::WaitForMsg;
} else {
let newbyte = byte.wrapping_add(1);
stream.write_all(&[newbyte]).await?;
}
}
}
}
}
Ok(())
}
Note how similar this code is to
serve_connectionfrom earlier; the only real differences are theawaitcalls on socket reads and writes [3], and the types involved. For example, instead of astd::net::TcpStreamused in the synchronous samples, here we're usingtokio::net::TcpStream. Tokio has an underlying dependency called mio to handle non-blocking APIs for all kinds of I/O. It wraps OS-specific event loops like epoll to do so efficiently.
请注意这段代码与先前的 serve_connection 是多么相似;唯一的真正区别是对套接字读写的 await 调用 [3],以及所涉及的类型。例如,我们没有使用同步示例中使用的 std::net::TcpStream,而是在这里使用了 tokio::net::TcpStream。Tokio 有一个名为 mio 的底层依赖项,用于处理各种 I/O 的非阻塞 API。它封装了操作系统特定的事件循环(如 epoll),从而高效地完成工作。
异步素数测试服务器 (Asynchronous Primality Testing Servers)
While most of the series has been using a simple state machine server as the driving example, Part 6 switched focus to a server for primality testing which simulates long compute tasks. Let's see how this is done in Rust with tokio:
虽然本系列的大多数文章一直使用简单的状态机服务器作为驱动示例,但第 6 部分将重点转移到了用于素数测试的服务器上,该服务器模拟了长时间的计算任务。让我们看看在 Rust 中如何使用 tokio 来实现这一点:
use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
#[tokio::main]
async fn main() -> io::Result<()> {
let port = match std::env::args().nth(1) {
Some(s) => s,
None => "8070".to_string(),
};
let addr = format!("127.0.0.1:{port}");
let listener = TcpListener::bind(addr).await?;
println!("Serving on port {port}");
loop {
let (socket, addr) = listener.accept().await?;
println!("connection received from {addr:?}");
tokio::spawn(async move {
if let Err(e) = serve_client(socket).await {
eprintln!("error serving connection from {addr:?}: {e}");
} else {
println!("peer done {addr:?}");
}
});
}
}
async fn serve_client(mut stream: tokio::net::TcpStream) -> std::io::Result<()> {
let mut buf = [0u8; 1024];
loop {
let n = stream.read(&mut buf).await?;
if n == 0 {
// Connection closed by the client.
return Ok(());
}
// Parse read buf into u64.
let num = std::str::from_utf8(&buf[..n])
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?
.trim()
.parse::<u64>()
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
let answer = if isprime(num, true) {
"prime"
} else {
"composite"
};
stream
.write_all((answer.to_string() + "\n").as_bytes())
.await?;
}
}
This code is very similar to the previous snippet conceptually;
isprimeis:
这段代码在概念上与前一个代码片段非常相似;isprime 如下:
// Check if n is prime, returning a boolean. The delay parameter is optional;
// if true, the function will block for n milliseconds before computing the
// answer. This is useful for simulating a long-running computation.
fn isprime(n: u64, delay: bool) -> bool {
if delay {
std::thread::sleep(std::time::Duration::from_millis(n));
}
if n < 2 {
return false;
}
if n % 2 == 0 {
return n == 2;
}
let mut r = 3;
while r * r <= n {
if n % r == 0 {
return false;
}
r += 2;
}
true
}
Note that this sample demonstrates a job that can block (simulated with a sleep in this case). This can be problematic in an async context, as the tokio documentation explains.
请注意,此示例演示了一个可能会阻塞的任务(在本例中通过 sleep 模拟)。正如 tokio 文档所解释的,这在异步上下文中可能会出问题。
One potential solution would be to dispatch a blocking task to a separate thread pool and use tokio channels to communicate with it; this is similar to the approach we've taken in the thread pool sample above.
一个潜在的解决方案是将阻塞任务分发到单独的线程池,并使用 tokio 通道与其通信;这类似于我们在上面的线程池示例中采用的方法。
Part 6 also included a version of this server that caches data on a local Redis instance; the goal was to demonstrate the complexity of event-driven code when additional layers of callbacks are added and how async/await can help mitigate that. Here's our Rust version of this server, using the
rediscrate (that has a tokio component enabled explicitly to support async calls):
第 6 部分还包含了该服务器的一个版本,它将数据缓存在本地 Redis 实例上;其目的是演示当添加额外的回调层时事件驱动代码的复杂性,以及 async/await 如何帮助缓解这种情况。以下是我们该服务器的 Rust 版本,它使用了 redis crate(显式启用了 tokio 组件以支持异步调用):
use redis::AsyncTypedCommands;
use redis::aio::MultiplexedConnection;
use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
#[derive(Clone)]
struct AppState {
redis_connection: MultiplexedConnection,
}
const REDIS_URL: &str = "redis://127.0.0.1";
#[tokio::main]
async fn main() -> io::Result<()> {
let port = match std::env::args().nth(1) {
Some(s) => s,
None => "8070".to_string(),
};
let addr = format!("127.0.0.1:{port}");
let app_state = AppState {
redis_connection: {
redis::Client::open(REDIS_URL)
.map_err(io::Error::other)?
.get_multiplexed_async_connection()
.await
.map_err(io::Error::other)?
},
};
let listener = TcpListener::bind(addr).await?;
println!("Serving on port {port}");
loop {
let (socket, addr) = listener.accept().await?;
println!("connection received from {addr:?}");
let app_state = app_state.clone();
tokio::spawn(async move {
if let Err(e) = serve_client(socket, app_state).await {
eprintln!("error serving connection from {addr:?}: {e}");
} else {
println!("peer done {addr:?}");
}
});
}
}
async fn serve_client(
mut stream: tokio::net::TcpStream,
mut app_state: AppState,
) -> std::io::Result<()> {
let mut buf = [0u8; 1024];
loop {
let n = stream.read(&mut buf).await?;
if n == 0 {
// Connection closed by the client.
return Ok(());
}
// Parse read buf into u64.
let num = std::str::from_utf8(&buf[..n])
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?
.trim()
.parse::<u64>()
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
// Try the redis cache first. If found in cache, send the cached
// answer.
let cachekey = format!("primecache:{num}");
match app_state.redis_connection.get(cachekey.clone()).await {
Ok(Some(cached)) => {
stream.write_all((cached.clone() + "\n").as_bytes()).await?;
println!("cached num {num} is {cached}");
continue;
}
Ok(None) => {
// Not found in cache, continue to compute.
}
Err(error) => {
return Err(io::Error::other(error));
}
}
let answer = if isprime(num, true) {
"prime"
} else {
"composite"
};
// Save answer in cache and send it to the client.
app_state
.redis_connection
.set(cachekey, answer)
.await
.map_err(io::Error::other)?;
stream
.write_all((answer.to_string() + "\n").as_bytes())
.await?;
}
}
注意事项 (Notes)
- Because of the function color problem, the redis crate has a connection constructor specifically for async:
get_multiplexed_async_connection.- Here we have an example of shared state between tokio tasks — the Redis connection. Note that we don't require any particular synchronization because
MultiplexedConnectionisClone; cloning it to different tasks is safe — and in fact that's what we do for each new task. There's no magic here; if you look insideMultiplexedConnection, you'll see that it already has all the synchronization mechanisms implemented internally, as needed.- Due to the magic of async/await, the code in
serve_clientis nice and linear. We simplyawaiton the Redis call, and once it's back we continue with the rest of the handler. Since we're using an async Redis connection, in case waiting is required, control will be ceded to some other task that's not currently blocked on I/O.
- 由于函数颜色问题(function color problem),redis crate 专门为异步提供了一个连接构造函数:
get_multiplexed_async_connection。 - 这里我们有一个 tokio 任务之间共享状态的示例——Redis 连接。请注意,我们不需要任何特定的同步机制,因为
MultiplexedConnection实现了Clone;将其克隆到不同的任务是安全的——事实上,我们对每个新任务都是这样做的。这里没有什么魔术;如果你查看MultiplexedConnection的内部,你会发现它已经根据需要内部实现了所有同步机制。 - 得益于 async/await 的魔术,
serve_client中的代码非常清晰且呈线性。我们只需在 Redis 调用上执行await,一旦它返回,我们就继续执行处理程序的其余部分。由于我们使用的是异步 Redis 连接,如果需要等待,控制权将让渡给其他当前未被 I/O 阻塞的任务。
In conclusion, while Rust provides excellent support for async programming, it doesn't solve its inherent issues like function colors and the need for careful separation between blocking and non-blocking tasks. These issues are typically surmountable with some extra care, and async programming with Tokio in Rust is very popular due to its performance benefits.
总之,虽然 Rust 为异步编程提供了极佳的支持,但它并未解决异步编程固有的问题,例如函数颜色问题以及需要仔细区分阻塞和非阻塞任务的需求。只要格外小心,这些问题通常是可以克服的,并且由于其性能优势,在 Rust 中使用 Tokio 进行异步编程非常受欢迎。
代码 (Code)
All the code for this post is available on GitHub.
本文的所有代码均可在 GitHub 上获取。
脚注 (Footnotes)
[1] Rust makes a conscious design choice to keep its standard library minimal. In part 2, we've used Python's builtin stdlib thread pools; Rust also has several crates that implement thread pools, but I decided against using them because they obscure the underlying working of the code too much. Our sample implements a simple thread pool, but unfortunately still has to reach for an external crate to use a well-supported multi-consumer channel mechanism. [2] tokio is quite sophisticated; it manages a thread pool with an efficient work-stealing mechanism, onto which new tasks are spawned. Therefore, all shared data must be protected with mutexes or other synchronization primitives, and Rust's traits like SendandSyncplay an important role, where applicable.</tbody [3] Having to code this alternative just for the sake of async invocation is a classical example of the function color problem.
| [1] | Rust 在设计上有意保持其标准库的精简。在第 2 部分中,我们使用了 Python 内置的标准库线程池;Rust 也有几个实现线程池的 crate,但我决定不使用它们,因为它们将代码的底层工作机制掩盖得太深了。我们的示例实现了一个简单的线程池,但不幸的是,为了使用支持良好的多消费者通道机制,仍然不得不求助于外部 crate。 |
| [2] | tokio 非常成熟复杂;它管理着一个具有高效工作窃取(work-stealing)机制的线程池,新的任务就在其上生成。因此,所有共享数据必须用互斥锁或其他同步原语进行保护,而 Rust 的 Send 和 Sync 等 trait 在适用的情况下扮演着重要的角色。 |
| [3] | 仅仅为了异步调用的缘故而不得不编写这份替代代码,正是函数颜色问题的一个经典范例。 |