Rust多线程间数组共享问题求助:所有权语义与指针使用困境
解决Rust多线程数组乘法的所有权与线程安全问题
问题原因
你遇到的编译错误核心在于:*mut [f64; 1000000](可变原始指针)未实现Sync trait。Rust的线程安全规则要求,跨线程共享的数据必须保证不会引发数据竞争,而原始可变指针本身没有任何线程安全担保——Rust无法确认你会不会让多个线程同时修改同一块内存,因此拒绝将其视为可安全共享的类型。
当你把原始指针放入线程闭包时,闭包需要满足Send trait(才能被安全传递到新线程),但由于多个闭包捕获了同一个可变指针,这会涉及到共享该指针的引用,而&*mut T的Send实现依赖于*mut T的Sync实现,这正是缺失的部分,所以编译失败。
解决方案一:使用安全的作用域线程(推荐)
Rust 1.63+稳定了std::thread::scope,它允许线程借用当前作用域内的数据(无需满足'static生命周期),且能保证所有线程在作用域结束前完成,避免悬垂引用。结合切片拆分,我们可以安全地让每个线程处理数组的独立区间,完全不需要unsafe:
use std::time::SystemTime; use rand::Rng; use std::thread; fn main() { const N_CELLS: usize = 1_000_000; let concurrency = 1; let mut one = [0f64; N_CELLS]; let mut two = [0f64; N_CELLS]; let mut res = [0f64; N_CELLS]; println!("Creating data"); let mut rng = rand::thread_rng(); for i in 0..N_CELLS { one[i] = rng.gen::<f64>(); two[i] = rng.gen::<f64>(); res[i] = 0.0; } println!("Finished creating data"); let rounds = 100000; let start = SystemTime::now(); for _ in 0..rounds { let size_per_job = N_CELLS / concurrency; // 使用作用域线程,允许线程借用当前作用域的数组 thread::scope(|s| { for j in 0..concurrency { let from = j * size_per_job; let to = (j + 1) * size_per_job; // 拆分出每个线程要处理的切片:one和two是不可变切片,res是可变切片 let one_slice = &one[from..to]; let two_slice = &two[from..to]; let res_slice = &mut res[from..to]; s.spawn(move || { process_data(one_slice, two_slice, res_slice); }); } }); // 作用域结束时,所有线程已自动join } let duration_us = SystemTime::now().duration_since(start).unwrap().as_micros(); let duration_per_round = duration_us / rounds; println!("duration per round {} us", duration_per_round); } #[inline(never)] pub fn process_data(one: &[f64], two: &[f64], res: &mut [f64]) { // 因为三个切片长度一致,用zip遍历更安全 for ((&a, &b), r) in one.iter().zip(two.iter()).zip(res.iter_mut()) { *r = a * b; } }
优势
- 完全无unsafe,依赖Rust的类型系统保证线程安全
- 自动管理线程生命周期,无需手动join
- 切片拆分天然避免数据竞争(每个线程只操作自己的切片区间)
解决方案二:正确使用unsafe标记线程安全(不推荐,仅为理解原理)
如果你坚持使用原始指针,需要手动告诉Rust这些指针是可以安全跨线程共享的,通过封装成自定义类型并实现Send和Sync trait(仅当你能保证不会有数据竞争时才这么做):
#![feature(box_syntax)] use std::time::SystemTime; use rand::Rng; use std::thread; use std::marker::{Send, Sync}; // 自定义线程安全的指针封装,仅当你能保证指针指向的内存不会被同时修改时才实现Send/Sync #[derive(Clone, Copy)] struct SafePtr<T>(*const T); unsafe impl<T: Sync> Send for SafePtr<T> {} unsafe impl<T: Sync> Sync for SafePtr<T> {} #[derive(Clone, Copy)] struct SafeMutPtr<T>(*mut T); unsafe impl<T: Sync> Send for SafeMutPtr<T> {} unsafe impl<T: Sync> Sync for SafeMutPtr<T> {} fn main() { let nCells = 1_000_000; let concurrency = 1; let mut one = box [0f64; 1_000_000]; let mut two = box [0f64; 1_000_000]; let mut res = box [0f64; 1_000_000]; println!("Creating data"); let mut rng = rand::thread_rng(); for i in 0..nCells { one[i] = rng.gen::<f64>(); two[i] = rng.gen::<f64>(); res[i] = 0 as f64; } println!("Finished creating data"); let rounds = 100000; let start = SystemTime::now(); let one_raw = SafePtr(Box::into_raw(one)); let two_raw = SafePtr(Box::into_raw(two)); let res_raw = SafeMutPtr(Box::into_raw(res)); let mut handlers = Vec::new(); for _ in 0..rounds { let sizePerJob = nCells / concurrency; for j in 0..concurrency { let from = j * sizePerJob; let to = (j + 1) * sizePerJob; // 因为SafePtr/SafeMutPtr是Copy的,每个闭包会复制一份指针 handlers.push(thread::spawn(move || { unsafe { processData(one_raw.0, two_raw.0, res_raw.0, from, to); } })); } for handler in handlers.drain(..) { handler.join().unwrap(); } } // 注意:需要手动回收内存,否则会泄漏 unsafe { Box::from_raw(one_raw.0); Box::from_raw(two_raw.0); Box::from_raw(res_raw.0); } let durationUs = SystemTime::now().duration_since(start).unwrap().as_micros(); let durationPerRound = durationUs / rounds; println!("duration per round {} us", durationPerRound); } #[inline(never)] pub unsafe fn processData(one: *const [f64;1000000], two: *const [f64;1000000], res: *mut [f64;1000000], from: usize, to: usize) { for i in from..to { (*res)[i] = (*one)[i] * (*two)[i]; } }
关键注意事项
- 必须手动保证没有数据竞争(每个线程处理独立区间),否则会导致未定义行为
- 需要手动回收
Box转成的原始指针,否则会造成内存泄漏 - 这种方式绕过了Rust的安全检查,仅适合理解底层原理,实际项目优先用方案一
内容的提问来源于stack exchange,提问作者pveentjer
相关产品推荐
相关产品推荐

