如何实现不同I²C总线实例的多线程并发访问?
问题
现有一段Rust代码用于管理I²C总线操作,但由于全局HashMap外层包裹了Mutex,导致不同I²C总线无法并发访问,如何修改代码实现不同总线的并发访问?
原代码如下:
use once_cell::sync::Lazy; use rppal::i2c::{Error, I2c}; use std::{ collections::HashMap, sync::{Arc, Mutex}, }; static GLOBAL_I2C: Lazy<Arc<Mutex<HashMap<u8, I2c>>>> = Lazy::new(|| Arc::new(Mutex::new(HashMap::new()))); fn perform_i2c_operation<T, F>( bus: u8, slave_address: u16, operation: F, ) -> Result<T, Error> where F: FnOnce(&mut I2c) -> Result<T, Error>, { // Get the mutex lock for the HashMap let mut i2c_map = match GLOBAL_I2C.lock() { Ok(i2c_map) => i2c_map, Err(e) => { return Err(Error::Io(std::io::Error::new( std::io::ErrorKind::Other, "Not really a thread panic.", ))); } }; // Check if the bus we're interested in is in the map, if not, insert it. if !i2c_map.contains_key(&bus) { match initialize_i2c(bus) { Ok(i2c) => { i2c_map.insert(bus, i2c); } Err(e) => { return Err(Error::Io(std::io::Error::new( std::io::ErrorKind::Other, format!("I²C bus couldn't be initialized: {}.", e), ))); } } } // Retrieve the i2c bus instance and perform the operations on it. match i2c_map.get_mut(&bus) { Some(i2c_instance) => { match i2c_instance.set_slave_address(slave_address) { Ok(()) => { operation(i2c_instance) } Err(e) => Err(e), } } None => { return Err(Error::Io(std::io::Error::new( std::io::ErrorKind::Other, "I²C bus couldn't be initialized:."))); } } }
解决方案
核心思路是缩小锁的粒度——不给整个HashMap加锁,而是给每个I²C总线实例单独加锁。这样不同总线的操作可以同时进行,只有同一条总线的操作会互斥。
具体修改步骤:
- 调整全局HashMap的类型,让每个总线实例被
Arc<Mutex>包裹,类型变为Arc<Mutex<HashMap<u8, Arc<Mutex<I2c>>>>>。 - 拆分锁的持有逻辑:先短暂锁定HashMap查找/初始化总线,拿到对应总线的
Arc<Mutex<I2c>>后立即释放HashMap的锁,再单独锁定该总线实例执行操作。
修改后的代码如下:
use once_cell::sync::Lazy; use rppal::i2c::{Error, I2c}; use std::{ collections::HashMap, sync::{Arc, Mutex}, }; // 全局HashMap存储带锁的I²C总线实例 static GLOBAL_I2C: Lazy<Arc<Mutex<HashMap<u8, Arc<Mutex<I2c>>>>>> = Lazy::new(|| Arc::new(Mutex::new(HashMap::new()))); fn perform_i2c_operation<T, F>( bus: u8, slave_address: u16, operation: F, ) -> Result<T, Error> where F: FnOnce(&mut I2c) -> Result<T, Error>, { // 第一步:锁定HashMap,查找或初始化目标总线 let i2c_mutex = { let mut i2c_map = match GLOBAL_I2C.lock() { Ok(map) => map, Err(_) => { return Err(Error::Io(std::io::Error::new( std::io::ErrorKind::Other, "Mutex poisoned", ))); } }; // 初始化未存在的总线 if !i2c_map.contains_key(&bus) { let i2c = initialize_i2c(bus)?; i2c_map.insert(bus, Arc::new(Mutex::new(i2c))); } // 克隆Arc,后续释放HashMap的锁后仍能操作总线 i2c_map.get(&bus).unwrap().clone() }; // HashMap的锁在此处自动释放 // 第二步:单独锁定目标总线,执行操作 let mut i2c_instance = match i2c_mutex.lock() { Ok(instance) => instance, Err(_) => { return Err(Error::Io(std::io::Error::new( std::io::ErrorKind::Other, "I2C bus mutex poisoned", ))); } }; // 设置从设备地址并执行自定义操作 i2c_instance.set_slave_address(slave_address)?; operation(&mut i2c_instance) } // 保留原初始化函数(示例实现) fn initialize_i2c(bus: u8) -> Result<I2c, Error> { I2c::new(bus) }
关键改进点
- 锁粒度拆分:HashMap的锁仅用于总线的查找和初始化,操作总线时只持有对应总线的锁,不同总线的操作完全并发。
- 资源复用:通过
Arc实现总线实例的多线程共享,避免重复初始化硬件资源。 - 代码简化:使用
?操作符替代嵌套match,提升代码可读性和简洁性。
内容的提问来源于stack exchange,提问作者Jamie
相关产品推荐
相关产品推荐

