20、Rust并发编程全解析

Rust并发编程全解析

1. 并发与多线程基础

在并发编程里,程序代码通常是线程共享的。每个线程能够执行程序文本段中不同的代码部分,并且将局部变量和函数参数存储在各自的线程栈中。当轮到某个线程执行时,CPU会加载该线程的程序计数器(包含要执行指令的地址),进而执行该线程的一组指令。

例如,若任务A2因等待I/O而被阻塞,CPU就会将执行权切换到其他任务,像B1或者A1。

Rust实现了1:1的线程模型,也就是每个操作系统线程对应由Rust标准库创建的一个用户级线程。另一种模型是M:N(也叫绿色线程),其中有M个绿色线程(由运行时管理的用户级线程)映射到N个内核级线程。

2. 创建和配置线程

2.1 使用 thread::spawn 函数创建线程

使用Rust标准库创建新线程有两种方式,首先来看 thread::spawn 函数的示例:

use std::thread;
fn main() {
    for _ in 1..5 {
        thread::spawn(|| {
            println!("Hi from thread id {:?}", 
                thread::current().id());
        });
    }
} 

在这个程序里,使用了 std::thread 模块, thread::spawn() 函数用于创建新线程。在主函数(在进程的主线程中运行)中创建了四个新的子线程。不过,运行这个程序时会发现,每次的输出结果都不一样,有时只打印一行,有时打印多行,有时甚至没有输出。这是因为线程的执行顺序是不确定的,而且如果 main() 函数在子线程执行之前就结束了,就无法看到预期的输出。

为了解决这个问题,需要将创建的子线程加入到主线程中,让主线程等待所有子线程执行完毕。修改后的代码如下:

use std::thread;
fn main() {
    let mut child_threads = Vec::new();
    for _ in 1..5 {
        let handle = thread::spawn(|| {
            println!("Hi from thread id {:?}", 
                thread::current().id());
        });
        child_threads.push(handle);
    }
    for i in child_threads {
        i.join().unwrap();
    }
}

修改后的程序确保了 main() 函数在所有子线程执行完毕后才退出,每次运行都会打印出四个线程的信息。但线程的执行顺序仍然是不确定的,这是多线程编程的一个特点,也是需要解决的挑战之一。

2.2 使用 thread::Builder 创建线程

thread::spawn 函数使用的是线程名称和栈大小的默认参数。如果想显式设置这些参数,可以使用 thread::Builder 。以下是使用 Builder 模式重写的示例:

use std::thread;
fn main() {
    let mut child_threads = Vec::new();
    for i in 1..5 {
        let builder = thread::Builder::new().name(format!(
            "mythread{}", i));
        let handle = builder
            .spawn(|| {
                println!("Hi from thread id {:?}", thread::
                    current().name().unwrap());
            })
            .unwrap();
        child_threads.push(handle);
    }

    for i in child_threads {
        i.join().unwrap();
    }
}

在这个代码中,首先使用 new() 函数创建一个新的构建器对象,然后使用 name() 方法配置线程的名称,最后使用 spawn() 方法创建线程。需要注意的是, spawn() 方法返回的是 io::Result<JoinHandle<T>> 类型,所以要对返回值进行解包以获取子进程句柄。运行代码后,会在终端看到四个线程的名称。

3. 线程中的错误处理

3.1 基本错误处理

Rust标准库包含 std::thread::Result 类型,这是专门为线程设计的 Result 类型。以下是一个使用示例:

use std::fs;
use std::thread;
fn copy_file() -> thread::Result<()> {
    thread::spawn(|| {
        fs::copy("a.txt", "b.txt").expect("Error 
            occurred");
    })
    .join()
}
fn main() {
    match copy_file() {
        Ok(_) => println!("Ok. copied"),
        Err(_) => println!("Error in copying file"),
    }
}

在这个示例中, copy_file() 函数将源文件复制到目标文件,并返回 thread::Result<()> 类型。在 main() 函数中使用 match 语句对返回值进行解包,如果 copy_file() 函数返回 Result::Err 变体,就会打印错误信息。

3.2 检测线程恐慌

如果想在错误传播到调用函数之前就识别当前线程是否正在恐慌,可以使用 thread::panicking() 函数。以下是修改后的示例:

use std::fs;
use std::thread;
struct Filenames {
    source: String,
    destination: String,
}
impl Drop for Filenames {
    fn drop(&mut self) {
        if thread::panicking() {
            println!("dropped due to  panic");
        } else {
            println!("dropped without panic");
        }
    }
}
fn copy_file(file_struct: Filenames) -> thread::Result<()> {
    thread::spawn(move || {
        fs::copy(&file_struct.source, 
            &file_struct.destination).expect(
            "Error occurred");
    })
    .join()
}
fn main() {
    let foo = Filenames {
        source: "a1.txt".into(),
        destination: "b.txt".into(),
    };
    match copy_file(foo) {
        Ok(_) => println!("Ok. copied"),
        Err(_) => println!("Error in copying file"),
    }
}

在这个代码中,创建了一个 Filenames 结构体,用于存储要复制的源文件名和目标文件名。同时,为 Filenames 结构体实现了 Drop trait,在 Drop trait的实现中使用 thread::panicking() 函数检测当前线程是否正在恐慌,并根据情况打印相应的信息。运行程序时,如果源文件名无效,会看到以下输出:

dropped due to  panic
Error in copying file

需要注意的是,在传递给 spawn() 函数的闭包中使用了 move 关键字,这是为了将 file_struct 数据结构的所有权从主线程转移到新创建的线程。

4. 线程间的消息传递

4.1 消息传递并发模型

并发编程虽然强大,但并发程序的执行和调试却很困难,因为其执行是不确定的。为了确保程序在不确定的线程执行顺序下也能正确运行,可以引入线程间同步机制。消息传递并发就是这样一种模型,Rust标准库实现了一种名为通道的消息传递并发解决方案。通道类似于管道,由生产者和消费者两部分组成,生产者将消息放入通道,消费者从通道中读取消息。

Rust的通道实现具有多生产者单消费者(mpsc)的特性,也就是说可以有多个发送端,但只有一个接收端。以下是一个逐步构建的示例:

use std::sync::mpsc;
use std::thread;
fn main() {
    let (transmitter1, receiver) = mpsc::channel();
    let transmitter2 = mpsc::Sender::clone(&transmitter1);
    thread::spawn(move || {
        let num_vec: Vec<String> = vec!["One".into(), 
            "two".into(), "three".into(), 
            "four".into()];
        for num in num_vec {
            transmitter1.send(num).unwrap();
        }
    });
    thread::spawn(move || {
        let num_vec: Vec<String> =
            vec!["Five".into(), "Six".into(), 
                "Seven".into(), "eight".into()];
        for num in num_vec {
            transmitter2.send(num).unwrap();
        }
    });
    for received_val in receiver {
        println!("Received from thread: {}", 
            received_val);
    }
}

4.2 操作步骤

  1. 导入标准库中的 mpsc thread 模块。
  2. main() 函数中创建一个新的mpsc通道。
  3. 克隆通道,以便有两个发送线程。
  4. 创建两个新线程,分别将 transmitter1 transmitter2 移动到线程闭包中,并向通道发送一组值。
  5. 在主线程中使用接收句柄从通道中消费子线程发送的值。

运行这个程序时,会在主线程中看到子线程发送的值,但每次运行时接收到的值的顺序可能不同,这是因为线程的执行顺序是不确定的。mpsc通道提供了一种轻量级的线程间同步机制,适用于创建多个线程进行不同类型的计算,并让主线程汇总结果的场景。需要注意的是,一旦值被发送到通道中,发送线程就不再拥有该值的所有权。

4.3 流程图

graph LR
    A[开始] --> B[创建mpsc通道]
    B --> C[克隆通道]
    C --> D[创建线程1并发送值]
    C --> E[创建线程2并发送值]
    D --> F[主线程接收值]
    E --> F
    F --> G[结束]

5. 通过共享状态实现并发

5.1 共享状态并发模型

在Rust标准库中,另一种支持的并发编程模型是共享状态或共享内存模型。由于进程中的所有线程共享相同的进程内存空间,所以可以利用这一点来实现线程间的通信。

实现共享状态并发的主要方式是结合使用 Mutex Arc Mutex (互斥锁)是一种机制,它确保同一时间只有一个线程能够访问某块数据。首先,将数据值包装在 Mutex 类型中,它就像一个带有外部锁的盒子,保护着里面的重要数据。要访问盒子里的内容,必须先请求打开锁并获取盒子,使用完后再将盒子交回。

同样,要访问或修改受 Mutex 保护的值,必须先获取锁。请求 Mutex 对象的锁会返回一个 MutexGuard 类型,通过它可以访问内部值。在此期间,其他线程无法访问受 MutexGuard 保护的值。使用完后, MutexGuard 会在超出作用域时自动释放锁。

为了支持一个值被多个线程拥有,Rust使用了引用计数智能指针 Rc Arc Rc 通过其 clone() 方法允许一个值有多个所有者,但 Rc 不能安全地跨线程使用,而 Arc (原子引用计数)是 Rc 的线程安全版本。因此,需要将 Mutex 包装在 Arc 引用计数智能指针中,并将值的所有权在多个线程间传递。当受 Arc 保护的 Mutex 的所有权转移到另一个线程后,接收线程可以调用 Mutex lock() 方法来获取对内部值的独占访问权。

5.2 示例程序

下面通过一个逐步示例来展示如何使用 Mutex Arc 实现共享状态并发。我们将修改之前计算目录树中所有Rust文件源文件统计信息的示例,使其成为一个并发程序。

use std::ffi::OsStr;
use std::fs;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::thread;

#[derive(Debug)]
pub struct SrcStats {
    pub number_of_files: u32,
    pub loc: u32,
    pub comments: u32,
    pub blanks: u32,
}

fn main() {
    let src_stats = SrcStats {
        number_of_files: 0,
        loc: 0,
        comments: 0,
        blanks: 0,
    };
    let stats_counter = Arc::new(
        Mutex::new(src_stats));
    let mut dir_list = File::open(
        "./dirnames.txt").unwrap();
    let reader = BufReader::new(&mut dir_list);
    let dir_lines: Vec<_> = reader.lines().collect();
    for dir in dir_lines {
        let stats_counter = Arc::clone(&stats_counter);
        thread::spawn(move || {
            let dir_path = PathBuf::from(dir.unwrap());
            // 遍历目录树并计算统计信息
            // 更新stats_counter中的值
        });
    }
    // 等待所有线程完成
}

5.3 操作步骤

  1. 导入必要的模块。
  2. 定义一个结构体 SrcStats 来存储源文件统计信息。
  3. main() 函数中创建一个 SrcStats 实例,用 Mutex 保护它,并将其包装在 Arc 中。
  4. 读取 dirnames.txt 文件,将每个目录名存储在一个向量中。
  5. 遍历目录名向量,为每个目录创建一个新线程。在每个线程中,递归遍历目录结构,计算Rust源文件的统计信息,并更新受 Mutex Arc 保护的共享数据结构。

5.4 流程图

graph LR
    A[开始] --> B[创建SrcStats实例并包装]
    B --> C[读取dirnames.txt文件]
    C --> D[遍历目录名向量]
    D --> E[创建线程]
    E --> F[遍历目录树计算统计信息]
    F --> G[更新共享数据]
    D --> H{是否遍历完所有目录}
    H -- 否 --> D
    H -- 是 --> I[等待所有线程完成]
    I --> J[结束]

通过以上内容,我们详细介绍了Rust中的并发编程,包括线程的创建和配置、错误处理、线程间的消息传递以及共享状态并发等方面。掌握这些知识,能够帮助我们编写高效、安全的并发程序。

6. 共享状态并发示例程序详解

6.1 程序结构概述

我们要实现的程序是接收一个目录列表作为输入,计算每个目录下所有Rust文件的源文件统计信息,并输出汇总的源代码统计数据。具体步骤如下:
1. 在Cargo项目的根文件夹中创建一个 dirnames.txt 文件,该文件包含完整路径的目录列表,每行一个目录。
2. 读取该文件的每个条目,并为每个目录创建一个单独的线程,用于计算该目录树中Rust文件的源文件统计信息。
3. 每个线程将计算得到的值累加到一个共享的数据结构中,我们使用 Mutex Arc 来安全地保护对共享数据的访问和更新。

6.2 代码实现

use std::ffi::OsStr;
use std::fs;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::thread;

#[derive(Debug)]
pub struct SrcStats {
    pub number_of_files: u32,
    pub loc: u32,
    pub comments: u32,
    pub blanks: u32,
}

fn main() {
    let src_stats = SrcStats {
        number_of_files: 0,
        loc: 0,
        comments: 0,
        blanks: 0,
    };
    let stats_counter = Arc::new(
        Mutex::new(src_stats));
    let mut dir_list = File::open(
        "./dirnames.txt").unwrap();
    let reader = BufReader::new(&mut dir_list);
    let dir_lines: Vec<_> = reader.lines().collect();

    let mut handles = vec![];
    for dir in dir_lines {
        let stats_counter = Arc::clone(&stats_counter);
        let handle = thread::spawn(move || {
            let dir_path = PathBuf::from(dir.unwrap());
            let mut stats = SrcStats {
                number_of_files: 0,
                loc: 0,
                comments: 0,
                blanks: 0,
            };
            if let Ok(entries) = fs::read_dir(dir_path) {
                for entry in entries {
                    if let Ok(entry) = entry {
                        let path = entry.path();
                        if path.extension() == Some(OsStr::new("rs")) {
                            if let Ok(file) = File::open(&path) {
                                let reader = BufReader::new(file);
                                for line in reader.lines() {
                                    if let Ok(line) = line {
                                        let trimmed = line.trim();
                                        if trimmed.is_empty() {
                                            stats.blanks += 1;
                                        } else if trimmed.starts_with("//") {
                                            stats.comments += 1;
                                        } else {
                                            stats.loc += 1;
                                        }
                                    }
                                }
                                stats.number_of_files += 1;
                            }
                        }
                    }
                }
            }
            let mut locked_stats = stats_counter.lock().unwrap();
            locked_stats.number_of_files += stats.number_of_files;
            locked_stats.loc += stats.loc;
            locked_stats.comments += stats.comments;
            locked_stats.blanks += stats.blanks;
        });
        handles.push(handle);
    }

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

    let final_stats = stats_counter.lock().unwrap();
    println!("Final Statistics: {:?}", final_stats);
}

6.3 代码解释

  1. 导入模块 :导入了处理文件系统、输入输出、路径、同步和线程相关的模块。
  2. 定义结构体 SrcStats 结构体用于存储源文件的统计信息,包括文件数量、代码行数、注释行数和空行数。
  3. 初始化共享数据 :在 main 函数中,创建 SrcStats 实例,用 Mutex 包装并使用 Arc 实现多线程共享。
  4. 读取目录列表 :打开 dirnames.txt 文件,将每行的目录名存储在 dir_lines 向量中。
  5. 创建线程 :遍历 dir_lines ,为每个目录创建一个线程。在每个线程中:
    • 初始化一个局部的 SrcStats 结构体用于存储当前目录的统计信息。
    • 遍历目录树,对于每个Rust文件,逐行读取文件内容,根据行的内容更新局部统计信息。
    • 获取 Mutex 锁,将局部统计信息累加到共享的 stats_counter 中。
  6. 等待线程完成 :使用 join 方法等待所有线程完成。
  7. 输出最终结果 :获取 Mutex 锁,打印最终的统计信息。

6.4 注意事项

  • 锁的使用 :在访问和修改受 Mutex 保护的共享数据时,必须先获取锁,使用完后会自动释放。
  • 所有权转移 :使用 Arc::clone 来复制 Arc 指针,实现多线程间的数据共享,同时保证线程安全。

7. 并发编程的挑战与应对策略

7.1 线程执行顺序不确定性

在多线程编程中,线程的执行顺序是不确定的,这可能导致程序的输出结果每次运行都不同。例如,在使用 thread::spawn 创建多个线程时,无法保证哪个线程先执行。

应对策略
- 使用 join 方法 :确保主线程等待所有子线程执行完毕后再继续执行,如前面创建线程的示例中,将子线程的句柄存储在向量中,最后遍历向量调用 join 方法。
- 使用同步机制 :如 Mutex Semaphore 等,控制线程对共享资源的访问顺序。

7.2 数据竞争

当多个线程同时访问和修改共享数据时,可能会发生数据竞争,导致数据不一致或程序崩溃。例如,多个线程同时对一个共享的计数器进行递增操作。

应对策略
- 使用 Mutex :确保同一时间只有一个线程能够访问和修改共享数据,如共享状态并发示例中,使用 Mutex 保护 SrcStats 结构体。
- 使用原子类型 :对于简单的共享数据,如计数器,可以使用Rust标准库中的原子类型,如 AtomicU32 ,它们提供了线程安全的操作。

7.3 死锁

死锁是指两个或多个线程相互等待对方释放锁,从而导致所有线程都无法继续执行的情况。例如,线程A持有锁L1并请求锁L2,而线程B持有锁L2并请求锁L1。

应对策略
- 避免嵌套锁 :尽量避免在持有一个锁的同时请求另一个锁,减少死锁的可能性。
- 统一锁的获取顺序 :所有线程按照相同的顺序获取锁,避免循环等待。

7.4 线程恐慌处理

线程恐慌可能会导致程序崩溃,因此需要在多线程程序中正确处理线程恐慌。

应对策略
- 使用 catch_unwind :在创建线程时,可以使用 catch_unwind 来捕获线程中的恐慌,避免程序崩溃。
- 使用 thread::panicking :在 Drop trait的实现中使用 thread::panicking 函数,检测当前线程是否正在恐慌,并进行相应的处理。

8. 总结

8.1 并发编程要点回顾

  • 线程创建与配置 :可以使用 thread::spawn thread::Builder 来创建和配置线程,使用 join 方法确保主线程等待子线程完成。
  • 错误处理 :使用 std::thread::Result 类型处理线程中的错误,使用 thread::panicking 检测线程恐慌。
  • 消息传递 :通过 mpsc 通道实现线程间的消息传递,支持多生产者单消费者模式。
  • 共享状态并发 :结合使用 Mutex Arc 实现共享状态并发,确保线程安全地访问和修改共享数据。

8.2 并发编程的优势与应用场景

并发编程可以充分利用多核处理器的性能,提高程序的执行效率。在以下场景中,并发编程尤为有用:
- I/O密集型任务 :如网络编程、文件读写等,使用多线程可以在等待I/O操作完成时执行其他任务,提高程序的响应速度。
- 计算密集型任务 :将复杂的计算任务分解为多个子任务,并行执行,加快计算速度。

8.3 未来展望

随着计算机硬件技术的不断发展,多核处理器将越来越普及,并发编程的重要性也将日益凸显。Rust作为一种安全、高效的系统编程语言,其强大的并发编程能力将在更多的领域得到应用,如分布式系统、人工智能等。掌握Rust的并发编程技术,将为开发者带来更多的机遇和挑战。

通过本文的学习,希望读者能够深入理解Rust中的并发编程概念和技术,掌握线程的创建、配置、错误处理、消息传递和共享状态并发等方面的知识,从而编写高效、安全的并发程序。

8.4 常见问题解答

问题 解答
为什么线程执行顺序不确定? 操作系统的调度算法决定了线程的执行顺序,不同的调度策略会导致线程执行顺序的不确定性。
Mutex Arc 的作用是什么? Mutex 用于确保同一时间只有一个线程能够访问共享数据, Arc 用于实现数据的多线程共享,保证线程安全。
如何避免死锁? 避免嵌套锁,统一锁的获取顺序,确保线程不会相互等待对方释放锁。
mpsc 通道的优势是什么? mpsc 通道提供了轻量级的线程间同步机制,支持多生产者单消费者模式,适用于线程间的消息传递。
【无人机】基于改进粒子群算法的无人机路径规划研究[和遗传算法、粒子群算法进行比较](Matlab代码实现)内容概要:本文围绕基于改进粒子群算法的无人机路径规划展开研究,重点探讨了在复杂环境中利用改进粒子群算法(PSO)实现无人机三维路径规划的方法,并将其与遗传算法(GA)、标准粒子群算法等传统优化算法进行对比分析。研究内容涵盖路径规划的多目标优化、避障策略、航路点约束以及算法收敛性和寻优能力的评估,所有实验均通过Matlab代码实现,提供了完整的仿真验证流程。文章还提到了多种智能优化算法在无人机路径规划中的应用比较,突出了改进PSO在收敛速度和局寻优方面的优势。; 适合人群:具备一定Matlab编程基础和优化算法知识的研究生、科研人员及从事无人机路径规划、智能优化算法研究的相关技术人员。; 使用场景及目标:①用于无人机在复杂地形或动态环境下的三维路径规划仿真研究;②比较不同智能优化算法(如PSO、GA、蚁群算法、RRT等)在路径规划中的性能差异;③为多目标优化问题提供算法选型和改进思路。; 阅读建议:建议读者结合文中提供的Matlab代码进行实践操作,重点关注算法的参数设置、适应度函数设计及路径约束处理方式,同时可参考文中提到的多种算法对比思路,拓展到其他智能优化算法的研究与改进中。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值