You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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锁被长时间持有,导致两个线程无法正确同步:

  1. MIDI线程中,config和leds_status的锁在进入循环前就被获取,并且整个循环过程中一直持有——这意味着LED线程永远拿不到leds_status的锁,自然无法读取到更新后的状态。
  2. 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的消息传递模型更符合"共享内存不如通信"的理念,也更安全:

  1. MIDI线程不直接修改共享状态,而是把MIDI事件(或者处理后的LED状态更新指令)发送到channel中。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 18:45:48