Rust Arc<Mutex>按键计数场景读取值始终为0问题排查
问题现象
给出的Rust代码预期实现按秒统计周期内的键盘按键次数,每轮周期先打印周期内累计按键数,再将计数重置为0并打印重置后的值。实际运行时两处打印结果始终为0;移除重置计数的逻辑后,计数可以正常累加。
附问题代码:
use std::{thread, time::{Duration}, sync::{Arc, Mutex}}; fn main() { let num = Arc::new(Mutex::new(0u8)); let clone = num.clone(); thread::spawn(move || { loop { println!("{:?};", *num.lock().unwrap()); // 预期打印重置前计数值,实际始终为0 thread::sleep(Duration::from_secs(1)); *num.lock().unwrap() = 0; println!("{:?};", *num.lock().unwrap()); // 预期打印重置后的0,实际确实始终为0 } }); listen(clone); } fn listen(num: Arc<Mutex<u8>>) { rdev::listen(move |event| { match event.event_type { rdev::EventType::KeyPress(_) => { *num.lock().unwrap() += 1; }, _ => {}, } }).unwrap(); }
问题根因
- 核心逻辑时序完全写反:循环执行顺序和预期不匹配。
原循环的执行流是:- 进入循环立刻读取计数打印 → 此时是上一轮刚把计数重置为0的时间点,读到的必然是0
- 休眠1秒 → 这1秒内的按键事件会正常触发回调,给计数累加
- 直接把计数设为0 → 这一步没有读取休眠期间累加的数值,1秒内攒下的计数被直接清空
- 读取计数打印 → 刚设完0,读到的必然是0
- 回到步骤1进入下一轮
整个流程里,1秒休眠期累加的有效计数从来没有被读取过,自然两处打印都只能看到0。
- 存在计数丢失的竞态风险:原代码每次读、改计数都是单独申请、释放锁,读和重置两个操作之间锁会被释放,如果这时候刚好有按键触发,回调会先给计数加1,紧接着重置逻辑就会把这个新增的值清掉,导致这次按键的计数丢失,统计不准。
- 额外小问题:用
u8存储计数最大值仅为255,如果1秒内按键次数超过255会触发整数溢出panic。
至于移除重置逻辑后看起来正常,是因为计数只会累加不会被清空,虽然打印时机还是错的(打印的是上一个周期结束时的累计值,不是当前周期的增量),但数值会持续上涨,给人“正常运行”的错觉。
修复方案
- 调整循环内的执行顺序,把休眠移到每轮循环的最开头
- 将“读取累计值、重置计数”的逻辑放在同一个锁的临界区内,保证操作原子性,避免计数丢失
- 将计数类型替换为
u32,避免高频按键下的溢出问题
修复后的核心代码如下:
fn main() { // 计数类型替换为u32避免溢出 let num = Arc::new(Mutex::new(0u32)); let clone = num.clone(); thread::spawn(move || { loop { // 先休眠1秒,累计这1秒内的按键 thread::sleep(Duration::from_secs(1)); // 单次申请锁,完成读值、重置两个操作,全程不提前释放锁 let mut count_guard = num.lock().unwrap(); println!("重置前周期内按键数:{}", *count_guard); *count_guard = 0; println!("重置后计数值:{}", *count_guard); // 锁守卫超出作用域自动释放,不会阻塞按键回调 } }); listen(clone); } fn listen(num: Arc<Mutex<u32>>) { rdev::listen(move |event| { match event.event_type { rdev::EventType::KeyPress(_) => { *num.lock().unwrap() += 1; }, _ => {}, } }).unwrap(); }
这个方案完全保留了每秒重置计数的业务逻辑,不会出现全局输入钩子阻塞、死锁问题:锁的持有时间极短,仅完成内存读、写操作就立刻释放,休眠全程不持有锁,按键回调可以正常拿到锁更新计数。
内容的提问来源于stack exchange,提问作者Yoskutik
相关产品推荐
相关产品推荐

