MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Rust中的并发编程与曼德博集实现

2022-08-195.5k 阅读

Rust 并发编程基础

Rust 因其出色的内存安全性和性能,在系统级编程领域崭露头角。其并发编程模型更是一大亮点,通过 std::thread 模块提供了线程创建和管理的能力。

创建线程

在 Rust 中,创建线程非常简单。下面是一个基本示例:

use std::thread;

fn main() {
    let handle = thread::spawn(|| {
        println!("This is a new thread!");
    });

    handle.join().unwrap();
    println!("The new thread has finished.");
}

在上述代码中,thread::spawn 函数用于创建一个新线程,其参数是一个闭包,该闭包中的代码将在新线程中执行。handle.join() 方法用于等待新线程完成,unwrap 方法用于处理可能出现的错误。如果新线程正常结束,join 方法会返回线程的返回值(这里因为闭包没有返回值,所以忽略)。如果线程发生恐慌(panic),join 方法会将恐慌传播到主线程。

线程间通信

线程间通信是并发编程中的重要部分。Rust 提供了通道(channel)机制来实现线程间的数据传递。通道由发送端(Sender)和接收端(Receiver)组成。下面是一个简单的示例:

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let data = String::from("Hello, from another thread!");
        tx.send(data).unwrap();
    });

    let received = rx.recv().unwrap();
    println!("Received: {}", received);
}

在这段代码中,mpsc::channel 创建了一个通道,tx 是发送端,rx 是接收端。新线程通过 tx.send 方法发送数据,主线程通过 rx.recv 方法接收数据。sendrecv 方法都是阻塞的,即如果没有数据可发送或接收,线程会等待。move 关键字用于将 tx 所有权转移到新线程中,确保新线程可以独立使用发送端。

共享状态与同步

当多个线程需要访问共享数据时,就需要考虑同步问题,以避免数据竞争。Rust 通过 Mutex(互斥锁)和 RwLock(读写锁)来解决这个问题。

Mutex:互斥锁只允许一个线程在同一时间访问共享数据。下面是一个使用 Mutex 的示例:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Counter: {}", *counter.lock().unwrap());
}

在这个例子中,Arc(原子引用计数)用于在多个线程间共享 Mutexcounter.lock().unwrap() 获取锁并返回一个可修改的引用,修改完成后,锁会自动释放。

RwLock:读写锁允许多个线程同时读共享数据,但只允许一个线程写数据。以下是一个简单示例:

use std::sync::{Arc, RwLock};
use std::thread;

fn main() {
    let data = Arc::new(RwLock::new(String::from("Initial data")));
    let mut handles = vec![];

    for _ in 0..5 {
        let data = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            let read_data = data.read().unwrap();
            println!("Read: {}", read_data);
        }));
    }

    for _ in 0..2 {
        let data = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            let mut write_data = data.write().unwrap();
            *write_data = String::from("New data");
        }));
    }

    for handle in handles {
        handle.join().unwrap();
    }
}

这里读操作使用 data.read().unwrap(),写操作使用 data.write().unwrap()。读操作可以并发执行,而写操作会独占锁,其他读写操作都要等待写操作完成。

曼德博集简介

曼德博集是在复平面上组成分形的点的集合,以数学家本华·曼德博的名字命名。其数学定义基于以下迭代公式: [ z_{n + 1} = z_n^2 + c ] 其中 ( z ) 和 ( c ) 都是复数,( z_0 = 0 )。对于给定的 ( c ),如果序列 ( {z_n} ) 保持有界(即 ( |z_n| ) 不会无限增大),那么 ( c ) 属于曼德博集。

从直观上看,我们可以通过对复平面上的每个点 ( c ) 进行上述迭代,并记录迭代次数 ( n ),当 ( |z_n| ) 超过某个阈值(比如 2)时停止迭代。如果迭代次数达到一个预设的最大值(如 100)仍未超过阈值,则认为该点属于曼德博集。迭代次数可以用来为该点着色,从而生成曼德博集的精美图像。

Rust 中实现曼德博集

基本实现

首先,我们需要定义复数类型以及相关的操作。Rust 标准库提供了 std::complex::Complex 类型,我们可以直接使用。

use std::fmt;
use std::complex::Complex;

// 迭代函数
fn mandelbrot(c: Complex<f64>, max_iter: u32) -> u32 {
    let mut z = Complex::new(0.0, 0.0);
    let mut n = 0;

    while n < max_iter && z.norm_sqr() <= 4.0 {
        z = z * z + c;
        n += 1;
    }

    n
}

// 打印曼德博集的简单文本表示
fn print_mandelbrot(xmin: f64, xmax: f64, ymin: f64, ymax: f64, width: u32, height: u32, max_iter: u32) {
    for j in 0..height {
        let y = ymin + (ymax - ymin) * (j as f64 / height as f64);
        for i in 0..width {
            let x = xmin + (xmax - xmin) * (i as f64 / width as f64);
            let c = Complex::new(x, y);
            let n = mandelbrot(c, max_iter);
            let pixel = if n == max_iter { '#' } else { ' ' };
            print!("{}", pixel);
        }
        println!();
    }
}

fn main() {
    let xmin = -2.0;
    let xmax = 1.0;
    let ymin = -1.5;
    let ymax = 1.5;
    let width = 80;
    let height = 40;
    let max_iter = 100;

    print_mandelbrot(xmin, xmax, ymin, ymax, width, height, max_iter);
}

在上述代码中,mandelbrot 函数实现了曼德博集的核心迭代逻辑。print_mandelbrot 函数则在给定的复平面区域内对每个点进行迭代,并根据结果打印出简单的文本图像。

并发实现优化

为了提高曼德博集的计算效率,我们可以利用 Rust 的并发编程能力。我们将复平面区域划分为多个部分,每个部分交给一个线程进行计算。

use std::sync::{Arc, Mutex};
use std::thread;
use std::complex::Complex;

// 迭代函数
fn mandelbrot(c: Complex<f64>, max_iter: u32) -> u32 {
    let mut z = Complex::new(0.0, 0.0);
    let mut n = 0;

    while n < max_iter && z.norm_sqr() <= 4.0 {
        z = z * z + c;
        n += 1;
    }

    n
}

// 线程计算部分曼德博集
fn calculate_chunk(xmin: f64, xmax: f64, ymin: f64, ymax: f64, width: u32, height: u32, max_iter: u32, chunk_y_start: u32, chunk_height: u32, result: &Arc<Mutex<Vec<u32>>>) {
    for j in chunk_y_start..chunk_y_start + chunk_height {
        let y = ymin + (ymax - ymin) * (j as f64 / height as f64);
        for i in 0..width {
            let x = xmin + (xmax - xmin) * (i as f64 / width as f64);
            let c = Complex::new(x, y);
            let n = mandelbrot(c, max_iter);
            let mut result_lock = result.lock().unwrap();
            let index = (j * width + i) as usize;
            result_lock[index] = n;
        }
    }
}

// 打印曼德博集的简单文本表示
fn print_mandelbrot(xmin: f64, xmax: f64, ymin: f64, ymax: f64, width: u32, height: u32, max_iter: u32) {
    let num_threads = 4;
    let chunk_height = height / num_threads;
    let result = Arc::new(Mutex::new(vec![0; (width * height) as usize]));
    let mut handles = vec![];

    for i in 0..num_threads {
        let xmin = xmin;
        let xmax = xmax;
        let ymin = ymin;
        let ymax = ymax;
        let width = width;
        let height = height;
        let max_iter = max_iter;
        let chunk_y_start = i * chunk_height;
        let chunk_height = chunk_height;
        let result = Arc::clone(&result);

        let handle = thread::spawn(move || {
            calculate_chunk(xmin, xmax, ymin, ymax, width, height, max_iter, chunk_y_start, chunk_height, &result);
        });

        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    let result_lock = result.lock().unwrap();
    for j in 0..height {
        for i in 0..width {
            let n = result_lock[(j * width + i) as usize];
            let pixel = if n == max_iter { '#' } else { ' ' };
            print!("{}", pixel);
        }
        println!();
    }
}

fn main() {
    let xmin = -2.0;
    let xmax = 1.0;
    let ymin = -1.5;
    let ymax = 1.5;
    let width = 80;
    let height = 40;
    let max_iter = 100;

    print_mandelbrot(xmin, xmax, ymin, ymax, width, height, max_iter);
}

在这段代码中,calculate_chunk 函数负责计算复平面区域中的一个小块。print_mandelbrot 函数创建多个线程,每个线程计算一个小块,并将结果存储在共享的 result 向量中。这里使用 Arc<Mutex<Vec<u32>>> 来确保线程安全地访问和修改结果向量。通过这种并发方式,可以显著提高曼德博集的计算速度,尤其是在处理较大的区域或较高的分辨率时。

进一步优化与拓展

并行迭代器

Rust 的并行迭代器提供了一种更简洁的方式来实现并发计算。我们可以将复平面上的点集合转换为并行迭代器,然后对每个点进行曼德博集的迭代计算。

use std::complex::Complex;
use rayon::prelude::*;

// 迭代函数
fn mandelbrot(c: Complex<f64>, max_iter: u32) -> u32 {
    let mut z = Complex::new(0.0, 0.0);
    let mut n = 0;

    while n < max_iter && z.norm_sqr() <= 4.0 {
        z = z * z + c;
        n += 1;
    }

    n
}

// 打印曼德博集的简单文本表示
fn print_mandelbrot(xmin: f64, xmax: f64, ymin: f64, ymax: f64, width: u32, height: u32, max_iter: u32) {
    let points: Vec<Complex<f64>> = (0..height).flat_map(|j| {
        let y = ymin + (ymax - ymin) * (j as f64 / height as f64);
        (0..width).map(move |i| {
            let x = xmin + (xmax - xmin) * (i as f64 / width as f64);
            Complex::new(x, y)
        })
    }).collect();

    let results: Vec<u32> = points.par_iter().map(|c| mandelbrot(*c, max_iter)).collect();

    for j in 0..height {
        for i in 0..width {
            let n = results[(j * width + i) as usize];
            let pixel = if n == max_iter { '#' } else { ' ' };
            print!("{}", pixel);
        }
        println!();
    }
}

fn main() {
    let xmin = -2.0;
    let xmax = 1.0;
    let ymin = -1.5;
    let ymax = 1.5;
    let width = 80;
    let height = 40;
    let max_iter = 100;

    print_mandelbrot(xmin, xmax, ymin, ymax, width, height, max_iter);
}

这里使用了 rayon 库,通过 par_iter 方法将普通迭代器转换为并行迭代器,使得 mandelbrot 函数可以并发地应用到每个复平面点上。这种方式代码更加简洁,同时利用了系统的多核能力,进一步提升计算效率。

图形化输出

虽然文本形式的曼德博集展示有一定的直观性,但为了获得更美观和详细的结果,我们可以使用图形库来绘制曼德博集图像。例如,使用 image 库来生成 PNG 图像。

use std::complex::Complex;
use rayon::prelude::*;
use image::{ImageBuffer, Rgb, RgbImage};

// 迭代函数
fn mandelbrot(c: Complex<f64>, max_iter: u32) -> u32 {
    let mut z = Complex::new(0.0, 0.0);
    let mut n = 0;

    while n < max_iter && z.norm_sqr() <= 4.0 {
        z = z * z + c;
        n += 1;
    }

    n
}

// 生成曼德博集图像
fn generate_mandelbrot_image(xmin: f64, xmax: f64, ymin: f64, ymax: f64, width: u32, height: u32, max_iter: u32) -> RgbImage {
    let points: Vec<Complex<f64>> = (0..height).flat_map(|j| {
        let y = ymin + (ymax - ymin) * (j as f64 / height as f64);
        (0..width).map(move |i| {
            let x = xmin + (xmax - xmin) * (i as f64 / width as f64);
            Complex::new(x, y)
        })
    }).collect();

    let results: Vec<u32> = points.par_iter().map(|c| mandelbrot(*c, max_iter)).collect();

    let mut image = ImageBuffer::new(width, height);
    for (index, &n) in results.iter().enumerate() {
        let (x, y) = (index % width as usize, index / width as usize);
        let color = if n == max_iter {
            Rgb([0, 0, 0])
        } else {
            let ratio = (n as f32) / (max_iter as f32);
            let intensity = (ratio * 255.0) as u8;
            Rgb([intensity, intensity, intensity])
        };
        image.put_pixel(x as u32, y as u32, color);
    }

    image
}

fn main() {
    let xmin = -2.0;
    let xmax = 1.0;
    let ymin = -1.5;
    let ymax = 1.5;
    let width = 800;
    let height = 600;
    let max_iter = 255;

    let image = generate_mandelbrot_image(xmin, xmax, ymin, ymax, width, height, max_iter);
    image.save("mandelbrot.png").unwrap();
}

在这段代码中,generate_mandelbrot_image 函数使用并行迭代器计算曼德博集的每个点,并根据迭代结果为图像的每个像素着色。最后使用 image.save 方法将生成的图像保存为 PNG 格式。这样我们就可以得到一个高质量的曼德博集图形化表示。

通过以上从并发编程基础到曼德博集实现及优化的过程,我们可以看到 Rust 在并发编程和高性能计算方面的强大能力,无论是简单的文本输出还是复杂的图形化展示,都能通过合理的并发设计得到高效的实现。