文章目录
前言
这一篇介绍Rust中如何进行并发开发~
相关知识点包括线程、通道、互斥锁、并发相关trait以及异步并发介绍。
13.1 Rust中的线程介绍
不同的编程语言线程实现通常有两种方式:
名称 | 具体实现 | 运行时大小 |
---|---|---|
1:1模型 | 过调用OS的API来创建线程,一个系统线程对应一个程序线程 | 需要较小的运行时 |
M:N模型 | 语言自己实现的线程(绿色线程),M个绿色线程对应N个系统线程 | 需要更大的运行时 |
- 1:1模型和M:N模型对比,前者具有更小的运行时,性能更高,后者对线程运行更精细的控制及更低的上下文切换成本。
- Rust标准库仅提供1:1模型的线程,当然第三方库应该也有提供绿色线程。
- 对于其他编程语言,它们通常提供的线程被称为绿色(green)线程,使用绿色线程的语言会在不同数量的OS线程的上下文中执行它们。
- Rust标准库中的
std::thread
模块用于支持多线程编程,提供了很多方法来创建线程、管理线程和结束线程。
13.2 Rust线程使用
13.2.1 线程的新建、阻塞和休眠
- 创建新线程,通过
thread::JoinHandle
:use std::thread; // 返回的handle值类型为thread::JoinHandle let handle = thread::spawn(|| { /* 线程中的逻辑 */ })
- 阻塞线程,通过使用
thread::JoinHandle
结构体的join
方法:let handle = thread::spawn(|| { /* ... */}) handle.join().unwrap();
JoinHandle
是一个拥有所有权的值,调用其的join
方法会阻塞当前线程,并等待JoinHandle
所持有的的线程结束;
- 当前线程休眠:
std::thread::sleep();
- 示例:
use std::thread; use std::time::Duration; fn main() { // 创建第一个子线程 let thread_1 = thread::spawn(|| { for i in 1..=5 { println!("number {} from the spawned_1 thread!", i); thread::sleep(Duration::from_secs(2)); } }); // 创建第二个子线程 let thread_2 = thread::spawn(|| { for i in 1..=5 { println!("number {} from the spawned_2 thread!", i); thread::sleep(DUration::from_secs(4)); } }); // 主线程执行的代码 for i in 1..=5 { println!("number {} from the main thread!", i); thread::sleep(Duration::from_secs(8)); } // 阻塞主线程直到子线程执行至结束 thread_1.join().unwrap(); thread_2.join().unwrap(); }
13.2.2 线程与 move 闭包
move
闭包通常和thread::spawn
函数一起使用,它允许你使用其它线程的数据。- 在创建线程使用
move
,可以把当前线程中的值的所有权转移到新创建的线程中。 - 闭包虽然可以借用上下文中值,然而在线程场景下,直接在闭包中借用当前线程上下文的值,编译器无法判断新建的线程会借用该值多久,无法知道借用期间该值是否会一直有效,因此在使用
spwan
配合闭包创建线程时,如果需要在子线程中使用主线程的值,可以通过在闭包之前增加move
关键字,强制闭包获取闭包内使用的值的所有权。use std::thread; fn main() { let v = vec![1, 2, 3]; let handle = thread::spawn(move || { println!("Here's a vector: {:?}", v); }); // 如果这里drop,会报错 // drop(v); handle.join().unwrap(); }
- 使用
move
关键字,线程会获得其内部使用到的上下文的值的所有权,因此v
的所有权归为子线程。 - 如果此时在主线程中
drop(v)
,由于v
的所有权已经归为子线程了,因此drop
操作被认为是非法的。
- 使用
13.2.3 线程池
题外话
Rust的标准库并没有提供线程池实现,需要使用第三方库。
可能会有人好奇现在居然还有编程语言不提供像线程池这种这么基础的实现,Rust太原始了吧。
之前有看过相关文章,Rust之所以很多基础功能都没有提供,是因为Rust作为系统级别的编程语言,会被用在不同场景下,而不同的应用场景对同一种功能的要求会有区别,比如说开发Web时使用的线程池和开发嵌入式时使用的线程池因为资源的不同设计上应该有所侧重。类似的还有字符串、线程、序列化等基础功能。当然,我觉得Rust标准库中还是可以提供一个通用实现,对于特殊场景再让开发者选用特殊的实现即可,这样可能更能降低Rust的入门成本,这一点像Python、Golang等就就做得不错,标准包中就有线程池、JSON、Http服务器等的实现,方便开发者入门。
使用
- 先在
Cargo.toml
中引入threadpool
:[dependencies] threadpool = "1.8.1"
- 查了下文档,threadpool应该只能创建固定线程数的线程池,属于比较基础的线程池实现,实际使用应该还是用更高级的实现。
- 创建一个线程池:
use threadpool::ThreadPool; let thread_pool_size = 10; let pool = ThreadPool::new(thread_pool_size);
- 使用:
use threadpool::ThreadPool; fn main() { let pool = ThreadPool::new(3); for i in 1..=5 { pool.execute(move || { println!("number {} from the spawned_1 thread!", i); }); } for i in 1..=5 { pool.execute(move || { println!("number {} from the spawned_2 thread!", i); }); } for i in 1..=5 { println!("number {} from the main thread!", i); } pool.join(); }
13.3 消息传递(mpsc)
- 消息传递是一种很流行且能保证安全并发的技术。
- 线程(或Actor)通过彼此发送消息(数据)来进行通信。比如Golang就推荐不要用共享内存来通信,要用通信来共享内存。
- Rust标准库提供的消息传递并发实现是 通道(channel)。
- 通道由两部分组成:发送者(transmitter)和接收者(receiver),由发送者发送消息,接收者检查接收端收到的消息。当发送者或接收者任一被丢弃时可以认为通道被 关闭(closed)了。
- 通道在标准库中的位置:
std::sync::mpsc
:多个生产者,单个消费者(multiple producer, single consumer)。
13.3.1 通道的基础用法
- Rust 标准库实现通道的方式意味着一个通道可以有多个产生值的 发送(sending)端,但只能有一个消费这些值的 接收(receiving)端。
- 创建通道、发送者、接收者相关API:
use std::sync::mpsc; // 创建通道,返回一个数组 // tx: 发送者 transmitter 的缩写 // rx: 接收者 receiver 的缩写 let (tx, rx) = mpsc::channel(); // 发送者发送消息,会发生所有权转移 // send 返回 Result<T,E> // 如果接收端已经被丢弃了,则没有接收对象,发送操作会返回错误。 let v = /*传输消息*/; let res: Result<T, >tx.send(v); // 接收者阻塞线程接收消息,当接收到Ok表示收到有效值,通道还没有关闭 let r1 = rx.recv().unwrap(); // 接收者不阻塞线程接收消息,当接收到Ok表示收到有效值 let r2 = rx.try_recv().unwrap(); // 接收者连续接收消息,当通道关闭后迭代器结束 for received in rx { println!("Got: {}", received); }
- 示例:
use std::thread; use std::sync::mpsc; use std::time::Duration; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let vals = vec![ String::from("hi"), String::from("from"), String::from("the"), String::from("thread"), ]; for val in vals { tx.send(val).unwrap(); thread::sleep(Duration::from_secs(1)); } }); for received in rx { println!("Got: {}", received); } }
13.3.2 创建多个发送者
- 默认情况下通过
mpsc::channel()
方法得到的只有一个发送者和一个接收者。当需要多个发送者给同一个接收者发送消息时,需要使用mpsc::Sender::clone()
方法克隆发送者,其中参数为这个发送者值的引用:// 创建通道 let (tx, rx) = mpsc::channel(); // 创建多个发送者 let tx1 = mpsc::Sender::clone(&tx); let tx2 = mpsc::Sender::clone(&tx); let tx3 = mpsc::Sender::clone(&tx);
- 示例:
let (tx, rx) = mpsc::channel(); let tx1 = mpsc::Sender::clone(&tx); thread::spawn(move || { let vals = vec![ String::from("hi"), String::from("from"), String::from("the"), String::from("thread"), ]; for val in vals { tx1.send(val).unwrap(); thread::sleep(Duration::from_secs(1)); } }); thread::spawn(move || { let vals = vec![ String::from("more"), String::from("messages"), String::from("for"), String::from("you"), ]; for val in vals { tx.send(val).unwrap(); thread::sleep(Duration::from_secs(1)); } }); for received in rx { println!("Got: {}", received); }
13.4 互斥锁(Mutex)
不同编程语言都提供了互斥锁作为并发的重要工具,Rust也不例外,互斥锁被包含在标准库中:
use std::sync::Mutex;
13.4.1 基础用法
- 互斥锁(mutex, mutual exclusion):被加互斥锁的资源在任意时刻,其只允许被一个线程访问。
- 当一个线程需要访问被加锁的数据,操作流程如下:
- 获取互斥锁;
- 访问数据;
- 归还锁;
- 基础API:
use std::sync::Mutex; // 初始化,将需要加锁的数据传入new方法 let data = /* 数据 */; let m = Mutex::new(data); // 另一个线程获得锁,得到数据的引用 let mut p = m.lock().unwrap(); // 使用数据 *p
Mutex<T>
是一个智能指针,更准确的说:lock
调用返回一个叫作MutexGuard
的智能指针。MutexGuard
实现了Deref
指向其内部数据,同时MutexGuard
也实现了Drop
,因此当MutexGuard
离开作用域时,会自动释放锁。
13.4.2 多个线程共用一个互斥锁
- 使用互斥锁的目的是共享状态,然而根据前面move闭包的说明可知,多个子线程中直接使用主线程的互斥锁是无法通过编译的,因为move闭包只能将互斥锁移动到一个子线程中。合理的方法是使用
Arc
(原子引用计数)配合Mutex
(互斥锁)。 - 通过将一个互斥锁值封装进一个引用计数指针值中,并将引用计数指针克隆多份传到不同的子线程中,间接使用互斥锁,可以实现多个线程共同使用一个互斥锁。但是这里有另外一个问题:
Rc<T>
是线程不安全的,无法在线程间共享,此时可以使用原子引用计数Arc<T>
。 - 原子引用计数
Arc<T>
(std::sync::Arc
)是Rc<T>
的线程安全版,a代表原子性(atomic)。Arc<T>
和Rc<T>
基础功能一模一样,API基本一样,标准库类型不默认使用Arc<T>
因为需要性能作为代价。 - 示例:
use std::sync::{Mutex, Arc}; use std::thread; fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap()); }
13.5 Sync
和 Send
trait
- Rust语言的并发特性比较少,前面提到的并发特性都来自标准库而不是语言本身。
Sync
和Send
trait 位于std::marker
中,是语言层面的并发概念相关trait。std::marker::Sync
和std::marker::Send
,这两个trait没有定义任何属性,只是两个标记trait。
13.5.1 Send
trait
Send
标记 trait 表明类型的所有权可以在线程间传递。- 几乎所有的 Rust 类型都实现了
Send
,但也有一些没有实现Send
的类型,比如Rc<T>
。 - 通过类型系统和trait bound确保不会将不安全的类型发送到不同的线程中。
- 任何完全由
Send
的类型组成的类型也会自动被标记为Send
。几乎所有基本类型都是Send
的,除了裸指针(原始指针,raw pointer)。
13.5.2 Sync
trait
Sync
标记 trait 表明一个实现了Sync
的类型可以安全的在多个线程中拥有其值的引用。- 对于任意类型
T
,如果T
是Sync
的,则&T
(T
的引用)是Send
的,这意味着其引用就可以安全的发送到另一个线程。类似于Send
的情况,基本类型是Sync
的,完全由Sync
的类型组成的类型也是Sync
的。 - 基础类型都是
Sync
,完全由Sync
类型组成的类型也是Sync
。 - 智能指针
Rc<T>
也不是Sync
的,出于其不是Send
相同的原因。RefCell<T>
和Cell<T>
系列类型不是Sync
的。RefCell<T>
在运行时所进行的借用检查也不是线程安全的。 Mutex<T>
是Sync
的。
13.5.3 手动实现 Send
和 Sync
并不安全
- 通常并不需要手动实现
Send
和Sync
trait,因为由Send
和Sync
的类型组成的类型,自动就是Send
和Sync
的。因为它们是标记 trait,甚至都不需要实现任何方法。它们只是用来加强并发相关的不可变性的。 - 手动实现这些标记 trait 涉及到编写不安全的 Rust 代码,在创建新的由不是
Send
和Sync
的部分构成的并发类型时需要多加小心,以确保维持其安全保证。
13.6 异步并发
13.6.1 async
和 .await
- Rust通过future并发模型和
async/.await
方案来实现异步并发。 async/.await
是Rust内置语法,可以使异步代码像普通代码那样易于编写。async
:通常与fn函数定义一起使用,用于创建异步函数,返回值的类型实现了Future
trait,而这个返回值需要由执行器来运行。.await
:不阻塞当前线程,异步等待future完成。在当前future无法执行时,.await
将调度当前future让出线程控制权,由其他future继续执行。这个语法只有future对象才能调用,且必须在async
函数内使用。
- 示例:
use futures::executor::block_on; use futures::join; async fn learn_data_structure() -> DataStructure { ... } async fn learn_algorithm(data_structure: DataStructure) { ... } async fn learn_rust() { ... } async fn learn_data_structure_and_algorithm() { let data_structure = learn_data_structure().await; learn_algorithm(data_structure).await; } async fn async_main() { let future1 = learn_data_structure_and_algorithm(); let future2 = learn_rust(); join!(future1, future2); } fn main() { block_on(async_main()); }
async-std库
- 需要添加依赖:
# Cargo.toml [dependencies] async-std = "1.6.3"
task::spawn
函数:生成异步任务。use async_std::task; let handle = task::spawn(async { 1 + 2 }); assert_eq!(handle.await, 3);
task::block_on
函数:阻塞当前线程直到任务执行结束。use async_std::task; fn main() { task::block_on(async { println!("hello async"); }); }
task::sleep
函数:通过非阻塞的方式让任务等待一段时间再执行。use async_std::task; use std::time::Duration; task::sleep(Duration::from_secs(1)).await;