19、Rust编程:从编写简易shell到并发编程基础

Rust Shell与并发编程详解

Rust编程:从编写简易shell到并发编程基础

1. 用Rust编写shell程序

1.1 项目准备

我们将逐步构建一个shell程序,每次迭代添加新功能。首先,创建一个新项目并设置文件结构:
1. 创建新项目:

cargo new myshell && cd myshell
  1. 创建三个文件: src/iter1.rs src/iter2.rs src/iter3.rs ,分别存放三次迭代的代码。
  2. Cargo.toml 中添加以下内容:
[[bin]]
name = "iter1"
path = "src/iter1.rs"
[[bin]]
name = "iter2"
path = "src/iter2.rs"
[[bin]]
name = "iter3"
path = "src/iter3.rs"

这将为三次迭代分别构建独立的二进制文件。

1.2 第一次迭代:创建子进程执行命令

第一次迭代的目标是编写一个程序,从终端接受命令,并创建子进程执行这些命令。代码如下:

// src/iter1.rs
use std::io::Write;
use std::io::{stdin, stdout};
use std::process::Command;
fn main() {
    loop {
        print!("$ ");                               // <1>
        stdout().flush().unwrap();                  // <2>
        let mut user_input = String::new();         // <3>
        stdin()
            .read_line(&mut user_input)             // <4>
            .expect("Unable to read user input");   
        let command_to_execute = user_input.trim(); // <5>
        let mut child = Command::new(command_to_execute) // <6>
            .spawn()
            .expect("Unable to execute command");
        child.wait().unwrap();                       // <7>
    }
}

代码解释:
1. 显示 $ 提示符,提示用户输入命令。
2. 刷新 stdout 句柄,确保 $ 提示符立即显示在终端上。
3. 创建一个缓冲区来保存用户输入的命令。
4. 逐行读取用户命令。
5. 从缓冲区中移除换行符(用户按下回车键提交命令时添加)。
6. 创建一个新的子进程,并将用户命令传递给子进程执行。
7. 等待子进程完成执行后,再接受额外的用户输入。

运行程序:

cargo run --bin iter1

$ 提示符下输入无参数的命令,如 ls ps du ,将看到命令执行的输出显示在终端上。按 Ctrl + C 退出程序。不过,此程序在命令后输入参数或标志时会失败。

1.3 第二次迭代:支持命令参数

第二次迭代使用 args() 方法添加对命令参数的支持:

// src/iter2.rs
// Module imports not shown here
fn main() {
    loop {
        print!("$ ");
        stdout().flush().unwrap();
        let mut user_input = String::new();
        stdin()
            .read_line(&mut user_input)
            .expect("Unable to read user input");
        let command_to_execute = user_input.trim();
        let command_args: Vec<&str> =      
            command_to_execute.split_whitespace().
            collect(); // <1>

        let mut child = Command::new(command_args[0]) // <2>
            .args(&command_args[1..])   // <3>
            .spawn()
            .expect("Unable to execute command");
        child.wait().unwrap();
    }
}

代码解释:
1. 将用户输入按空格分割,并将结果存储在 Vec 中。
2. Vec 的第一个元素对应命令,创建子进程执行此命令。
3. 将 Vec 中从第二个元素开始的列表作为参数传递给子进程。

运行程序:

cargo run --bin iter2

在按下回车键之前输入命令并传递参数,例如:
- ls –lah
- ps -ef
- cat a.txt (假设 a.txt 是项目根文件夹中包含某些内容的现有文件)

1.4 第三次迭代:支持自然语言命令

第三次迭代实现对自然语言命令的支持,例如用 show files 代替 ls 。代码如下:

// src/iter3.rs
use std::io::Write;
use std::io::{stdin, stdout};
use std::io::{Error, ErrorKind};
use std::process::Command;

fn main() {
    loop {
        print!("$ ");
        stdout().flush().unwrap();
        let mut user_input = String::new();
        stdin()
            .read_line(&mut user_input)
            .expect("Unable to read user input");
        let command_to_execute = user_input.trim();
        let command_args: Vec<&str> = command_to_execute.split_whitespace().collect();

        if command_args.len() > 0 {
            let child = match command_args[0] {
                "show" if command_args.len() > 1 => match command_args[1] {
                    "files" => Command::new("ls").args(&command_args[2..]).spawn(),
                    "process" => Command::new("ps").args(&command_args[2..]).spawn(),
                    _ => Err(Error::new(
                        ErrorKind::InvalidInput,
                        "please enter valid command",
                    )),
                },
                "show" if command_args.len() == 1 => Err(Error::new(
                    ErrorKind::InvalidInput,
                    "please enter valid command",
                )),
                "quit" => std::process::exit(0),
                _ => Command::new(command_args[0]).args(&command_args[1..]).spawn(),
            };

            match child {
                Ok(mut child) => {
                    if child.wait().unwrap().success() {
                    } else {
                        println!("\n{}", "Child process failed")
                    }
                }
                Err(e) => match e.kind() {
                    ErrorKind::InvalidInput => eprintln!(
                        "Sorry, show command only supports following options: files , process "
                    ),
                    _ => eprintln!("Please enter a valid command"),
                },
            }
        }
    }
}

运行程序:

cargo run --bin iter3

$ 提示符下测试以下命令:
- show files
- show process
- du

此迭代还添加了错误处理,处理以下错误情况:
- 用户不输入命令直接按回车键。
- 用户输入 show 命令但无参数(文件或进程)。
- 用户输入 show 命令但参数无效。
- 用户输入有效的Unix命令,但程序不支持(例如管道或重定向)。

1.5 总结与扩展

我们已经实现了一个基本的shell程序,能够识别自然语言命令,并处理了一些错误情况。作为练习,可以进行以下扩展:
- 添加对管道运算符分隔的命令链的支持,如 ps | grep sys
- 添加对重定向(如 > 运算符)的支持,将进程执行的输出转移到文件中。
- 将命令行解析逻辑移到单独的分词器模块中。

2. 并发编程基础

2.1 并发编程的价值

现代程序需要快速做出决策或在短时间内处理大量数据,许多用例无法通过顺序执行实现。例如,自动驾驶汽车需要同时处理来自各种传感器的输入、规划车辆路径并向执行器发送指令;网页浏览器需要同时处理用户输入和逐步渲染网页;网站需要处理多个用户的并发请求;网络爬虫需要同时访问数千个网站。

此外,单核CPU时钟速度已接近实际上限,需要增加CPU核心和处理器,这推动了软件对并发执行的需求,以充分利用额外的CPU核心。

2.2 并发与并行的区别

2.2.1 顺序执行

假设一个进程有两个任务A和B,任务A有三个子任务A1、A2和A3,任务B有两个子任务B1和B2,所有任务按顺序执行。如果任务A2需要等待外部网络、用户输入或系统资源,那么A2之后的所有任务都将被阻塞,直到A2完成,这不是CPU的有效利用方式,会导致进程中所有计划任务的完成延迟。

2.2.2 并发执行

现代应用程序通常是并发的,有多个执行线程同时运行。在并发模型中,进程交错执行任务,交替执行任务A和任务B,直到两者都完成。即使A2被阻塞,其他子任务仍可继续进行。每个子任务可以安排在单独的执行线程上,这些线程可以在单个处理器上运行,也可以跨多个处理器核心调度。并发是关于顺序无关的计算,编写适应顺序无关计算的程序比编写顺序程序更具挑战性。

2.2.3 并行执行

并行执行是并发执行模型的一种变体,进程在单独的CPU处理器或核心上真正并行地执行任务A和任务B。这假设软件的编写方式允许这种并行执行,并且任务A和任务B之间没有依赖关系,不会导致执行停滞或数据损坏。并行计算是一个广义术语,可以通过单台机器的多核或多处理器实现,也可以通过不同计算机的集群协同执行一组任务。

2.2.4 何时使用并发与并行执行
  • 计算密集型程序 :涉及大量计算,如图形、气象或基因组处理,这类程序大部分时间使用CPU周期,使用更好更快的CPU会受益。
  • I/O密集型程序 :大部分处理涉及与输入/输出设备(如网络套接字、文件系统和其他设备)通信,使用更快的I/O子系统(如磁盘或网络访问)会受益。

一般来说,并行执行(真正的并行性)更适合提高计算密集型用例中程序的吞吐量,而并发处理(或伪并行性)适合提高I/O密集型用例中的吞吐量和减少延迟。

2.3 多线程概念

Unix支持线程作为进程同时执行多个任务的机制。一个Unix进程启动时只有一个线程,即主执行线程,但可以创建额外的线程,这些线程可以在单处理器系统中并发执行,或在多处理器系统中并行执行。

每个线程都有自己的栈,用于存储局部变量和函数参数,还维护自己的寄存器状态,包括栈指针和程序计数器。一个进程中的所有线程共享相同的内存地址空间,意味着它们共享对数据段(初始化数据、未初始化数据和堆)的访问,也共享相同的程序代码(进程指令)。

在多线程进程中,多个线程可以同时执行同一个程序的不同部分(如不同函数),或在不同线程中调用同一个函数(处理不同的数据集)。但一个函数要能被多个线程同时调用,需要是线程安全的。使函数线程安全的方法包括:避免在函数中使用全局或静态变量、使用互斥锁限制函数一次只能由一个线程使用、使用互斥锁同步对共享数据的使用。

线程和进程模型各有优缺点:
| 对比项 | 线程 | 进程 |
| ---- | ---- | ---- |
| 数据共享 | 同一进程空间内,数据共享容易 | 数据共享复杂 |
| 资源共享 | 共享进程的公共资源,如文件描述符和用户/组ID | 资源独立 |
| 创建速度 | 线程创建速度快 | 进程创建速度慢 |
| 上下文切换 | 由于共享内存空间,CPU上下文切换快 | 上下文切换慢 |
| 复杂性 | 共享函数需线程安全,访问共享全局数据需同步,一个线程的缺陷可能影响其他线程或整个进程,代码执行顺序无保证,可能导致数据竞争、死锁或难以重现的错误 | 相对独立,一个进程的问题一般不会影响其他进程 |

2.4 多线程的内存布局

所有线程都在进程内存空间内分配内存。默认情况下,主线程有自己的栈,创建额外线程时也会为其分配自己的栈。进程的全局和静态变量可被所有线程访问,每个线程还可以将堆上创建的内存指针传递给其他线程,从而实现共享并发模型。

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    classDef thread fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;

    P1(进程P1):::process --> MT(主线程):::thread
    P1 --> T1(线程1):::thread
    P1 --> T2(线程2):::thread
    P1 --> T3(线程3):::thread

    MT --> MST(主线程栈):::thread
    T1 --> T1ST(线程1栈):::thread
    T2 --> T2ST(线程2栈):::thread
    T3 --> T3ST(线程3栈):::thread

    P1 --> DS(数据段):::process
    P1 --> PC(程序代码):::process
    MST --> DS
    T1ST --> DS
    T2ST --> DS
    T3ST --> DS
    MST --> PC
    T1ST --> PC
    T2ST --> PC
    T3ST --> PC

通过学习并发编程基础,我们了解了如何创建子进程、与子进程的标准输入和标准输出交互、执行带参数的命令、设置和清除环境变量、处理进程错误和外部信号,以及编写一个能执行标准Unix命令并接受自然语言命令的shell程序。同时,我们也掌握了并发和并行的概念、多线程的实现和内存布局。为了进一步巩固学习,建议编写代码完成上述shell程序的扩展练习。

2.5 线程的创建与配置

在 Rust 中,可以使用 std::thread 模块来创建和管理线程。下面是一个简单的线程创建示例:

use std::thread;

fn main() {
    let handle = thread::spawn(|| {
        // 线程执行的代码
        println!("This is a new thread!");
    });

    // 等待新线程执行完毕
    handle.join().unwrap();

    println!("Main thread is done.");
}

代码解释:
1. thread::spawn 函数用于创建一个新线程,它接受一个闭包作为参数,闭包中的代码将在新线程中执行。
2. spawn 函数返回一个 JoinHandle 类型的句柄,用于等待新线程执行完毕。
3. handle.join().unwrap() 会阻塞主线程,直到新线程执行完毕。

2.6 线程中的错误处理

在多线程程序中,错误处理尤为重要。当一个线程出现错误时,我们需要确保不会影响其他线程的正常运行。下面是一个包含错误处理的线程示例:

use std::thread;
use std::io::{Error, ErrorKind};

fn main() {
    let handle = thread::spawn(|| {
        let result = do_something_that_might_fail();
        if let Err(e) = result {
            eprintln!("Error in thread: {}", e);
        }
    });

    handle.join().unwrap();
    println!("Main thread is done.");
}

fn do_something_that_might_fail() -> Result<(), Error> {
    // 模拟一个可能失败的操作
    Err(Error::new(ErrorKind::Other, "Something went wrong!"))
}

代码解释:
1. do_something_that_might_fail 函数返回一个 Result 类型,用于表示操作是否成功。
2. 在新线程中,如果操作失败,会打印错误信息。
3. 主线程等待新线程执行完毕,确保错误被正确处理。

2.7 线程间的消息传递

线程间的消息传递是并发编程中的重要概念。在 Rust 中,可以使用 std::sync::mpsc 模块来实现多生产者单消费者(MPSC)的消息传递。下面是一个简单的示例:

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

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

    let handle = thread::spawn(move || {
        // 发送消息
        tx.send("Hello from the new thread!").unwrap();
    });

    // 接收消息
    let msg = rx.recv().unwrap();
    println!("Received: {}", msg);

    handle.join().unwrap();
}

代码解释:
1. mpsc::channel 函数创建一个通道,返回一个发送端 tx 和一个接收端 rx
2. 在新线程中,使用 tx.send 函数发送消息。
3. 在主线程中,使用 rx.recv 函数接收消息。

2.8 共享状态下的并发

在多线程程序中,多个线程可能会同时访问和修改共享状态,这可能会导致数据竞争和其他并发问题。在 Rust 中,可以使用 std::sync 模块中的同步原语(如 Mutex RwLock 等)来确保线程安全。下面是一个使用 Mutex 的示例:

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

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!("Result: {}", *counter.lock().unwrap());
}

代码解释:
1. Arc 是原子引用计数类型,用于在多个线程之间共享数据。
2. Mutex 是互斥锁,用于确保同一时间只有一个线程可以访问共享数据。
3. counter.lock().unwrap() 用于获取互斥锁,修改共享数据后会自动释放锁。

2.9 定时器暂停线程执行

在 Rust 中,可以使用 std::thread::sleep 函数来暂停线程的执行。下面是一个简单的示例:

use std::thread;
use std::time::Duration;

fn main() {
    println!("Going to sleep for 2 seconds...");
    thread::sleep(Duration::from_secs(2));
    println!("Woke up after 2 seconds!");
}

代码解释:
1. Duration::from_secs(2) 创建一个 2 秒的时间间隔。
2. thread::sleep 函数会暂停当前线程的执行,直到指定的时间间隔过去。

2.10 总结与展望

通过学习并发编程,我们掌握了 Rust 中线程的创建、配置、错误处理、消息传递、共享状态并发和定时器的使用。并发编程可以让程序更高效地利用系统资源,处理多个任务。但同时,并发编程也带来了一些挑战,如数据竞争、死锁等。因此,在编写并发程序时,需要仔细设计和使用同步原语来确保线程安全。

为了进一步提升并发编程能力,建议进行以下练习:
1. 实现一个多线程的网络爬虫,使用消息传递机制协调不同线程的工作。
2. 编写一个多线程的文件处理程序,使用共享状态并发来提高处理效率。
3. 尝试使用不同的同步原语(如 RwLock )来优化并发程序的性能。

通过不断实践和学习,我们可以更好地掌握并发编程的技巧,编写出高效、安全的并发程序。

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    classDef thread fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;

    A(主线程):::thread --> B(创建新线程):::thread
    B --> C(发送消息):::thread
    A --> D(接收消息):::thread
    B --> E(修改共享数据):::thread
    A --> F(读取共享数据):::thread
    B --> G(线程休眠):::thread
    A --> H(等待线程结束):::thread

以上表格和流程图展示了并发编程中线程的主要操作流程,包括线程创建、消息传递、共享数据操作和线程休眠等。通过这些操作,我们可以构建出复杂的并发系统。

本资源为黑龙江省 2023 年水系分布数据,涵盖河流、沟渠、支流等线状要素,以及湖泊、水库、湿地等面状水体,提供完整的二维水文地理框架。数据以标准 GIS 格式发布,包含可编辑 MXD 工程文件、Shapefile 数据以及标准制图 TIF,适用于科研、规划设计、生态评估与地图制图等多类应用场景。 【数据内容】 1、水系线状要素(.shp) 包括主要河流、支流、人工渠道等 属性字段涵盖:名称、类别等 线要素拓扑规范,无断裂与悬挂节点 2、水体面状要素(.shp) 覆盖湖泊、水库、池塘、湿地等面状水体 属性包含:名称、类型等信息 几何边界经过平滑与精修,保证面积统计可靠 3、可编辑 MXD 工程文件(.mxd) 预设图层渲染、图例、比例尺、指北针与布局 支持用户根据自身制图需求快速调整样式、色带及标注规则 博主使用的 ArcMap 10.8 环境 4、标准成图 TIF(.tif) 专业级地图输出,含必要图廓与标注,可直接用于报告、论文与展示 输出分辨率高,适合印刷与电子稿应用 【数据技术说明】 坐标系统:WGS 84 地理坐标系 数据年份:2023 年 制作流程:基于卫星影像、水利普查数据和地理编码信息进行提取 → 几何校正 → 拓扑审查 → 分类整理 → 成图渲染 质量控制措施:保证线状与面状水体不重叠、不缺失;对水库与湖泊边界进行了人工校核,提高空间精度 【应用价值】 地表水资源调查与监测,水利、水文模型的空间输入,城市与农村规划中的水系布局分析,生态修复、水环境治理与湿地保护研究,教学、制图与地理信息可视化应用 【使用说明】 首次打开 MXD 文件前,请确保 Shapefile 和栅格文件均已解压至同一目录,以免出现路径丢失。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值