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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 13:09:29