Rust高级之并发和智能指针
1 并发编程
安全高效的处理并发是 Rust 诞生的目的之一,主要解决的是服务器高负载承受能力。
并发(concurrent)的概念是指程序不同的部分独立执行,这与并行(parallel)的概念容易混淆,并行强调的是"同时执行",类似于:并发是两个队列用一个咖啡机交替服务;并行是两个队列配两个咖啡机同时服务。
1.1 线程
线程(thread)是一个程序中独立运行的一个部分,线程不同于进程(process)的地方是线程是程序以内的概念,程序往往是在一个进程中执行的。
在有操作系统的环境中进程往往被交替地调度得以执行,线程则在进程以内由程序进行调度。
由于线程并发很有可能出现并行的情况,所以在并行中可能遇到的死锁、延宕错误常出现于含有并发机制的程序。
为了解决这些问题,很多其它语言(如 Java、C#)采用特殊的运行时(runtime)软件来协调资源,但这样无疑极大地降低了程序的执行效率。C/C++ 语言在操作系统的最底层也支持多线程,且语言本身以及其编译器不具备侦察和避免并行错误的能力。
Rust 不依靠运行时环境,这一点像 C/C++ 一样,但 Rust 在语言本身就设计了包括所有权机制在内的手段来尽可能地把最常见的错误消灭在编译阶段
Rust 中通过 std::thread::spawn 函数创建新线程:
use std::thread;
use std::time::Duration;
fn spawn_function() {
for i in 0..5 {
println!("spawned thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
}
fn main() {
thread::spawn(spawn_function);
for i in 0..3 {
println!("main thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
}
或使用闭包的形式
fn thread_01() {
thread::spawn(|| {
for i in 0..5 {
println!("spawned thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 0..3 {
println!("main thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
}
运行结果:
main thread print 0
spawned thread print 0
spawned thread print 1
main thread print 1
main thread print 2
spawned thread print 2
spawned thread print 3
此程序有一个子线程,目的是打印 5 行文字,主线程打印三行文字,但很显然随着主线程的结束,spawn 线程也随之结束了,并没有完成所有打印。
1.2 join 方法
join:让当前线程(调用方)阻塞等待,直到目标线程执行完
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 0..5 {
println!("spawned thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 0..3 {
println!("main thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
}
1.3 move 强制所有权迁移
这是一个经常遇到的情况:
use std::thread;
fn main() {
let s = "hello";
let handle = thread::spawn(|| {
println!("{}", s);
});
handle.join().unwrap();
}
在子线程中尝试使用当前函数的资源,这一定是错误的,因为所有权机制禁止这种危险情况的产生,它将破坏所有权机制销毁资源的一定性,可以使用闭包的 move 关键字来处理:
use std::thread;
fn main() {
let s = "hello";
let handle = thread::spawn(move || {
println!("{}", s);
});
handle.join().unwrap();
}
1.4 消息传递
Rust 中一个实现消息传递并发的主要工具是通道(channel),通道有两部分组成,一个发送者(transmitter)和一个接收者(receiver),std::sync::mpsc 包含了消息传递的方法,
核心 API:
rx.recv():阻塞等待,直到收到消息;通道关闭返回Errrx.try_recv():非阻塞,立刻返回;没消息返回 Err(Empty)rx.recv_timeout(d):最多等 d时长,超时返回 Errtx.send(v):发送,接收端已关闭返回 Errtx.clone():克隆发送端,实现多生产者
recv()不是内置 join,join等待线程终结,recv只等一条消息——消息到了就放行,线程是死是活它不管。想等线程结束,还得显式 join()
| join() | recv() | |
|---|---|---|
| 等什么 | 线程执行完毕 | 通道里出现一条消息 |
| 触发解除 | 线程退出 | 对端调用 send |
| 返回什么 | 线程的最终返回值 | 一条消息 |
| 能调几次 | 一次(handle被消耗) | 多次,持续收 |
| 线程状态 | 结束后才放行 | 线程可能还活着 |
1.4.1 单生产者
use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel(); //创建通道,返回 (发送者,接收者)
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();//发送
// println!("{}", s); //错误 s已被 send转移走
});
let received = rx.recv().unwrap();//接收(阻塞等待)
println!("Got: {}", received);
}
子线程获得了主线程的发送者 tx,并调用了它的 send 方法发送了一个字符串,然后主线程就通过对应的接收者 rx 接收到了。
mpsc = multiple producer, single consumer(多生产者,单消费者)。
1.4.2 多生产者
let (tx, rx) = mpsc::channel();
let tx2 = tx.clone(); //克隆出第二个发送端
thread::spawn(move || tx.send("线程1").unwrap());
thread::spawn(move || tx2.send("线程2").unwrap());
//用迭代器方式接收,所有发送端关闭后循环自动结束
for msg in rx {
println!("收到: {}", msg);
}
2 智能指针
2.1 简介
智能指针(Smart pointers)是一种在 Rust 中常见的数据结构,它们提供了额外的功能和安全性保证,以帮助管理内存和数据。
在 Rust 中,智能指针是一种封装了对动态分配内存的所有权和生命周期管理的数据类型,智能指针通常封装了一个原始指针,并提供了一些额外的功能,比如引用计数、所有权转移、生命周期管理等。
智能指针和普通引用的区别:
| 普通引用 | 智能指针 | |
|---|---|---|
| 所有权 | 借用,不拥有 | 拥有数据 |
| 指针本身的位置 | 通常在栈上 | 也在栈上(它只是个局部变量) |
| 指向数据的位置 | 不一定:可在栈上、堆上或静态区 | 通常在堆上(Box::new总是堆分配) |
| 自动释放 | 离开作用域不释放指向的数据 | 离开作用域通过 Drop 释放指向的堆数据 |
| 实现 | 语言内置 | 用 struct实现 Deref/Drop trait |
在 Rust 中,标准库提供了几种常见的智能指针类型,例如 Box、Rc、Arc 和 RefCell。
智能指针的使用场景:
- 当需要在堆上分配内存时,使用
Box<T>。 - 当需要多处共享所有权时,使用
Rc<T> 或 Arc<T>。 - 当需要内部可变性时,使用
RefCell<T> - 当需要线程安全的共享所有权时,使用
Arc<T> - 当需要互斥访问数据时,使用
Mutex<T> - 当需要读取-写入访问数据时,使用
RwLock<T> - 当需要解决循环引用问题时,使用
Weak<T>
2.2 智能指针分类
2.2.1 Box<T>
Box<T> 是 Rust 中最简单的智能指针之一,它允许在堆上分配一块内存,并将值存储在这个内存中。
Box<T> 让递归类型(如链表、树)有已知大小,或者让大对象放在堆上,避免栈溢出
let b = Box::new(5);
println!("b = {}", b);
2.2.2 Rc<T>
Rc<T>(引用计数指针)允许多个所有者共享数据,它使用引用计数来跟踪数据的所有者数量,并在所有者数量为零时释放数据。Rc<T> 适用于单线程环境下的数据共享。
use std::rc::Rc;
let data = Rc::new(5);
let data_clone = Rc::clone(&data);
2.2.3 Arc<T>
Arc<T>(原子引用计数指针)与 Rc<T> 类似,但是可以安全地在多线程环境中共享数据,因为它使用原子操作来更新引用计数。
use std::sync::Arc;
let data = Arc::new(5);
let data_clone = Arc::clone(&data);
2.2.4 RefCell<T>
RefCell<T> 允许在运行时检查借用规则,它使用内部可变性来提供了一种安全的内部可变性模式,允许在不可变引用的情况下修改数据。但是,RefCell<T> 只能用于单线程环境。
use std::cell::RefCell;
let data = RefCell::new(5);
let mut borrowed_data = data.borrow_mut();
*borrowed_data = 10;
2.2.5 Mutex<T>
Mutex<T> 是一个互斥锁,它保证了在任何时刻只有一个线程可以访问 Mutex 内部的数据。
use std::sync::Mutex;
let m = Mutex::new(5);
let mut data = m.lock().unwrap();
2.2.6 RwLock<T>
RwLock<T> 是一种读取-写入锁,允许多个读取者同时访问数据,但在写入时是排他的。
use std::sync::RwLock;
let lock = RwLock::new(5);
let read_guard = lock.read().unwrap();
2.2.7 Weak<T>
Weak<T> 是 Rc<T> 的非拥有智能指针,它不增加引用计数,用于解决循环引用问题。
use std::rc::{Rc, Weak};
let five = Rc::new(5);
let weak_five = Rc::downgrade(&five);
2.3 智能指针的生命周期管理
智能指针可以帮助管理数据的生命周期,当智能指针被销毁时,它们会自动释放内存,从而避免了内存泄漏和野指针的问题。此外,智能指针还允许在创建时指定特定的析构函数,以实现自定义的资源管理。
下面是一个简单的 Rust 智能指针完整实例,该示例使用 Rc<T> 智能指针实现了一个简单的引用计数功能,并演示了多个所有者共享数据的情况。
// 引入所需的依赖库
use std::rc::Rc;
// 定义一个结构体,用于存储数据
#[derive(Debug)]
struct Data {
value: i32,
}
// 主函数
fn main() {
// 创建一个 Rc 智能指针,共享数据
let data = Rc::new(Data { value: 5 });
// 克隆 Rc 智能指针,增加数据的引用计数
let data_clone1 = Rc::clone(&data);
let data_clone2 = Rc::clone(&data);
// 输出数据的值和引用计数
println!("Data value: {}", data.value);
println!("Reference count: {}", Rc::strong_count(&data));
// 打印克隆后的 Rc 智能指针
println!("Data clone 1: {:?}", data_clone1);
println!("Data clone 2: {:?}", data_clone2);
}
以上代码中,我们首先定义了一个 Data 结构体,用于存储一个整数值。然后在 main 函数中创建了一个 Rc<Data> 智能指针,用于共享数据。接着通过 Rc::clone 方法克隆了两个智能指针,增加了数据的引用计数。最后打印了数据的值、引用计数和克隆后的智能指针。

浙公网安备 33010602011771号