0%

如果我们正在开发一个聊天室,并且使用线程处理每个连接,我们的代码可能看起来像下面这个样子:

1
2
3
4
5
6
7
8
9
use std::{net, thread};
let listener = net::TcpListener::bind(address)?;
for socket_result in listener.incoming() {
let socket = socket_result?;
let groups = chat_group_table.clone();
thread::spawn(|| {
log_error(serve(socket, groups));
});
}

对于每个新连接,这都会产生一个运行 serve 函数的新线程,该线程能够专注于管理单个连接的处理。

这很好用,但是如果突然用户达到成千上万时,线程堆栈增长到 100 KiB 或这更多时,这可能要花费几个GB的内存。线程对于在多个处理器之间分配工作是非常好的一种形式,但是它们的内存需求使得我们在使用时要非常小心。

不过可以使用 Rust 异步任务在单个线程或工作线程池上并发运行许多独立活动。异步任务类似于线程,但创建速度更快,并且内存开销比线程少一个数量级。在一个程序中同时运行数十万个异步任务是完全可行的。当然,应用程序可能仍会受到网络带宽、数据库速度、计算或工作固有内存要求等其他因素的限制,但内存开销远没有线程那么多。

一般来说,异步 Rust 代码看起来很像普通的多线程代码,除了涉及到的 I/O 操作,互斥锁等阻塞操作需要稍微的不同处理。之前代码的异步版本如下所示:

1
2
3
4
5
6
7
8
9
10
11
use async_std::{net, task};

let listener = net::TcpListener::bind(address).await?;
let mut new_connections = listener.incoming();
while let Some(socket_result) = new_connections.next().await {
let socket = socket_result?;
let groups = chat_group_table.clone();
task::spawn(async {
log_error(serve(socket, groups).await);
});
}

这使用 async_stdnettask模块,并在可能阻塞的调用之后添加 .await。但整体结构与基于线程的版本相同。

本节的目标不仅是帮助编写异步代码,而且还以足够详细的方式展示它的工作原理,以便可以预测它在应用程序中的表现,并了解它最有价值的地方。

  • 为了展示异步编程的机制,我们列出了涵盖所有核心概念的最小语言特性集:futures、异步函数、await 表达式、task 以及 block_onspawn_local executor

  • 然后我们介绍异步代码块和 spawn executor。这些对于完成实际工作至关重要,但从概念上讲,它们只是我们刚刚提到的功能的变体。在此过程中,我们会可能会遇到一些异步编程特有的问题,但是需要学习如何处理它们;

  • 为了展示所有这些部分的协同工作,我们浏览了聊天服务器和客户端的完整代码,前面的代码片段是其中的一部分;

  • 为了说明原始 futuresexecutors 是如何工作的,我们提供了 spawn_blockingblock_on 的简单但功能性的实现;

  • 最后,我们解释了 Pin 类型,它在异步接口中不时出现,以确保安全使用异步函数和 futures

阅读全文 »

Rust 提供了一种非常好的并发使用方法,它不强制所有程序采用单一风格,而是通过安全地支持多种风格,并由编译器强制执行。我们将介绍三种使用 Rust 线程的方法:

  • Fork-join 并行;
  • 通道(Chanel);
  • 共享可变状态;

在此过程中,将使用到目前为止所学的有关 Rust 语言的所有内容,Rust 对引用、可变性和生命周期的关注在单线程程序中足够有价值,但在并发编程中,这些规则的真正意义变得显而易见。

Fork-Join Parallelism

最简单的用于多线程的案例是处理互不相干的任务,例如,我们要处理大量的文档,可能会这样写:

1
2
3
4
5
6
7
8
fn process_files(filenames: Vec<String>) -> io::Result<()> {
for document in filenames {
let text = load(&document)?; // read source file
let results = process(text); // compute statistics
save(&document, results)?; // write output file
}
Ok(())
}
阅读全文 »

Rust 用于输入和输出的标准库功能围绕三个Trait组织:ReadBufReadWrite

  • 实现 Read 的值具有面向字节的输入的方法,他们被称为 Reader

  • 实现 BufRead 的值是缓冲读取器,它们支持 Read 的所有方法,以及读取文本行等的方法;

  • 实现 Write 的值支持面向字节和UTF-8 文本输出,它们被称为 Writer

在本节中,将解释如何使用这些Trait及其方法,涵盖图中所示的读取器和写入器类型,并展示与文件、终端和网络交互的其他方式。

ReadersWriters

Readers 是内容输入源,可以从哪里读取字节。例如:

Writers 是那些你可以把值写入的地方,例如:

  • 使用 std::fs::File::create 创建的文件;

  • 基于网络连接 std::net::TcpStream 传输数据;

  • std::io::stdout()std::io:stderr() 可以用于向标准输出和标准错误写入内容;

  • std::io::Cursor<Vec<u8>> 类似,但允许读取和写入数据,并在vector中寻找不同的位置;

  • std::io::Cursor<&mut [u8]> 和上面的类似,但是不能增长内部的 buffer,因为它仅仅是已存在的字节数组的引用;

由于ReaderWriter有标准的 Traitstd::io::Readstd::io::Write),编写适用于各种输入或输出通道的通用代码是很常见的。 例如,这是一个将所有字节从任何读取器复制到任何写入器的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::io::{self, ErrorKind, Read, Write};

const DEFAULT_BUF_SIZE: usize = 8 * 1024;

pub fn copy<R: ?Sized, W: ?Sized>(reader: &mut R, writer: &mut W) -> io::Result<u64>
where
R: Read,
W: Write,
{
let mut buf = [0; DEFAULT_BUF_SIZE];
let mut written = 0;
loop {
let len = match reader.read(&mut buf) {
Ok(0) => return Ok(written),
Ok(len) => len,
Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
};
writer.write_all(&buf[..len])?;
written += len as u64;
}
}

这是 Rust 标准库 std::io::copy() 的实现,因为它是泛型的,所以可以把数据从 File 复制到 TcpStream,或者从 Stdin 到内存中的 Vec<u8>

阅读全文 »

UnicodeASCII 匹配所有 ASCII 字符,从 00x7f。例如,都将字符 * 分配给码点 42。类似地,Unicode00xff 分配给与 ISO/IEC 8859-1 字符集相同的字符,用于西欧语言的 8ASCII 超集。Unicode 将此码点范围称为 Latin-1 代码块。

因为 UnicodeLatin-1 的超集,所以从 Latin-1 转换到 Unicode 是完全允许的:

1
2
3
fn latin1_to_char(latin1: u8) -> char {
latin1 as char
}

假设码点在 Latin-1 范围内,反向转换也很简单:

1
2
3
4
5
6
7
fn char_to_latin1(c: char) -> Option<u8> {
if c as u32 <= 0xff {
Some(c as u8)
} else {
None
}
}

RustStringstr 类型都是使用 UTF-8 编码格式,它是一种变长编码,使用14个字节对字符进行编码。有效的 UTF-8 序列有两个限制。首先,对于任何给定码点,只有最短的编码被认为是有效的,也就是不能花费4个字节来编码一个适合3个字节的码点。 此规则确保给定代码点只有一个 UTF-8 编码。其次,有效的 UTF-8 不得编码为 0xd8000xdfff 或超过 0x10ffff 的数字:这些数字要么保留用于非字符目的,要么完全超出 Unicode 的范围。

阅读全文 »

Rust 标准库包含几个集合,用于在内存中存储数据的泛型类型。我们已经在前面使用了集合,例如 VecHashMap。在本章中,我们将详细介绍这两种类型的方法,以及其他6个标准集合。

Rust 一共有8个标准集合类型,它们都是泛型:

  • Vec<T>:一个可增长的、堆分配的 T 类型值数组;

  • VecDeque<T>:与 Vec<T> 类似,但更适合用作先进先出队列,它支持在列表的前面和后面有效地添加和删除值;

  • BinaryHeap<T>:一个优先队列,BinaryHeap 中的值是有组织的,所以它总是有效地找到并删除最大值;

  • HashMap<K, V>:键值对表,通过键查找值很快,item 以任意顺序存储;

  • BTreeMap<K, V>:与 HashMap<K, V> 类似,但它保持entries按键排序。 BTreeMap<String, i32> 以字符串比较顺序存储其entries。除非需要entries保持排序,否则 HashMap 更快;

  • HashSet<T>:一组 T 类型的值。添加和删除值很快,查询给定值是否在集合中也很快;

  • BTreeSet<T>:与 HashSet<T> 类似,但它保持元素按值排序。 同样,除非需要对数据进行排序,否则 HashSet 更快;

阅读全文 »

迭代器是产生一系列值的值,通常用于循环操作。Rust 的标准库提供了遍历vector、字符串、哈希表和其他集合的迭代器,还提供了从输入流生成文本行、网络连接、用于多线程之间值传递的迭代器,Rust 的迭代器灵活、富有表现力且高效。

Rust 中,std::iter::Iteratorstd::iter::IntoIterator 是实现迭代器的基础。

1
2
3
4
5
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
...
}

迭代器可以是任何实现了 Iterator 的值,Item 是每次迭代产生的值,next 要么返回 Some(v)v 是下一个值,要么返回 None 表示迭代结束。

想要被迭代的类型也可以实现 std::iter::IntoIterator,它的 into_iter 返回一个迭代器:

1
2
3
4
5
pub trait IntoIterator {
type Item;
type IntoIter: Iterator;
fn into_iter(self) -> Self::IntoIter;
}

我们常用的 for 循环仅仅是先调用 into_iter 生成一个迭代器,然后重复调用迭代器的 next 方法,直到 next 返回 None,预示着迭代结束。

阅读全文 »

很多语言中都有闭包,有的叫匿名函数,有的叫 lambda 函数,用一个最简单的例子演示闭包,例如 sort_by_key 传入的就是一个闭包函数:

1
2
3
4
5
6
7
8
9
10
struct City {
name: String,
population: i64,
country: String,
}

fn main() {
let mut cities = Vec::<City>::new();
cities.sort_by_key(|city| -city.population)
}
阅读全文 »

Rust 中的 Trait 可以分为三类:

  1. 语言扩展 Trait:主要用于运算符重载,我们可以将常用的运算符使用在自己的类型之中,只要相应的 Trait 即可,例如 EqAddAssignDereDrop 以及 FromInto 等;

  2. 标记类型 Trait:这些 Trait 主要用于绑定泛型类型变量,以表达无法以其他方式捕获的约束,这些包括 SizedCopy

  3. 剩下的主要是一些为解决常遇到的问题,例如:DefaultAsRefAsMutBorrowBorrowMutTryFromTryInto

Drop

Rust 中当一个值离开作用域时就会对它的内存进行清理,但是所有权转移不会,这类似于 C++ 中的析构函数。在 Rust 中我们也可以对析构的过程进行自定义,只要实现 std::ops::Drop 即可,在值需要清理的时候会自动调用 drop 函数,不能显示调用:

1
2
3
pub trait Drop {
fn drop(&mut self);
}

通常不需要实现 std::ops::Drop,除非定义了一个拥有 Rust 不知道的资源的类型。 例如,在 Unix 系统上,Rust 的标准库在内部使用以下类型来表示操作系统文件描述符:

1
2
3
struct FileDesc {
fd: c_int,
}

FileDescfd 字段只是程序完成时应该关闭的文件描述符的编号,c_inti32 的别名。标准库为 FileDesc 实现 Drop 如下:

1
2
3
4
5
6
7
impl Drop for FileDesc {
fn drop(&mut self) {
if self.close_on_drop {
unsafe { ::libc::close(self.fd); }
}
}
}

这里,libc::closeC 库关闭函数的 Rust 名称,Rust 仅能在 unsafe 块中调用 C 函数。

如果一个类型实现了 Drop,它就不能实现 Copy,如果类型可 Copy,则意味着简单的逐字节复制足以生成该值的独立副本,但是在相同的数据上多次调用相同的 drop 方法通常是错误的。

标准库预包含的 drop 函数可以显示删除一个值:

1
2
3
let v = vec![1, 2, 3];

drop(v); // explicitly drop the vector
阅读全文 »

我们可以为自定义的类型实现加减乘除操作,只要实现标准库的一些 Trait,这称之为运算符重载。下图是可以重载的运算符和需要对应实现的 Trait 列表:

阅读全文 »

编程中可能经常遇到要用相同的逻辑处理不同的类型,即使这个类型是还没出世的自定义类型。这种能力对于 Rust 来说并不新鲜,它被称为多态性,诞生于 1970 年代的编程语言技术,到现在为止仍然普遍。Rust 支持具有两个相关特性的多态性:Trait 和 泛型。

TraitRust 对接口或抽象基类的对照实现,它们看起来就像 JavaC# 中的接口:

1
2
3
4
5
6
7
8
trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;

fn flush(&mut self) -> Result<()>;

fn write_all(&mut self, buf: &[u8]) -> Result<()> { ... }
...
}

FileTcpStream 以及 Vec<u8> 都实现了 std::io::Write,这3个类型都提供了 .write().flush() 等等方法,我们可以使用 write 方法而不用关心它的实际类型:

1
2
3
4
5
6
use std::io::Write;

fn say_hello(out: &mut dyn Write) -> std::io::Result<()> {
out.write_all(b"hello world\n")?;
out.flush()
}

&mut dyn Write 的意思是任何实现了 Write 的可变引用,我们可以调用 say_hello 并且给他传递这样一个引用:

1
2
3
4
5
6
7
use std::fs::File;
let mut local_file = File::create("hello.txt")?;
say_hello(&mut local_file)?; // works

let mut bytes = vec![];
say_hello(&mut bytes)?; // also works
assert_eq!(bytes, b"hello world\n");

泛型函数就像 C++ 中模板函数,一个泛型函数或者类型可以用于许多不同类型的值:

1
2
3
4
5
6
7
8
9
/// Given two values, pick whichever one is less.
fn min<T: Ord>(value1: T, value2: T) -> T {

if value1 <= value2 {
value1
} else {
value2
}
}

<T: Ord> 意思是 T 类型必须实现 Ord,这称为边界,因为它设置了 T 可能是哪些类型,编译器为实际使用的每种类型 T 生成自定义机器代码。

阅读全文 »