Rust

2025年09月16日更新 8 人订阅
原价: ¥ 6 限时优惠
专栏简介 Rust编程语言之错误处理 Rust 语言之 flod Rust编程语言之Cargo、Crates.io详解 Rust编程语言之枚举与模式匹配 Rust语言 - 接口设计的建议之受约束(Constrained) Rust编程语言之无畏并发 Rust语言 - 接口设计的建议之灵活(flexible) Rust语言 - 接口设计的建议之显而易见(Obvious) Rust语言 - 接口设计的建议之不意外(unsurprising) Rust 实战:构建实用的 CLI 工具 HTTPie Rust编程语言学习之高级特性 Rust内存管理揭秘:深度剖析指针与智能指针 解决Rust中数组和切片的编译时大小问题 《Rust编程之道》学习笔记一 Rust Async 异步编程 简易教程 使用 Async Rust 构建简单的 P2P 节点 Rust编程语言入门之模式匹配 Rust async 编程 Rust编程语言之编写自动化测试 Rust编程语言之函数式语言特性:迭代器和闭包 《Rust编程之道》学习笔记二 Rust Tips 比较数值 使用 Rust 开发一个微型游戏 Rust编程初探:深入理解Struct结构体 深入理解Rust中的内存管理:栈、堆与静态内存详解 深入理解 Rust 结构体:经典结构体、元组结构体和单元结构体的实现 深入掌握 Rust 结构体:从模板到实例化的完整指南 深入理解Rust中的结构体:逻辑与数据结合的实战示例 深入理解 Rust 枚举:从基础到实践 掌握Rust字符串的精髓:String与&str的最佳实践 全面解析 Rust 模块系统:实战案例与应用技巧 Rust 中的 HashMap 实战指南:理解与优化技巧 掌握Rust模式匹配:从基础语法到实际应用 Rust 中的面向对象编程:特性与实现指南 深入理解 Rust 的 Pin 和 Unpin:理论与实践解析 Rust Trait 与 Go Interface:从设计到实战的深度对比 从零开始:用 Rust 和 Axum 打造高效 Web 应用 Rust 错误处理详解:掌握 anyhow、thiserror 和 snafu Rust 如何优雅实现冒泡排序 链表倒数 K 节点怎么删?Python/Go/Rust 实战 用 Rust 玩转数据存储:JSON 文件持久化实战 Rust实战:打造高效字符串分割函数 如何高效学习一门技术:从知到行的飞轮效应 Rust 编程入门:Struct 让代码更优雅 Rust 编程:零基础入门高性能开发 用 Rust 写个猜数游戏,编程小白也能上手! Rust 入门教程:变量到数据类型,轻松掌握! 深入浅出 Rust:函数、控制流与所有权核心特性解析 从零开始:用 Rust 和 Axum 打造高效 Web 服务 Rust 集合类型解析:Vector、String、HashMap 深入浅出Rust:泛型、Trait与生命周期的硬核指南 Rust实战:博物馆门票限流系统设计与实现 用 Rust 打造高性能图片处理服务器:从零开始实现类似 Thumbor 的功能 Rust 编程入门实战:从零开始抓取网页并转换为 Markdown 深入浅出 Rust:高效处理二进制数据的 Bytes 与 BytesMut 实战 Rust智能指针:解锁内存管理的进阶之道 用 Rust 打造命令行利器:从零到一实现 mini-grep 解锁Rust代码组织:轻松掌握Package、Crate与Module Rust 所有权:从内存管理到生产力释放 深入解析 Rust 的面向对象编程:特性、实现与设计模式 Rust + Protobuf:从零打造高效键值存储项目 bacon 点燃 Rust:比 cargo-watch 更爽的开发体验 用 Rust 打造微型游戏:从零开始的 Flappy Dragon 开发之旅 函数式编程的Rust之旅:闭包与迭代器的深入解析与实践 探索Rust编程之道:从设计哲学到内存安全的学习笔记 精读《Rust编程之道》:吃透语言精要,彻底搞懂所有权与借用 Rust 避坑指南:搞定数值比较,别再让 0.1 + 0.2 != 0.3 困扰你! 告别 Vec!掌握 Rust bytes 库,解锁零拷贝的真正威力 告别竞态条件:基于 Axum 和 Serde 的 Rust 并发状态管理最佳实践 Rust 异步编程实践:从 Tokio 基础到阻塞任务处理模式 Rust 网络编程实战:用 Tokio 手写一个迷你 TCP 反向代理 (minginx) 保姆级教程:Zsh + Oh My Zsh 终极配置,让你的 Ubuntu 终端效率倍增 不止于后端:Rust 在 Web 开发中的崛起之路 (2024数据解读) Rust核心利器:枚举(Enum)与模式匹配(Match),告别空指针,写出优雅健壮的代码 Rust 错误处理终极指南:从 panic! 到 Result 的优雅之道 想用 Rust 开发游戏?这份超详细的入门教程请收好! 用 Rust 实现 HTTPie:一个现代 CLI 工具的构建过程 Rust 异步实战:从0到1,用 Tokio 打造一个高性能并发聊天室 深入 Rust 核心:彻底搞懂指针、引用与智能指针 Rust 生产级后端实战:用 Axum + sqlx 打造高性能短链接服务 深入 Rust 内存模型:栈、堆、所有权与底层原理 Rust 核心概念解析:引用、借用与内部可变性 掌握 Rust 核心:生命周期与借用检查全解析 Rust 内存布局深度解析:从对齐、填充到 repr 属性 Rust Trait 分派机制:静态与动态的抉择与权衡 Rust Thread::Builder 用法详解:线程命名与栈大小设置 Rust 泛型 Trait:关联类型与泛型参数的核心区别 Rust Scoped Threads 实战:更安全、更简洁的并发编程 Rust 核心设计:孤儿规则与代码一致性解析 Rust 实战:从零构建一个多线程 Web 服务器 Rust Web 开发实战:构建教师管理 API 硬核实战:从零到一,用 Rust 和 Axum 构建高性能聊天服务后端 Rust Web 开发实战:使用 SQLx 连接 PostgreSQL 数据库 硬核入门:从零开始,用 Actix Web 构建你的第一个 Rust REST API (推荐 🔥) Rust 并发编程:详解线程间数据共享的几种核心方法 Rust并发安全基石:Mutex与RwLock深度解析 Rust Web实战:构建优雅的 Actix Web 统一错误处理 煮咖啡里的大学问:用 Rust Async/Await 告诉你如何边烧水边磨豆 深入浅出:Rust 原子类型与多线程编程实践

深入浅出:Rust 原子类型与多线程编程实践

深入浅出:Rust原子类型与多线程编程实践在现代软件开发中,充分利用多核CPU的性能至关重要。然而,在多线程环境中共享数据,一不小心就可能引入棘手的数据竞争问题。Rust以其出色的内存安全机制而闻名,但它如何解决多线程下的并发挑战呢?答案就是:原子类型(AtomicTypes)。本文将带

深入浅出:Rust 原子类型与多线程编程实践

在现代软件开发中,充分利用多核 CPU 的性能至关重要。然而,在多线程环境中共享数据,一不小心就可能引入棘手的数据竞争问题。Rust 以其出色的内存安全机制而闻名,但它如何解决多线程下的并发挑战呢?答案就是:原子类型(Atomic Types)。本文将带你深入探索 Rust 的原子操作,通过实际代码案例,为你揭示如何安全、高效地在线程间共享数据,并解决那些看似简单的并发陷阱。

Rust 多线程:Atomics 原子性

Atomic Type 原子类型

原子类型(Atomic Type)提供线程之间原始的共享内存通信机制,是其他并发类型的基础构件

Atomic Operations 原子操作

Atomic:不可分割的操作,要么执行完了,要么还没发生,它不存在一个中间的状态,比如说执行一半这个状态它是不存在的,或者更准确的说它这个中间状态对外是不可见的。

原子操作允许不同线程安全地读取和修改同一个变量。

原子操作是并发编程的基础。

所有高级并发工具都是通过原子操作实现的。

原子类型 Atomic Types

位于 std::sync::atomic,以 Atomic 开头。例如:AtomicBool、AtomicIsize、AtomicUsize、AtomicI8、AtomicU16...

内部可变性,允许通过共享引用进行修改(例如 &AtomicUsize)

相同的接口:加载与存储(load/store)、获取并修改(fetch-modify)、比较并交换(compare-exchange)

Load & Store 加载 & 存储

  • load:以原子方式加载原子变量中存储的值。pub fn load(&self, order: Ordering) -> usize
  • store:以原子方式将新值存入变量。pub fn store(&self, val: usize, order: Ordering)

Atomic Memory Ordering 原子内存排序

#[non_exhaustive]
pub enum Ordering {
  Relaxed,
  Release,
  Acquire,
  AcqRel,
  SeqCst,
}
  • 内存排序用于指定原子操作如何同步内存
  • Relaxed -- 最弱保证。含义:只保证这个操作是原子的,没有对排序的约束。适合场景:只关心“计数正确”,而不关心不同线程之间的操作顺序(例如:一个全局统计计数器,线程只需要把值加一)

Fetch Modify 获取 & 修改

fetch_add
fetch_and
fetch_max
fetch_min
fetch_nand
fetch_or
fetch_sub
fetch_update
fetch_xor
  • fetch_modify:修改原子变量、同时获取(Fetch)其原始值、整个过程是单一的原子操作

示例一

use std::{
    sync::atomic::{AtomicUsize, Ordering},
    thread,
    time::Duration,
};

fn main() {
    let done = AtomicUsize::new(0);

    thread::scope(|s| {
        for t in 0..10 {
            s.spawn(|| {
                for i in 0..100 {
                    thread::sleep(Duration::from_millis(20));
                    let current = done.load(Ordering::Relaxed);
                    done.store(current + 1, Ordering::Relaxed);
                }
            });
        }

        loop {
            let n = done.load(Ordering::Relaxed);
            if n == 1000 {
                break;
            }
            println!("Progress: {n}/1000 done!");
            thread::sleep(Duration::from_secs(1));
        }
    });

    println!("All done!");
}

这段 Rust 代码演示了如何在多线程环境下,使用原子类型 AtomicUsize 安全地追踪任务进度。

代码解释

1. 原子类型 AtomicUsize

代码首先创建了一个名为 doneAtomicUsize 类型变量。AtomicUsize 是一种特殊的整数类型,它提供了原子操作,这意味着即使在多个线程同时对其进行读写时,也能保证操作的完整性,不会出现数据竞争。这是实现多线程安全计数的关键。

2. 创建并运行线程

thread::scope 块中,程序启动了10个独立的线程。每个线程都执行一个任务:

  • 线程会循环100次。
  • 在每次循环中,线程会暂停20毫秒,模拟一个耗时操作。
  • 随后,它会执行一个原子操作:首先使用 load(Ordering::Relaxed) 读取 done 的当前值,然后将该值加1,最后使用 store(current + 1, Ordering::Relaxed) 将新值写回 done

3. 主线程监控进度

与此同时,主线程进入一个 loop 循环,它的作用是监控任务进度:

  • 主线程每隔1秒,会使用 done.load(Ordering::Relaxed) 操作,非阻塞地读取 done 的当前值。
  • 它将读取到的值打印出来,显示当前已完成的任务数量,形成一个进度条效果。
  • done 的值达到1000(10个线程 * 100次循环)时,主线程跳出循环并结束程序。

4. 内存排序 Ordering::Relaxed

代码中使用的 Ordering::Relaxed 是最弱的内存排序模型。它只保证原子操作本身是不可分割的,但不保证不同线程之间操作的顺序。在这个例子中,我们只需要保证每个线程对 done 的加1操作是正确的,而不需要关心这些加1操作在时间上的具体顺序,因此 Relaxed 就可以了。这使得编译器可以进行更多的优化,从而获得更好的性能。

总而言之,这段代码通过 AtomicUsize 成功地实现了多个线程并行执行任务,而主线程则可以安全、实时地监控所有任务的总体完成进度。

运行

➜ cargo run  
warning: unused variable: `t`
  --> src/main.rs:11:13
   |
11 |         for t in 0..10 {
   |             ^ help: if this is intentional, prefix it with an underscore: `_t`
   |
   = note: `#[warn(unused_variables)]` on by default

warning: unused variable: `i`
  --> src/main.rs:13:21
   |
13 |                 for i in 0..100 {
   |                     ^ help: if this is intentional, prefix it with an underscore: `_i`

warning: `atomic_demo` (bin "atomic_demo") generated 2 warnings
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/atomic_demo`
Progress: 0/1000 done!
Progress: 418/1000 done!
Progress: 843/1000 done!
Progress: 995/1000 done!
Progress: 995/1000 done!
Progress: 995/1000 done!
Progress: 995/1000 done!
Progress: 995/1000 done!
Progress: 995/1000 done!
Progress: 995/1000 done!
Progress: 995/1000 done!
Progress: 995/1000 done!
Progress: 995/1000 done!
Progress: 995/1000 done!
Progress: 995/1000 done!
^C

我们可以看到运行之后它跑到 995 就不跑了,这是因为在这段代码thread::sleep(Duration::from_millis(20)); let current = done.load(Ordering::Relaxed); done.store(current + 1, Ordering::Relaxed);中,在每个线程里面每一个任务它这个整体的操作不是原子性的。它把这个读和写分成了两步。所以就有可能出现同时读取,但一个先更新一个后更新把原来的覆盖了。这种情况应该发生了5次,所以它的结果是 995。

现在我们只需要把这两步变成一步单一的原子操作,就应该不会出现问题 了。

示例二

use std::{
    sync::atomic::{AtomicUsize, Ordering},
    thread,
    time::Duration,
};

fn main() {
    let done = AtomicUsize::new(0);

    thread::scope(|s| {
        for _ in 0..10 {
            s.spawn(|| {
                for _ in 0..100 {
                    thread::sleep(Duration::from_millis(20));
                    // let current = done.load(Ordering::Relaxed);
                    // done.store(current + 1, Ordering::Relaxed);

                    done.fetch_add(1, Ordering::Relaxed);
                }
            });
        }

        loop {
            let n = done.load(Ordering::Relaxed);
            if n == 1000 {
                break;
            }
            println!("Progress: {n}/1000 done!");
            thread::sleep(Duration::from_secs(1));
        }
    });

    println!("All done!");
}

这段 Rust 代码演示了如何使用原子类型(Atomic types)在多线程环境中安全地追踪任务进度。程序创建了一个名为 doneAtomicUsize 变量来安全地共享一个计数器,因为原子类型可以确保即使在多个线程同时访问时也不会发生数据竞争。它在 thread::scope 中启动了 10 个线程,每个线程都模拟执行 100 个耗时任务,并通过调用 done.fetch_add(1, Ordering::Relaxed) 原子地增加计数器。主线程则进入一个循环,每秒钟非阻塞地读取 done 的值并打印进度,直到总数达到 1000 时,所有线程的任务都已完成,主线程退出循环并结束程序。这种模式在多任务并行处理中非常常见,确保了任务计数的准确性。

运行

➜ cargo run
   Compiling atomic_demo v0.1.0 (/Users/qiaopengjun/Code/Rust/RustJourney/atomic_demo)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.55s
     Running `target/debug/atomic_demo`
Progress: 0/1000 done!
Progress: 420/1000 done!
Progress: 848/1000 done!
All done!

整个过程是单一的原子操作。

Compare Exchange 比较和交换

pub fn compare_exchange(
  &self,
  current: usize,
  new: usize,
  success: Ordering,
  failure: Ordering,
) -> Result<usize, usize>
  • compare_exchange:会检查原子值是否等于给定的值(current 参数),如果相等,就用新值(new 参数)替换它,否则不做任何修改,整个过程是单一原子操作。它会返回之前的值,并告诉我们是否替换成功。

为什么说 compare_exchange 很强大?

  • 你可以用它来实现几乎所有的其它原子操作,例如 fetch_add、fetch_sub 等。
  • 只需在一个循环中不断尝试,直到成功为止。这种模式叫做 CAS loop (Compare-And-Swap 循环)

示例三

use std::{
    sync::atomic::{AtomicUsize, Ordering},
    thread,
};

fn main() {
    let counter = AtomicUsize::new(0);

    thread::scope(|s| {
        for _ in 0..1000 {
            s.spawn(|| {
                incr(&counter);
            });
        }
    });

    println!("Counted: {}", { counter.load(Ordering::Relaxed) });
}

fn incr(counter: &AtomicUsize) {
    let mut current = counter.load(Ordering::Relaxed);
    loop {
        let new = current + 1;
        match counter.compare_exchange(current, new, Ordering::Relaxed, Ordering::Relaxed) {
            Ok(_) => return,
            Err(v) => {
                println!("value changed {current} -> {v}");
                current = v;
            }
        }
    }
}

这段 Rust 代码展示了如何利用原子类型(Atomic types)\在多线程环境下安全地对一个计数器进行递增操作。它创建了一个名为 counterAtomicUsize 变量作为共享计数器,然后通过 thread::scope 启动了 1000 个线程。每个线程都调用 incr 函数,该函数使用一个**循环compare_exchange 方法来尝试递增计数器。compare_exchange 会比较计数器的当前值(current)是否与它期望的值相同,如果相同就将其更新为新值(new),这个过程是原子性的。如果另一个线程抢先更新了计数器,compare_exchange 就会失败并返回新的值(Err(v)),incr 函数则会打印出冲突并用新值重新尝试,直到成功递增为止,这种模式被称为自旋锁**。最后,主线程会加载并打印出计数器的最终值,该值将是精确的 1000。

运行

➜ cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/atomic_demo`
Counted: 1000

RustJourney/atomic_demo on  main [?] is 📦 0.1.0 via 🦀 1.89.0 
➜ cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/atomic_demo`
value changed 1 -> 2
value changed 2 -> 3
Counted: 1000

A -> B -> A 问题

读取出来的值中间可能变了, 变成其它的值,然后又变回来了。

  • 如果原子值从A 变成 B,又变回A,在你调用 compare_exchange 之前,你是无法察觉这个变化的。
  • 虽然值看起来没变,但它确实经历了变化
  • 这在某些算法中可能导致严重问题,例如涉及原子指针的复杂算法

compare_exchange_weak 更轻量但可能失败

  • 即使值匹配,weak 版本也可能返回失败(Err),即所谓的“伪失败(spurious failure)”
  • 这个方法在某些平台上会更高效
  • 如果失败的代价不大(例如简单的重试循环),推荐优先使用 weak 版本

总结

通过本文的学习,我们不仅掌握了 Rust 原子类型的基本概念,更重要的是,通过实践深入理解了原子操作在多线程编程中的关键作用。我们亲眼见证了简单的 load/store 组合可能引发的数据竞争问题,并通过使用 fetch_add 或更强大的 compare_exchange 方法,成功解决了这些并发难题。原子类型是 Rust 安全并发的基石,掌握了它们,你就能在多线程的世界中自信地编写出高性能、无数据竞争的代码。

参考

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论