Rust 多线程:真的能提高性能吗?

科技   2024-09-22 16:35   广东  

Rust 凭借其速度和高效的线程处理能力而闻名。今天,我们将通过一个例子来测试 Rust 多线程的实际性能。

问题: 找到一个非常大的数字的所有质因数。

回顾: 因数是指能被另一个数整除且没有余数的数。例如,10 的因数是 1,2,5 和 10。90 的因数是 1,2,3,5,6,9,10,15,18,30,45 和 90。

我们的代码将先找到一个数字的所有因数,然后判断这些因数是否是质数。

第 1 部分:不使用多线程

步骤 1:编写寻找因数的函数

fn find_factors(n: u64) -> Vec<u64> {
    let mut factors = Vec::new();
    let square_root = (n as f64).sqrt() as u64;

    for i in 2..=square_root {
        if n % i == 0 {
            factors.push(i);
        }
    }

    factors
}

步骤 2:添加质数判断

fn find_factors(n: u64) -> Vec<u64> {
    let mut factors = Vec::new();
    let mut prime_factors = Vec::new();
    let square_root = (n as f64).sqrt() as u64;

    for i in 2..=square_root {
        if n % i == 0 {
            factors.push(i);
        }
    }

    factors.iter().for_each(|&factor| {
        if is_prime(factor) {
            prime_factors.push(factor);
        }
    });

    prime_factors
}

fn is_prime(n: u64) -> bool {
    if n < 2 {
        return false;
    }

    for i in 2..=(n as f64).sqrt() as u64 {
        if n % i == 0 {
            return false;
        }
    }

    true
}

步骤 3:测试执行

use std::time::Instant;

fn main() {
    let numbers = [1002008054223232324444426];

    for i in numbers {
        let start = Instant::now();
        println!("{:?}", find_factors(i));
        let duration = start.elapsed();

        println!("Time elapsed: {:?}", duration);
    }
}

输出:

[2, 5]
Time elapsed: 34.458µs
[2, 5]
Time elapsed: 4µs
[2, 14923]
Time elapsed: 16.687481416s

第 2 部分:使用多线程

现在,我们将把质数判断函数改造成多线程操作。我们将定义要使用的线程数,将因数集合平均分配到各个线程,并将它们发送到不同的线程进行处理。

步骤 1:编写新的多线程质数判断函数

use std::thread;

fn is_prime_mt(n: u64) -> bool {
    if n < 2 {
        return false;
    }

    let sqrt_n = (n as f64).sqrt() as u64;
    let num_threads = 4;
    let chunk_size = sqrt_n / num_threads;
    let mut handles = vec![];

    for i in 0..num_threads {
        let start = i * chunk_size + 2;
        let end = if i == num_threads - 1 { sqrt_n } else { (i + 1) * chunk_size + 1 };
        let handle = thread::spawn(move || {
            for divisor in start..=end {
                if n % divisor == 0 {
                    return false;
                }
            }
            true
        });
        handles.push(handle);
    }

    for handle in handles {
        if let Ok(is_prime_in_chunk) = handle.join() {
            if !is_prime_in_chunk {
                return false;
            }
        }
    }

    true
}

步骤 2:更新 find_factors 函数以使用新的多线程质数判断

fn find_factors(n: u64) -> Vec<u64> {
    let mut factors = Vec::new();
    let mut prime_factors = Vec::new();
    let square_root = (n as f64).sqrt() as u64;

    for i in 2..=square_root {
        if n % i == 0 {
            factors.push(i);
        }
    }

    factors.iter().for_each(|&factor| {
        if is_prime_mt(factor) {
            prime_factors.push(factor);
        }
    });

    prime_factors
}

步骤 3:测试新的多线程解决方案

[25]
Time elapsed: 471.166µs
[25]
Time elapsed: 464.208µs
[214923]
Time elapsed: 16.587179166s

咦? 这与预期结果不符,但可以理解。因为线程本身也需要开销,所以对于简单的任务来说,创建和管理线程的成本会超过并发带来的收益。

在这个例子中,由于计算量很轻,创建和管理线程的开销超过了并发的优势。相反,当问题涉及更复杂的计算或更大的数据集时,线程可以带来显著的性能提升。

什么时候应该考虑使用线程?

在以下情况下,你应该考虑使用线程:

  • 任务计算量很大,或者可以分割成更小的独立块。
  • 程序涉及大量等待(例如网络请求、文件 I/O),线程可以在等待时处理其他任务。
  • 你正在处理大型数据集或进行繁重的计算,并行处理可以显著减少执行时间。

在像这个例子这样的简单情况下,单线程执行通常更有效。

文章精选

Tailspin:用 Rust 打造的炫彩日志查看器

Rust: 重塑系统编程的安全壁垒

Youki:用Rust编写的容器运行时,性能超越runc

使用C2Rust将C代码迁移到Rust

Rust语言中如何优雅地应对错误

Rust编程笔记
与你一起在Rust的世界里探索、学习、成长!
 最新文章