💻博主现有专栏:
C51单片机(STC89C516),c语言,c++,离散数学,算法设计与分析,数据结构,Python,Java基础,MySQL,linux,基于HTML5的网页设计及应用,Rust(官方文档重点总结),jQuery,前端vue.js,Javaweb开发,Python机器学习等
🥏主页链接:
目录
在某种程度上,任何编程语言中的信道都类似于单所有权,因为一旦将一个值传送到信道中,将无法再使用这个值。共享内存类似于多所有权:多个线程可以同时访问相同的内存位置。第十五章介绍了智能指针如何使得多所有权成为可能,然而这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。Rust 的类型系统和所有权规则极大的协助了正确地管理这些所有权。作为一个例子,让我们看看互斥器,一个更为常见的共享内存并发原语。
🎯互斥器一次只允许一个线程访问数据
互斥器(mutex)是 mutual exclusion 的缩写,也就是说,任意时刻,其只允许一个线程访问某些数据。为了访问互斥器中的数据,线程首先需要通过获取互斥器的 锁(lock)来表明其希望访问数据。锁是一个作为互斥器一部分的数据结构,它记录谁有数据的排他访问权。因此,我们描述互斥器为通过锁系统 保护(guarding)其数据。
互斥器以难以使用著称,因为你不得不记住:
- 在使用数据之前尝试获取锁。
- 处理完被互斥器所保护的数据之后,必须解锁数据,这样其他线程才能够获取锁。
作为一个现实中互斥器的例子,想象一下在某个会议的一次小组座谈会中,只有一个麦克风。如果一位成员要发言,他必须请求或表示希望使用麦克风。一旦得到了麦克风,他可以畅所欲言,然后将麦克风交给下一位希望讲话的成员。如果一位成员结束发言后忘记将麦克风交还,其他人将无法发言。如果对共享麦克风的管理出现了问题,座谈会将无法如期进行!
正确的管理互斥器异常复杂,这也是许多人之所以热衷于信道的原因。然而,在 Rust 中,得益于类型系统和所有权,我们不会在锁和解锁上出错。
🎯Mutex<T>的API
作为展示如何使用互斥器的例子,让我们从在单线程上下文使用互斥器开始
use std::sync::Mutex; fn main() { let m = Mutex::new(5); { let mut num = m.lock().unwrap(); *num = 6; } println!("m = {:?}", m); }
像很多类型一样,我们使用关联函数
new
来创建一个Mutex<T>
。使用lock
方法获取锁,以访问互斥器中的数据。这个调用会阻塞当前线程,直到我们拥有锁为止。如果另一个线程拥有锁,并且那个线程 panic 了,则
lock
调用会失败。在这种情况下,没人能够再获取锁,所以这里选择unwrap
并在遇到这种情况时使线程 panic。一旦获取了锁,就可以将返回值(在这里是
num
)视为一个其内部数据的可变引用了。类型系统确保了我们在使用m
中的值之前获取锁。m
的类型是Mutex<i32>
而不是i32
,所以 必须 获取锁才能使用这个i32
值。我们是不会忘记这么做的,因为反之类型系统不允许访问内部的i32
值。正如你所怀疑的,
Mutex<T>
是一个智能指针。更准确的说,lock
调用 返回 一个叫做MutexGuard
的智能指针。这个智能指针实现了Deref
来指向其内部数据;其也提供了一个Drop
实现当MutexGuard
离开作用域时自动释放锁,这正发生于示例 16-12 内部作用域的结尾。为此,我们不会忘记释放锁并阻塞互斥器为其它线程所用的风险,因为锁的释放是自动发生的。
🎃在线程间共Mytex<T>
现在让我们尝试使用
Mutex<T>
在多个线程间共享值。我们将启动十个线程,并在各个线程中对同一个计数器值加一,这样计数器将从 0 变为 10。示例 16-13 中的例子会出现编译错误,而我们将通过这些错误来学习如何使用Mutex<T>
,以及 Rust 又是如何帮助我们正确使用的。use std::sync::Mutex; use std::thread; fn main() { let counter = Mutex::new(0); let mut handles = vec![]; for _ in 0..10 { 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()); }
这里创建了一个
counter
变量来存放内含i32
的Mutex<T>
,,接下来遍历 range 创建了 10 个线程。使用了thread::spawn
并对所有线程使用了相同的闭包:它们每一个都将调用lock
方法来获取Mutex<T>
上的锁,接着将互斥器中的值加一。当一个线程结束执行,num
会离开闭包作用域并释放锁,这样另一个线程就可以获取它了。
🎃多线程和多所有权
通过使用智能指针
Rc<T>
来创建引用计数的值,以便拥有多所有者。让我们在这也这么做看看会发生什么。use std::rc::Rc; use std::sync::Mutex; use std::thread; fn main() { let counter = Rc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Rc::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()); }
再一次编译并...出现了不同的错误!编译器真是教会了我们很多!
$ cargo run Compiling shared-state v0.1.0 (file:///projects/shared-state) error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely --> src/main.rs:11:36 | 11 | let handle = thread::spawn(move || { | ------------- ^------ | | | | ______________________|_____________within this `[closure@src/main.rs:11:36: 11:43]` | | | | | required by a bound introduced by this call 12 | | let mut num = counter.lock().unwrap(); 13 | | 14 | | *num += 1; 15 | | }); | |_________^ `Rc<Mutex<i32>>` cannot be sent between threads safely | = help: within `[closure@src/main.rs:11:36: 11:43]`, the trait `Send` is not implemented for `Rc<Mutex<i32>>` note: required because it's used within this closure --> src/main.rs:11:36 | 11 | let handle = thread::spawn(move || { | ^^^^^^^ note: required by a bound in `spawn` For more information about this error, try `rustc --explain E0277`. error: could not compile `shared-state` due to previous error
哇哦,错误信息太长不看!这里是一些需要注意的重要部分:第一行错误表明
`Rc<Mutex<i32>>` cannot be sent between threads safely
。编译器也告诉了我们原因the trait `Send` is not implemented for `Rc<Mutex<i32>>`
。下一部分会讲到Send
:这是确保所使用的类型可以用于并发环境的 trait 之一。不幸的是,
Rc<T>
并不能安全的在线程间共享。当Rc<T>
管理引用计数时,它必须在每一个clone
调用时增加计数,并在每一个克隆被丢弃时减少计数。Rc<T>
并没有使用任何并发原语,来确保改变计数的操作不会被其他线程打断。在计数出错时可能会导致诡异的 bug,比如可能会造成内存泄漏,或在使用结束之前就丢弃一个值。我们所需要的是一个完全类似Rc<T>
,又以一种线程安全的方式改变引用计数的类型。
🎃原子引用计数Arc<T>
所幸
Arc<T>
正是 这么一个类似Rc<T>
并可以安全的用于并发环境的类型。字母 “a” 代表 原子性(atomic),所以这是一个 原子引用计数(atomically reference counted)类型。
Arc<T>
和Rc<T>
有着相同的 API,所以修改程序中的use
行和new
调用。示例中的代码最终可以编译和运行:use std::sync::{Arc, Mutex}; 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()); }
成功了!我们从 0 数到了 10,这可能并不是很显眼,不过一路上我们确实学习了很多关于
Mutex<T>
和线程安全的内容!这个例子中构建的结构可以用于比增加计数更为复杂的操作。使用这个策略,可将计算分成独立的部分,分散到多个线程中,接着使用Mutex<T>
使用各自的结算结果更新最终的结果。
🎯RefCell<T>?Rc<T>与Mute/Arc<T>的相似性
你可能注意到了,因为
counter
是不可变的,不过可以获取其内部值的可变引用;这意味着Mutex<T>
提供了内部可变性,就像Cell
系列类型那样。正如以前使用RefCell<T>
可以改变Rc<T>
中的内容那样,同样的可以使用Mutex<T>
来改变Arc<T>
中的内容。另一个值得注意的细节是 Rust 不能避免使用
Mutex<T>
的全部逻辑错误。回忆一下以前使用Rc<T>
就有造成引用循环的风险,这时两个Rc<T>
值相互引用,造成内存泄漏。同理,Mutex<T>
也有造成 死锁(deadlock)的风险。这发生于当一个操作需要锁住两个资源而两个线程各持一个锁,这会造成它们永远相互等待。如果你对这个主题感兴趣,尝试编写一个带有死锁的 Rust 程序,接着研究任何其他语言中使用互斥器的死锁规避策略并尝试在 Rust 中实现它们。标准库中Mutex<T>
和MutexGuard
的 API 文档会提供有用的信息。