如何在Rust两线程间共享Vector?多线程同步问题求助
Rust线程同步问题:MIDI事件与LED灯带的状态同步
我是Rust新手,现在遇到了线程同步的问题:我需要一个线程循环接收MIDI事件并更新状态对象,另一个线程持续监控该对象并更新LED灯带。目前用Arc+Mutex的方法好像没法让两个线程的状态保持同步,请问该怎么实现?如果Arc+Mutex不合适,有没有更好的替代方案?
当前代码
main.rs
mod functions; mod structs; use crate::functions::*; use crate::structs::*; use portmidi as pm; use rs_ws281x::{ChannelBuilder, ControllerBuilder, StripType}; use std::sync::{Arc, Mutex}; use std::{fs, thread, time}; const MIDI_TIMEOUT: u64 = 10; const MIDI_CHANNEL: usize = 0; #[tokio::main] async fn main() { let config: Arc<std::sync::Mutex<Config>> = Arc::new(Mutex::new( toml::from_str(&fs::read_to_string("config.toml").unwrap()).unwrap(), )); let config_midi = config.clone(); let config_leds = config.clone(); let leds_status = Arc::new(Mutex::new(vec![0; config.lock().unwrap().leds.num_leds])); let leds_status_midi = Arc::clone(&leds_status); let leds_status_leds = Arc::clone(&leds_status); thread::spawn(move || { let config = config_midi.lock().unwrap(); let midi_context = pm::PortMidi::new().unwrap(); let device_info = midi_context .device(config.midi.id) .expect(format!("Could not find device with id {}", config.midi.id).as_str()); println!("Using device {}) {}", device_info.id(), device_info.name()); let input_port = midi_context .input_port(device_info, config.midi.buffer_size) .expect("Could not create input port"); let mut leds_status = leds_status_midi.lock().unwrap(); loop { if let Ok(_) = input_port.poll() { if let Ok(Some(events)) = input_port.read_n(config.midi.buffer_size) { for event in events { let event_type = get_midi_event_type(event.message.status, event.message.data2); match event_type { MidiEventType::NoteOn => { let key = get_note_position(event.message.data1, &config); leds_status[key] = 1; } MidiEventType::NoteOff => { let key = get_note_position(event.message.data1, &config); leds_status[key] = 0; } _ => {} } } } } thread::sleep(time::Duration::from_millis(MIDI_TIMEOUT)); } }); thread::spawn(move || { let config = config_leds.lock().unwrap(); let mut led_controller = ControllerBuilder::new() .freq(800_000) .dma(10) .channel( MIDI_CHANNEL, ChannelBuilder::new() .pin(config.leds.pin) .count(config.leds.num_leds as i32) .strip_type(StripType::Ws2812) .brightness(config.leds.brightness) .build(), ) .build() .unwrap(); loop { let leds_status = leds_status_leds.lock().unwrap(); print!("\x1b[2J\x1b[1;1H"); println!( "{:?}", leds_status.iter().filter(|x| (**x) > 0).collect::<Vec<_>>() ); } }); }
functions.rs
use crate::structs::MidiEventType; pub fn get_note_position(note: u8, config: &crate::structs::Config) -> usize { let mut note_offset = 0; for i in 0..config.leds.offsets.len() { if note > config.leds.offsets[i][0] { note_offset = config.leds.offsets[i][1]; break; } } note_offset -= config.leds.shift; let note_pos_raw = 2 * (note - 20) - note_offset; config.leds.num_leds - (note_pos_raw as usize) } pub fn get_midi_event_type(status: u8, velocity: u8) -> MidiEventType { if status == 144 && velocity > 0 { MidiEventType::NoteOn } else if status == 128 || (status == 144 && velocity == 0) { MidiEventType::NoteOff } else { MidiEventType::ControlChange } }
structs.rs
use serde_derive::Deserialize; #[derive(Deserialize, Debug)] pub struct Config { pub leds: LedsConfig, pub midi: MidiConfig, } #[derive(Deserialize, Debug)] pub struct LedsConfig { pub pin: i32, pub num_leds: usize, pub brightness: u8, pub offsets: Vec<Vec<u8>>, pub shift: u8, pub fade: i8, } #[derive(Deserialize, Debug)] pub struct MidiConfig { pub id: i32, pub buffer_size: usize, } #[derive(Debug)] pub enum MidiEventType { NoteOn, NoteOff, ControlChange, }
问题分析与解决方案
当前代码的核心问题
你现在的代码里,Mutex锁被长时间持有,导致两个线程无法正确同步:
- MIDI线程中,
config和leds_status的锁在进入循环前就被获取,并且整个循环过程中一直持有——这意味着LED线程永远拿不到leds_status的锁,自然无法读取到更新后的状态。 - LED线程同样在初始化后就一直持有
config的锁,不过这里config是只读的,影响相对小,但leds_status的锁被MIDI线程独占才是同步失败的关键。
修复Arc+Mutex的实现
解决思路很简单:只在需要访问共享数据的短暂时间段内持有锁,不要长时间持有。
修改后的main.rs关键部分:
thread::spawn(move || { // 提前获取config并克隆,因为config是只读的,不需要一直持有锁 let config = { let locked_config = config_midi.lock().unwrap(); locked_config.clone() }; let midi_context = pm::PortMidi::new().unwrap(); let device_info = midi_context .device(config.midi.id) .expect(format!("Could not find device with id {}", config.midi.id).as_str()); println!("Using device {}) {}", device_info.id(), device_info.name()); let input_port = midi_context .input_port(device_info, config.midi.buffer_size) .expect("Could not create input port"); loop { if let Ok(_) = input_port.poll() { if let Ok(Some(events)) = input_port.read_n(config.midi.buffer_size) { // 只在处理事件的时候获取leds_status的锁 let mut leds_status = leds_status_midi.lock().unwrap(); for event in events { let event_type = get_midi_event_type(event.message.status, event.message.data2); match event_type { MidiEventType::NoteOn => { let key = get_note_position(event.message.data1, &config); leds_status[key] = 1; } MidiEventType::NoteOff => { let key = get_note_position(event.message.data1, &config); leds_status[key] = 0; } _ => {} } } // 处理完事件后,锁会自动释放(因为leds_status变量离开作用域) } } thread::sleep(time::Duration::from_millis(MIDI_TIMEOUT)); } }); thread::spawn(move || { // 同样提前克隆config,避免长时间持有锁 let config = { let locked_config = config_leds.lock().unwrap(); locked_config.clone() }; let mut led_controller = ControllerBuilder::new() .freq(800_000) .dma(10) .channel( MIDI_CHANNEL, ChannelBuilder::new() .pin(config.leds.pin) .count(config.leds.num_leds as i32) .strip_type(StripType::Ws2812) .brightness(config.leds.brightness) .build(), ) .build() .unwrap(); loop { // 只在读取状态的时候获取锁,读取完成后立即释放 let leds_status = leds_status_leds.lock().unwrap(); print!("\x1b[2J\x1b[1;1H"); println!( "{:?}", leds_status.iter().filter(|x| (**x) > 0).collect::<Vec<_>>() ); // 离开作用域后锁自动释放 thread::sleep(time::Duration::from_millis(10)); // 加个小延迟,避免占用过多CPU } });
替代方案:用消息传递(Channel)
如果觉得Mutex的使用容易出错,Rust的消息传递模型更符合"共享内存不如通信"的理念,也更安全:
- MIDI线程不直接修改共享状态,而是把MIDI事件(或者处理后的LED状态更新指令)发送到channel中。
- LED线程从channel接收消息,更新自己维护的状态,再同步到LED灯带。
示例简化逻辑:
use std::sync::mpsc; // 创建一个消息通道,用于传递LED状态更新指令 let (tx, rx) = mpsc::channel(); // MIDI线程发送消息 thread::spawn(move || { let config = { config_midi.lock().unwrap().clone() }; // ... 初始化MIDI部分 ... loop { if let Ok(Some(events)) = input_port.read_n(config.midi.buffer_size) { for event in events { let event_type = get_midi_event_type(event.message.status, event.message.data2); let key = get_note_position(event.message.data1, &config); match event_type { MidiEventType::NoteOn => tx.send((key, 1)).unwrap(), MidiEventType::NoteOff => tx.send((key, 0)).unwrap(), _ => {} } } } thread::sleep(time::Duration::from_millis(MIDI_TIMEOUT)); } }); // LED线程接收消息并更新状态 thread::spawn(move || { let config = { config_leds.lock().unwrap().clone() }; let mut led_controller = /* ... 初始化LED控制器 ... */; let mut leds_status = vec![0; config.leds.num_leds]; loop { // 接收消息并更新本地状态 if let Ok((key, value)) = rx.try_recv() { leds_status[key] = value; } // 更新LED灯带 // ... 这里添加更新LED的逻辑 ... print!("\x1b[2J\x1b[1;1H"); println!("{:?}", leds_status.iter().filter(|x| **x > 0).collect::<Vec<_>>()); thread::sleep(time::Duration::from_millis(10)); } });
这种方式不需要手动管理Mutex锁,避免了死锁和长时间持有锁的问题,代码逻辑也更清晰。
其他可选方案:RwLock
如果你的场景是读多写少(比如LED线程频繁读取状态,MIDI线程偶尔更新),可以用Arc<RwLock<T>>替代Arc<Mutex<T>>:
- RwLock允许多个读者同时持有读锁,只有写操作会独占锁,性能比Mutex更好。
- 使用方式和Mutex类似,但要注意
read()和write()的区别。
内容的提问来源于stack exchange,提问作者cstef
相关产品推荐
相关产品推荐

