研读Rust圣经解析——Rust learn-13(并发)
- 并发
- 创建新线程
- 使用 join 等待所有线程结束
- 线程获取环境所有权
- 通过消息传递传送数据
- 创建通道
- 发送|接收消息
- 隐式调用recv
- 共享状态并发
- 通过使用互斥器Mutex
- 创建Mutex
- 共享Mutex
- `Arc<T>`原子引用计数
- 使用 Sync 和 Send trait 的可扩展并发
- 通过Send允许在线程间转移所有权
- Sync 允许多线程访问
- 注意点:手动实现 Send 和 Sync 是不安全的
并发
并发编程(Concurrent programming),代表程序的不同部分相互独立的执行,而 并行编程(parallel programming)代表程序不同部分于同时执行,这两个概念随着计算机越来越多的利用多处理器的优势时显得愈发重要
已执行程序的代码在一个 进程(process)中运行,操作系统则会负责管理多个进程。在程序内部,也可以拥有多个同时运行的独立部分。这些运行这些独立部分的功能被称为 线程(threads)
创建新线程
我们可以通过spawn方法创建新线程,因为所需要的类型是FnOnce,所以我们可以直接传一个闭包进去
use std::thread;
fn main() {
thread::spawn(|| {
println!("new thread");
});
println!("origin thread");
}
使用 join 等待所有线程结束
join方法帮助我们阻塞住以保证线程执行结束,保证线程运行,因为我们不能保证线程是合适运行的也不能保证其运行顺序
use std::thread;
fn main() {
let t1 = thread::spawn(|| {
println!("new thread");
});
println!("origin thread");
t1.join();
println!("finish");
}
线程获取环境所有权
因为我们的线程常和闭包一起使用,所以也自然产生通过move关键字获取环境中取得的值的所有权并将这些值的所有权从一个线程传送到另一个线程
如下,这段程序是有问题的,在线程中并不能打印x的值,因为x的所有权还在main线程,所以我们应该通过使用move转移所有权
use std::thread;
fn main() {
let x = 5;
let t1 = thread::spawn(|| {
println!("new thread");
println!("{}", x);
});
println!("origin thread");
t1.join();
println!("finish");
}
修改:
use std::thread;
fn main() {
let x = 5;
let t1 = thread::spawn(move || {
println!("new thread");
println!("{}", x);
});
println!("origin thread");
t1.join();
println!("finish");
}
通过消息传递传送数据
一个日益流行的确保安全并发的方式是 消息传递(message passing),这里线程或 actor 通过发送包含数据的消息来相互沟通。为了实现消息传递并发,Rust 标准库提供了一个 信道(channel)实现。信道是一个通用编程概念,表示数据从一个线程发送到另一个线程。
创建通道
我们需要引入标准库中的sync中mpsc,使用channel方法构建一个通道
use std::thread;
use std::sync::mpsc;
fn main() {
let (sender, getter) = mpsc::channel();
}
发送|接收消息
发送:send方法,返回一个 Result<T, E>
类型,所以如果接收端已经被丢弃了,将没有发送值的目标,所以发送操作会返回错误
接受:recv方法,会阻塞主线程执行直到从信道中接收一个值。一旦发送了一个值,recv 会在一个 Result<T, E> 中返回它。当信道发送端关闭,recv 会返回一个错误表明不会再有新的值到来了。
use std::thread;
use std::sync::mpsc;
fn main() {
let (sender, getter) = mpsc::channel();
let msg = String::from("nihao");
sender.send(msg).unwrap_or(());
let t1 = thread::spawn(move||{
let g_msg = getter.recv().unwrap();
println!("{}", g_msg);
});
}
try_recv:不会阻塞,相反它立刻返回一个 Result<T, E>
:Ok 值包含可用的信息,而 Err 值代表此时没有任何消息。如果线程在等待消息过程中还有其他工作时使用 try_recv 很有用:可以编写一个循环来频繁调用 try_recv,在有可用消息时进行处理,其余时候则处理一会其他工作直到再次检查。
隐式调用recv
不再显式调用 recv 函数:而是将 rx 当作一个迭代器。对于每一个接收到的值,我们将其打印出来。当信道被关闭时,迭代器也将结束。
for received in rx {
println!("Got: {}", received);
}
共享状态并发
另一种方式是让多个线程拥有相同的共享数据
在某种程度上,任何编程语言中的信道都类似于单所有权,因为一旦将一个值传送到信道中,将无法再使用这个值。共享内存类似于多所有权:多个线程可以同时访问相同的内存位置。
通过使用互斥器Mutex
互斥器(mutex)是 mutual exclusion 的缩写,也就是说,任意时刻,其只允许一个线程访问某些数据。为了访问互斥器中的数据,线程首先需要通过获取互斥器的 锁(lock)来表明其希望访问数据。锁是一个作为互斥器一部分的数据结构,它记录谁有数据的排他访问权。因此,我们描述互斥器为通过锁系统 保护(guarding)其数据。
所以我们的步骤:
- 在使用数据之前尝试获取锁。
- 处理完被互斥器所保护的数据之后,必须解锁数据,这样其他线程才能够获取锁。
正确的管理互斥器异常复杂,这也是许多人之所以热衷于信道的原因。然而,在 Rust 中,得益于类型系统和所有权,我们不会在锁和解锁上出错。
创建Mutex
let m = Mutex::new(0);
共享Mutex
在这里我们可以通过使用lock获取锁,然后通过解引用对值进行修改
use std::thread;
use std::sync::{mpsc, Mutex};
fn main() {
let m = Mutex::new(0);
{
let mut num = m.lock().unwrap();
*num = 6;
}
println!("{:?}", m);
Arc<T>
原子引用计数
类似于Rc<T>
,但是Arc具备原子性,安全的应用于并发环境,通过使用Arc可以安全的在线程间共享值
use std::thread;
use std::sync::{Arc, mpsc, Mutex};
fn main() {
let m = Arc::new(Mutex::new(0));
let counter = Arc::clone(&m);
let handle = thread::spawn(move || {
let mut lock = counter.lock().unwrap();
*lock = 100;
});
handle.join().unwrap();
println!("{:?}", *m.lock().unwrap());
}
使用 Sync 和 Send trait 的可扩展并发
Rust 的并发模型中一个有趣的方面是:语言本身对并发知之 甚少。我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。由于不需要语言提供并发相关的基础设施,并发方案不受标准库或语言所限:我们可以编写自己的或使用别人编写的并发功能。
然而有两个并发概念是内嵌于语言中的:std::marker
中的Sync
和Send trait
。
通过Send允许在线程间转移所有权
- 只要实现Send trait类型的值就能将所有权在线程间传送
- 几乎所有Rust类型都是Send
Rc<T>
没有实现Send- 裸指针不是Send
Sync 允许多线程访问
- Sync 标记 trait 表明一个实现了 Sync 的类型可以安全的在多个线程中拥有其值的引用(对于任意类型 T,如果 &T(T 的不可变引用)是 Send 的话 T 就是 Sync 的)
- 基本类型都是Sync
Rc<T>
不是Sync
注意点:手动实现 Send 和 Sync 是不安全的
通常并不需要手动实现 Send 和 Sync trait,因为由 Send 和 Sync 的类型组成的类型,自动就是 Send 和 Sync 的。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变性的。