前言
虽然rust无畏并发,但并发中常常伴有共享状态(数据),有了共享状态(数据)那就离不开锁,锁的不恰当的使用和设计还是会引发性能瓶颈、死锁等问题,所以还是得对共享状态(数据)的并发性保持警惕!本篇文章旨在在学会锁基本使用后,如何正确高效编程避免并发中死锁等问题的发生,最后在不使用锁的情况下实现并发共享状态(数据)。
1. 入门
1.1 为什么使用锁
正在执行的不同线程通过共享内存相互通信。无论是哪种语言,线程之间共享状态通常会带来两个可怕的问题:
- 数据竞争:这可能导致数据损坏。
- 死锁:这可能导致你的程序陷入停滞。
所以锁的使用是为了在并发 (多线程或异步任务)访问同一份数据时,避免数据竞争; 而正确的使用锁,也可以避免死锁的发生。
1.2 parking_lot vs 标准库
| 特性 | parking_lot | std::sync 标准库 |
| 性能 | ✅ 高,极低开销,零分配,原子自旋 | ❌ 慢,重依赖 OS 锁,线程切换开销大 |
| 内存占用 | ✅ 极小,Mutex 只 1 个字节 | ❌ 较大,含内部结构(如 pthread_mutex_t) |
| 无毒设计(poisoning) | ✅ 默认不会 锁中毒 ,无需处理Result | ❌ panic 会 中毒,后续使用需处理 Result |
所以 parking_lot 更快、更小、更优雅。
为什么parking_lot要比标准库性能好
先扔出一张流程图

通过对比流程,我们发现当锁被持有时,标准库的做法是调用系统原语阻塞线程 ,而parking_lot则是先自旋几下 ,还是拿不到只是把线程挂起,这样还能执行其他的任务,所以说parking_lot性能要更好!
1.3 锁的基本使用
ini
[dependencies]
parking_lot = "=0.12.3"
rust
use parking_lot::{Mutex, RwLock};
#[test]
pub fn test() {
// parking_lot Mutex 基本使用
let str_mutex = Mutex::new("hello world!");
{
// 获取只读锁
let mutex_guard = str_mutex.lock();
println!("str_mutex={}", mutex_guard);
}
{
// 获取可写锁
let mut mutex_guard = str_mutex.lock();
*mutex_guard = "change!";
println!("str_mutex={}", mutex_guard);
}
// parking_lot RwLock 基本使用
let str_rwlock = RwLock::new("hello world!");
{
// 获取只读锁
let rwlock_guard = str_rwlock.read();
println!("str_rwlock={}", rwlock_guard);
}
{
// 获取可写锁
let mut rwlock_guard = str_rwlock.write();
*rwlock_guard = "change!";
println!("str_rwlock={}", rwlock_guard);
}
}
1.4 Mutex和RwLock区别及使用场景
Mutex 是"一写独占",任何操作都要加锁;
RwLock 是"多读共享,一写独占",读多写少时性能更优。
| 场景 | 推荐锁类型 |
| 读写均频繁 | Mutex |
| 读多写少 | RwLock |
| 没法预期读写的情况 | Mutex |
1.5 不要仅仅为了内部可变去使用锁
在一些数据结构使用锁的过程中,你会发现原本需要mut的修改数据的函数不再需要mut,举个🌰
rust
use parking_lot::Mutex;
struct Demo{
a: Mutex<i32>,
b:i32
}
impl Demo {
pub fn new() -> Self {
Demo {
a: Mutex::new(0),
b: 0,
}
}
pub fn update_a(&self, n: i32) {
let mut a = self.a.lock();
*a += n;
}
pub fn update_b(&mut self, n: i32) {
self.b += n;
}
}
#[test]
fn test() {
//不需要mut可变,也可以更新Demo内部的值
let demo = Demo::new();
demo.update_a(1);
// demo.update_b(1);
}
通过update_a方法看出它的内部需要mut可变的,但是对于外部却不需要mut可变。
但为什么不能仅仅为了内部可变去使用锁,原因有以下两点:
- 引入不必要的性能成本,这里不用多说;
- 违背 Rust idiomatic 风格,rust鼓励 "使用最小同步机制实现需要的可变性"。
所以锁是为线程同步而生的,不是为内部可变而设计的;如果你不需要 线程安全 ,请用更轻量的内部可变性工具,比如 RefCell 或 Cell。 当然,如果仅仅只为内部可变,使用轻量的内部可变工具,那也违背 Rust idiomatic,因为 Rust idiomatic鼓励我们尽可能地使用可变引用(&mut self)方式来进行修改,这更清晰、更安全。
2. 进阶一:死锁
数据竞争的问题使用锁得到了解决,那么死锁呢,它具体是什么,怎么产生的,在rust中又如何解决呢?
2.1 死锁的产生
举个🌰
我们创建一个游戏服务器,主要功能是添加玩家进入游戏和禁掉玩家从游戏中移除
rust
struct GameServer {
// 从玩家名字到玩家信息的映射。
players: Mutex<HashMap<String, Player>>,
// 当前游戏,由唯一的 game ID 来索引。
games: Mutex<HashMap<GameId, Game>>,
}
impl GameServer {
/// 添加新玩家并将他们加入当前游戏。
fn add_and_join(&self, username: &str, info: Player) -> Option<GameId> {
// 添加新玩家。
let mut players = self.players.lock();
players.insert(username.to_owned(), info);
// 找到一个未满的游戏房间来让他们加入。
let mut games = self.games.lock();
for (id, game) in games.iter_mut() {
if game.add_player(username) {
return Some(id.clone());
}
}
None
}
/// 通过 `username` 来封禁掉玩家,把他们从任何当前游戏中移除。
fn ban_player(&self, username: &str) {
// 找到该用户所在所有的游戏房间,并移除。
let mut games = self.games.lock();
games
.iter_mut()
.filter(|(_id, g)| g.has_player(username))
.for_each(|(_id, g)| g.remove_player(username));
// 从用户列表删除他们。
let mut players = self.players.lock();
players.remove(username);
}
}
#[tokio::test]
pub async fn test_game_server() {
let game_server = Arc::new(GameServer {
players: Mutex::new(HashMap::new()),
games: Mutex::new(HashMap::new()),
});
let game_server_clone = game_server.clone();
//起一个线程去频繁添加玩家进入游戏
thread::spawn( move ||{
for i in 0..1000 {
game_server_clone.add_and_join(&format!("Alice{}", i), Player);
println!("game_server add_and_join Alice{}", i)
}
});
//起一个线程去频繁禁掉玩家,把他们从任何当前游戏中移除
thread::spawn( move ||{
for i in 0..1000 {
game_server.ban_player(&format!("Alice{}", i));
println!("game_server ban_player Alice{}", i)
}
});
sleep(Duration::from_secs(2)).await;
println!("game_server start");
}
运行
sql
game_server add_and_join Alice0
game_server start
我们发现代码没有按照自己想象去执行,add_and_join和ban_player并没有执行1000次,甚至在开始的时候添加第一个玩家加入游戏后就结束了,实际上这就是产生了死锁,我们来分析一下原因:
线程死锁顺序
| 线程 1 | 线程 2 |
| 进入 add_and_join() 并立即获取 players 锁。 | |
| 进入 ban_player() 并立即获取 games 锁。 | |
| 尝试获取 games 锁;但目前由线程 2 所有,所以线程 1 阻塞。 | |
| 尝试获取 players 锁;但目前由线程 1 所有,所以线程 2 阻塞。 |
此时,程序陷入死锁:两个线程都不会继续运行,任何其他线程也不会对两个 Mutex 保护的数据结构中的任何一个执行任何操作。
那如何解决呢?
2.2 如何避免死锁的产生
缩小锁的范围
细心的小伙伴会在锁的基本使用的小节发现代码示例中,为什么不管获取只读锁和可写锁,每获取一次就要用{}包裹?其实这个操作就是缩小锁的范围,因为在一个作用域中,当你申请一把锁,没有释放掉,再去申请时,这就产生了情况最简单的死锁。
怎么解决,照葫芦画瓢,在使用完锁后释放掉,加{}。
scss
impl GameServer {
/// 添加新玩家并将他们加入当前游戏。
fn add_and_join(&self, username: &str, info: Player) -> Option<GameId> {
{
// 添加新玩家。
//players可写锁 ---- 开始
let mut players = self.players.lock();
players.insert(username.to_owned(), info);
//players可写锁 ---- 结束
}
//等同于
// let mut players = self.players.lock();
// players.insert(username.to_owned(), info);
// drop(players);
{
// 找到一个未满的游戏房间来让他们加入。
let mut games = self.games.lock();
for (id, game) in games.iter_mut() {
if game.add_player(username) {
return Some(id.clone());
}
}
}
None
}
/// 通过 `username` 来封禁掉玩家,把他们从任何当前游戏中移除。
fn ban_player(&self, username: &str) {
{
let mut games = self.games.lock();
games
.iter_mut()
.filter(|(_id, g)| g.has_player(username))
.for_each(|(_id, g)| g.remove_player(username));
}
// 从用户列表删除他们。
{
let mut players = self.players.lock();
players.remove(username);
}
}
}
修改完,我们运行一下:
erlang
game_server add_and_join Alice0
game_server ban_player Alice0
...
game_server ban_player Alice999
game_server add_and_join Alice999
将带有锁的数据结构的操作封装到辅助方法中
尽管我们有缩小锁的范围这一利器,但实际情况,我们有时因为记性不好偶尔会忘记加{}提前释放锁,别担心我们有更好的方法,尽量将带有锁的数据结构的操作封装到辅助方法中!
scss
impl GameServer {
fn insert_player(&self, username: &str, info: Player) {
let mut players = self.players.lock();
players.insert(username.to_owned(), info);
}
fn remove_player(&self, username: &str) {
let mut players = self.players.lock();
players.remove(username);
}
/// 添加新玩家并将他们加入当前游戏。
fn add_and_join(&self, username: &str, info: Player) -> Option<GameId> {
// 添加新玩家。
self.insert_player(username, info);
{
// 找到一个未满的游戏房间来让他们加入。
let mut games = self.games.lock();
for (id, game) in games.iter_mut() {
if game.add_player(username) {
return Some(id.clone());
}
}
}
None
}
/// 通过 `username` 来封禁掉玩家,把他们从任何当前游戏中移除。
fn ban_player(&self, username: &str) {
{
let mut games = self.games.lock();
games
.iter_mut()
.filter(|(_id, g)| g.has_player(username))
.for_each(|(_id, g)| g.remove_player(username));
}
// 从用户列表删除他们。
self.remove_player(username);
}
}
修改完我们运行一下:
game_server add_and_join Alice0
game_server ban_player Alice0
···
game_server add_and_join Alice999
game_server ban_player Alice999
运行成功,这时候细心的小伙伴发现为什么没有封装games的操作!!!
那好,我们沿着这个思路来继续封装一下games:
rust
impl GameServer {
fn insert_player(&self, username: &str, info: Player) {
let mut players = self.players.lock();
players.insert(username.to_owned(), info);
}
fn insert_game(&self, username: &str) -> Option<GameId>{
let mut games = self.games.lock();
for (id, game) in games.iter_mut() {
if game.add_player(username) {
return Some(id.clone());
}
}
None
}
fn add_and_join(&self, username: &str, info: Player) -> Option<GameId> {
// 添加新玩家。
self.insert_player(username, info);
// 找到一个未满的游戏房间来让他们加入。
self.insert_game( username)
}
...
}
完美,运行也没问题,若未来要求要 "player 必须先存在于 players 中才能加入 game",封装 insert_game其内部操作了games和game两种数据结构,且game的add_player方法可能依赖了players,那么调用方直接调用可能忽略了这个隐藏依赖,产生问题。
其实我们从函数封装的角度去考虑,它的职责必须是单一且清晰的 ,否则会产生副作用 。所以封装一些锁的数据结构的操作时,我们一样也要遵守这样的原则。对于单一数据结构的锁的操作建议封装,对于复杂多个带有锁的数据结构,且相互有依赖的操作,请保持 对锁 依赖的透明。 也就是说对于games的锁操作的,我们可能无法去封装,但我们可以改个更具语义的名字,让调用方明白这个方法你可能会失败,比如try_join_game。
注意,我们封装锁的操作时,更不能将
XxxGuard返回给调用者, 从死锁的角度看,这就像是分发一把已经上膛的枪!
找到合适的同步原语
解决了死锁问题,心里美滋滋,但这绝不是我们该得意的时候,我们继续添加打印看一下执行顺序:
rust
impl GameServer {
fn insert_player(&self, username: &str, info: Player) {
let mut players = self.players.lock();
players.insert(username.to_owned(), info);
}
fn remove_player(&self, username: &str) {
let mut players = self.players.lock();
players.remove(username);
}
/// 添加新玩家并将他们加入当前游戏。
fn add_and_join(&self, username: &str, info: Player) -> Option<GameId> {
// 添加新玩家。
self.insert_player(username, info);
println!("{:?} add players success.", username);
{
// 找到一个未满的游戏房间来让他们加入。
let mut games = self.games.lock();
for (id, game) in games.iter_mut() {
if game.add_player(username) {
return Some(id.clone());
}
}
println!("{:?} join game success.", username);
}
None
}
/// 通过 `username` 来封禁掉玩家,把他们从任何当前游戏中移除。
fn ban_player(&self, username: &str) {
// 找到该用户所在所有的游戏房间,并移除。
{
let mut games = self.games.lock();
games
.iter_mut()
.filter(|(_id, g)| g.has_player(username))
.for_each(|(_id, g)| g.remove_player(username));
println!("{:?} remove game success.", username);
}
// 从用户列表删除他们。
self.remove_player(username);
println!("{:?} remove players success.", username);
}
}
看一下运行结果:
erlang
...
"Alice12" add players success.
"Alice12" remove game success.
"Alice12" remove players success.
"Alice12" join game success.
...
有一个游戏包含不存在的玩家Alice12!这显然是不符合预期的。
刚解决死锁 问题,又多出一个数据 一致性 的问题,从整个过程来看games和players这两个数据结构都需要保持彼此同步,那如何解决这个问题,简单,我们使用一个覆盖二者的同步原语 就行,也就是将必须保持一致的数据结构包含在单个锁中。
rust
struct GameState {
players: HashMap<String, Player>,
games: HashMap<GameId, Game>,
}
struct GameServer {
state: Mutex<GameState>,
}
impl GameServer {
fn new() -> Self {
let mut games = HashMap::new();
games.insert(GameId, Game);
GameServer {
state: Mutex::new(GameState {
players: HashMap::new(),
games,
}),
}
}
fn add_and_join(&self, username: &str, info: Player) -> Option<GameId> {
let mut state = self.state.lock();
state.players.insert(username.to_string(), info);
println!("{:?} add players success.", username);
for (id, game) in state.games.iter_mut() {
if game.add_player(username) {
return Some(id.clone());
}
}
println!("{:?} join game success.", username);
None
}
fn ban_player(&self, username: &str) {
let mut state = self.state.lock();
state
.games
.iter_mut()
.filter(|(_id, g)| g.has_player(username))
.for_each(|(_id, g)| g.remove_player(username));
println!("{:?} remove game success.", username);
state.players.remove(username);
println!("{:?} remove players success.", username);
}
}
#[tokio::test]
pub async fn test_game_server() {
let game_server = Arc::new(GameServer::new());
let game_server_clone = game_server.clone();
thread::spawn(move || {
for i in 0..1000 {
game_server_clone.add_and_join(&format!("Alice{}", i), Player);
// println!("game_server add_and_join Alice{}", i)
}
});
thread::spawn(move || {
for i in 0..1000 {
game_server.ban_player(&format!("Alice{}", i));
// println!("game_server ban_player Alice{}", i)
}
});
sleep(Duration::from_secs(2)).await;
println!("game_server start");
}
看一下运行结果:
erlang
"Alice22" add players success.
"Alice22" join game success.
"Alice22" remove game success.
"Alice22" remove players success.
...
"Alice44" remove game success.
"Alice44" remove players success.
"Alice44" add players success.
"Alice44" join game success.
显然程序按照我们的逻辑去执行了(添加玩家列表和加入游戏同步,移除游戏和从玩家列表中删除同步)
2.3 DashMap 死锁问题
知识点补充
DashMap Rust 中极快的并发映射。
- DashMap 是 Rust 中并发关联数组/哈希图的实现。
- DashMap 尝试实现一个易于使用的 API,类似于 std::collections::HashMap,并进行了一些细微的更改来处理并发性。
- DashMap 试图非常简单易用,并直接替代 RwLock<HashMap<K、V>>。为了实现这些目标,所有方法都采用 &self 而不是修改方获取 &mut self。这允许您将 DashMap 放在 Arc 中并在线程之间共享它,同时仍然能够对其进行修改。
- DashMap 在性能上付出了巨大的努力,并旨在尽可能快。
- 提及锁定行为的文档在调用线程的参考帧中起作用。这意味着跨多个线程它是安全的。
平平无奇的代码引发的死锁
判断map里包不包含某个key,不包含或者包含且值不同时,再更新
arduino
let map = DashMap::new();
map.insert("key1", "a");
map.insert("key2", "b");
map.insert("key3", "c");
//错误示例
if let Some(exist_value) = map.get("key1") {
if exist_value.eq("new_value") {
return;
}
map.insert("key1", "new_value");
} else {
map.insert("key1", "new_value");
}
在读取Map某个key的对应的value时,返回了Ref<K, V>类型的数据,其实它内部是含有RwLockReadGuard,没错是读写锁,但在它生命周期内,我们同时去新增了数据,其内部需要再一次获取RwLockWriteGuard,在RwLockReadGuard没释放时去申请RwLockWriteGuard,造成了死锁。
正确写法
聪明的小伙伴想到了,缩小锁的范围
javascript
//RwLockReadGuard --- start
let need_insert = match map.get("key1") {
None => {true}
Some(value) => { *value != "new_value"}
};
//RwLockReadGuard --- end
if need_insert {
//RwLockWriteGuard --- start
map.insert("key1", "new_value");
//RwLockWriteGuard --- end
}
进阶写法
kotlin
//entry内部申请了一个可写锁RwLockWriteGuard,且and_modify、or_insert都不再申请锁
map.entry("key1")
.and_modify(|val|{
if *val != "new_value" {
*val = "new_value";
}
})
.or_insert("new_value");
3. 进阶二:同步锁与异步锁
3.1 同步锁在异步编程环境中的现状
3.1.1 举个🌰
rust
use parking_lot::RwLock;
use std::time::Duration;
use tokio::time::sleep;
trait TestTrait {
async fn run(&self);
}
struct TestA {
status: Mutex<i32>,
}
impl TestA {
pub fn new() -> Self {
TestA {
status: Mutex::new(0),
}
}
}
impl TestTrait for TestA {
async fn run(&self) {
let mutex_guard = self.status.lock();
if *mutex_guard < 0 {
return;
}
//模拟异步耗时
sleep(Duration::from_secs(1)).await;
println!("TestA run");
}
}
#[tokio::test]
pub async fn test_async() {
let test_a = TestA::new();
//多线程环境下调用
tokio::spawn(async move {
test_a.run().await;
});
sleep(Duration::from_secs(2)).await;
}
报错:
vbnet
tokio::spawn(async move {
test_a.run().await;
});
future cannot be sent between threads safely
future created by async block is not `Send`
Help: within `{async block@src/ lock_demo. rs:140:18: 140:28}`, the trait `Send` is not implemented for `*mut ()`
Note: future is not `Send` as this value is used across an await
错误解析:
- 整个 async block 内的数据结构都必须是 Send;
- 而 parking_lot::RwLockReadGuard 是
!Send(未实现Send),不能在线程间调度; - 异步函数返回的future不能在线程间安全的传输,原因是异步代码块中含有
!Send的数据。
知识点补充
有两个标准库 trait 会影响线程之间 Rust 对象的使用;
Sendtrait 表明某种类型的数据可以安全地跨线程传输;这种类型的数据的所有权可以从一个线程传递到另一个线程。Synctrait 表明某种类型的数据可以由多个线程安全地访问,但必须遵守借用检查器规则。换句话说,
Send意味着T可以在线程间传输,Sync意味着&T可以在线程间传输。
聪明的小伙伴反应出来,我们应该将带有锁的数据结构的操作封装到辅助方法中
修复:
rust
impl TestA {
...
fn get_status(&self) -> i32 {
let mutex_guard = self.status.lock();
*mutex_guard
}
}
impl TestTrait for TestA {
async fn run(&self) {
let status = self.get_status();
if status < 0 {
return;
}
sleep(Duration::from_secs(1)).await;
println!("TestA run");
}
}
错误消失了,而且可以正常运行
arduino
TestA run
3.1.2 为什么错误消失了
我们在没有基于错误提示,只是封装的锁操作,错误就消失了,这是为什么?其实只有锁(XxxGuard)或者!Send(未实现Send)的数据生命周期跨越了xxx.await,rust的编译器才会检查出错误,当我们封装了带有锁的数据结构的操作,锁的生命周期在调用get_status结束而结束,并没有跨越xxx.await,于是整个 async move { ... } 生成的 Future 中,不再包含!Send 类型,所以rust的编译器会识别出了这一点,编译就通过了并正常运行。
你以为这样就万事大吉了,我再举个例子,阁下该如何应对。
rust
struct TestA {
status: Mutex<i32>,
test_b: RwLock<TestB>,
}
impl TestA {
pub fn new() -> Self {
TestA {
status: Mutex::new(0),
test_b: RwLock::new(TestB::new()),
}
}
fn get_status(&self) -> i32 {
let mutex_guard = self.status.lock();
*mutex_guard
}
}
impl TestTrait for TestA {
async fn run(&self) {
let status = self.get_status();
if status < 0 {
return;
}
let test_b_guard = self.test_b.read();
test_b_guard.run().await;
sleep(Duration::from_secs(1)).await;
println!("TestA run");
}
}
上述例子中test_b无法封装其获取方法,且需要调用test_b的异步方法run,这时锁的生命周期不得不跨越xxx.await,这该怎么解?聪明的小伙伴又想到了,那有没有实现Send锁?
回答是肯定的,必须有!
3.2 实现Send的异步锁及使用
3.2.1 异步锁 tokio::sync::RwLock 和 tokio::sync::Mutex
rust
unsafe impl<T> Send for Mutex<T> where T: ?Sized + Send {}
//sync/mutex.rs
/// A handle to a held `Mutex`. The guard can be held across any `.await` point
/// as it is [`Send`].
///
/// As long as you have this guard, you have exclusive access to the underlying
/// `T`. The guard internally borrows the `Mutex`, so the mutex will not be
/// dropped while a guard exists.
///
/// The lock is automatically released whenever the guard is dropped, at which
/// point `lock` will succeed yet again.
#[clippy::has_significant_drop]
#[must_use = "if unused the Mutex will immediately unlock"]
pub struct MutexGuard<'a, T: ?Sized> {
// When changing the fields in this struct, make sure to update the
// `skip_drop` method.
#[cfg(all(tokio_unstable, feature = "tracing"))]
resource_span: tracing::Span,
lock: &'a Mutex<T>,
}
//sync/rwlock.rs
unsafe impl<T> Send for RwLock<T> where T: ?Sized + Send {}
// NB: These impls need to be explicit since we're storing a raw pointer.
// Safety: Stores a raw pointer to `T`, so if `T` is `Sync`, the lock guard over
// `T` is `Send`.
unsafe impl<T> Send for RwLockReadGuard<'_, T> where T: ?Sized + Sync {}
unsafe impl<T> Send for RwLockWriteGuard<'_, T> where T: ?Sized + Send + Sync {}
通过源码可以看出tokio中MutexGuard、RwLockWriteGuard、RwLockWriteGuard均实现了Send
Mutex 中的MutexGuard在源码中并未搜到实现Send的关键代码,但注释中确实也说明了它是
Send可以跨越xxx.await,未解之谜,感兴趣的小伙伴可以继续研究一下。
3.2.2 异步锁使用
ini
[dependencies]
tokio = "1.42.0"
rust
struct TestA {
status: Mutex<i32>,
test_b: tokio::sync::RwLock<TestB>,
}
impl TestTrait for TestA {
async fn run(&self) {
let status = self.get_status();
if status < 0 {
return;
}
let test_b = self.test_b.read().await;
test_b.run().await;
sleep(Duration::from_secs(1)).await;
println!("TestA run");
}
}
3.3 同步锁的性能瓶颈、与异步锁区别及各自使用 场景
难道真的就只有实不实现Send的区别吗,没那么简单。
3.3.1 同步锁的性能瓶颈及与异步锁区别
同步锁(如parking_lot::Mutex):会阻塞整个线程,直到获取到锁

通过流程图可以看出,同步锁在获取不到时,会阻塞当前线程,带来性能瓶颈!
异步锁(如 tokio::Mutex):不会阻塞线程,而是挂起任务,等待调度

可以看出,tokio的异步锁内部维护任务队列,以及任务挂起和唤醒机制,保证了即使暂时拿不到锁,也不会阻塞当前线程,只是暂时挂起任务,执行其他可执行的任务。
3.3.2 异步锁同步锁的使用场景
| 使用 场景 | 推荐锁类型 |
| 多线程程序(非 async),并发读写状态 | 同步锁 |
异步编程环境(async fn)未跨越 .await |
同步锁 |
异步编程环境(async fn)跨越了 .await |
异步锁 |
总之,在确认使用锁的情况下,优先使用同步锁,在锁的生命周期不得不跨越.await时,使用异步锁。
4. 高阶
在共享状态(数据)不需要持久化保存,能不能不使用锁呢?答案是肯定的,能!
4.1 不要通过共享状态(数据)来进行通信;相反,应该通过通信来共享状态
标题是来自Go语言的作者 Rob Pike对并发编程的经典总结,什么意思呢?
一句话解释
就是把我们思路颠倒过来!
在Rust侧如何实现
只需抓住两点:
- 数据由单个任务/线程独占管理(没有共享可变引用);
- 其他线程/任务通过 通道channel 发送请求来修改或读取数据;
这就是"单一数据拥有者+通信驱动 ",大名鼎鼎的Actor模型!这也是Rust语言推崇的共享状态方式。
4.2 使用"单一数据拥有者 + 通信驱动"Actor模型共享数据,从而替代锁
举个🌰
我们现有一需求,需要多线程操作比如添加、删除、查询设备集合
rust
/// 设备模型
#[derive(Debug, Clone)]
struct Device {
id: String,
name: String,
}
/// 设备操作消息
enum DeviceMessage {
Insert(Device),
Remove(String),
Get {
id: String,
respond_to: oneshot::Sender<Option<Device>>,//回调转异步
},
}
/// 设备处理器(多线程可用)
#[derive(Clone)]
struct DeviceHandle {
sender: mpsc::Sender<DeviceMessage>,
}
impl DeviceHandle {
pub fn new() -> Self {
//创建通道
let (tx, rx) = mpsc::channel(100);
//分配通道的接收端给设备管理器,并启动处理消息的任务
tokio::spawn(device_manager(rx));
//分配通道的发送端给设备处理器
DeviceHandle { sender: tx }
}
pub async fn insert(&self, device: Device) {
//发送添加设备消息(其他任务通过 channel 发送请求来修改或读取数据)
let _ = self.sender.send(DeviceMessage::Insert(device)).await;
}
pub async fn remove(&self, id: String) {
//发送删除设备消息(其他任务通过 channel 发送请求来修改或读取数据)
let _ = self.sender.send(DeviceMessage::Remove(id)).await;
}
pub async fn get(&self, id: String) -> Option<Device> {
let (tx, rx) = oneshot::channel();
let cmd = DeviceMessage::Get {
id,
respond_to: tx,
};
//发送获取设备消息(其他任务通过 channel 发送请求来修改或读取数据)
let _ = self.sender.send(cmd).await;
rx.await.ok().flatten()
}
}
/// 设备管理器,内部异步任务,持有设备数据所有权(数据由单个任务独占管理(没有共享可变引用))
async fn device_manager(mut rx: mpsc::Receiver<DeviceMessage>) {
let mut devices: HashMap<String, Device> = HashMap::new();
//接收设备操作的消息,并作用于devices
while let Some(cmd) = rx.recv().await {
match cmd {
DeviceMessage::Insert(device) => {
devices.insert(device.id.clone(), device);
}
DeviceMessage::Remove(id) => {
devices.remove(&id);
}
DeviceMessage::Get { id, respond_to } => {
let response = devices.get(&id).cloned();
let _ = respond_to.send(response);
}
}
}
}
#[tokio::test]
async fn main() {
let manager = DeviceHandle::new();
manager.insert(Device {
id: "dev123".into(),
name: "air conditioningr".into(),
}).await;
let found = manager.get("dev123".into()).await;
println!("Found: {:?}", found);
manager.remove("dev123".into()).await;
let missing = manager.get("dev123".into()).await;
println!("Missing: {:?}", missing);
}
输出:
css
Found: Some(Device { id: "dev123", name: "air conditioningr" })
Missing: None
思考
如果我是添加和删除是异步环境下,读取是在同步环境下的需求,那 DeviceHandle 的 get 方法是否能去掉 async ?
回答是否定的,因为整个数据维护在异步环境,获取的本质也是异步发送获取消息sender.send(cmd).await,异步接收消息rx.await,所以获取也必须是在异步环境,async去不掉!
5. 总结
我们整篇文章讲了:
- 使用锁的原因,锁的基本使用、以及mutex和rwlock两种锁的区别和使用场景;
- 分析了死锁产生的过程,以及如何避免,缩小范围,封装带有锁的简单操作等;
- 分析了同步锁带来的性能瓶颈、如何解决,以及异步锁使用场景;
- 在不使用锁的情况下,实现共享状态(数据);
6. 团队介绍
「三翼鸟数字化技术平台-智家APP平台」通过持续迭代演进移动端一站式接入平台为三翼鸟APP、智家APP等多个APP提供基础运行框架、系统通用能力API、日志、网络访问、页面路由、动态化框架、UI组件库等移动端开发通用基础设施;通过Z·ONE平台为三翼鸟子领域提供项目管理和技术实践支撑能力,完成从代码托管、CI/CD系统、业务发布、线上实时监控等Devops与工程效能基础设施搭建。