Rust Atomics and Locks内存序Memory Ordering详解

目录
  • Rust内存序
  • 重排序和优化
  • happens-before
  • Relexed Ordering
  • Release 和 Acquire Ordering
  • SeqCst Ordering

Rust内存序

Memory Ordering规定了多线程环境下对共享内存进行操作时的可见性和顺序性,防止了不正确的重排序(Reordering)。

重排序和优化

重排序是指编译器或CPU在不改变程序语义的前提下,改变指令的执行顺序。在单线程环境下,重排序可能会带来性能提升,但在多线程环境下,重排序可能会破坏程序的正确性,导致数据竞争、死锁等问题。

Rust提供了多种内存序,包括Acquire、Release、AcqRel、SeqCst等。这些内存序规定了在不同情况下,线程之间进行共享内存的读写时应该保持的顺序和可见性。

除了内存序之外,编译器还可以进行优化,例如常数折叠、函数内联等。这些优化可能会导致指令重排,从而影响多线程程序的正确性。为了避免这种情况,Rust提供了关键字volatilecompiler_fence来禁止编译器进行优化,保证程序的正确性。

总的来说,Rust的内存序机制和优化控制机制可以帮助程序员在多线程环境下编写高效且正确的程序。 下面是一个简单的 Rust 代码示例,它演示了 Rust 中的代码重新排序和优化如何影响程序行为:

use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
    let shared_counter = Arc::new(Mutex::new(0));
    let mut threads = vec![];
    for i in 0..10 {
        let counter = shared_counter.clone();
        let t = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += i;
        });
        threads.push(t);
    }
    for t in threads {
        t.join().unwrap();
    }
    let final_counter = shared_counter.lock().unwrap();
    println!("Final value: {}", *final_counter);
}

在这个示例中,我们创建了一个共享计数器 shared_counter,它被多个线程并发地访问和修改。为了保证线程安全,我们使用了一个 Mutex 来对计数器进行互斥访问。

在主线程中,我们创建了 10 个子线程,并让它们分别增加计数器的值。然后我们等待所有线程都执行完毕后,打印出最终的计数器值。

在这个示例中,由于 Rust 的内存序保证,所有对共享变量的访问和修改都按照程序中的顺序进行。也就是说,每个线程增加计数器的值的操作不会重排到其他线程之前或之后的位置。

不过,如果我们在代码中加入一些优化指令,就可能会破坏这种顺序。比如,下面这段代码就使用了 fence 指令来保证所有线程对共享变量的修改都在主线程中得到了同步:

use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
    let shared_counter = Arc::new(Mutex::new(0));
    let mut threads = vec![];
    for i in 0..10 {
        let counter = shared_counter.clone();
        let t = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += i;
            std::sync::atomic::fence(std::sync::atomic::Ordering::SeqCst);
        });
        threads.push(t);
    }
    for t in threads {
        t.join().unwrap();
    }
    let final_counter = shared_counter.lock().unwrap();
    println!("Final value: {}", *final_counter);
}

在这个示例中,我们在每个子线程结束时都加入了一个 fence 指令,来确保所有线程对共享变量的修改都在主线程中得到了同步。这样一来,虽然所有线程对计数器的修改仍然是并发进行的,但它们对计数器的修改操作的顺序可能会被重新排序,从而导致最终的计数器值与期望值不同。 但是,这可能是由于编译器的优化策略和硬件平台的差异所导致的。在某些情况下,编译器可能会选择不进行代码重排或重新优化,因为这可能会影响程序的正确性。但是,在其他情况下,编译器可能会根据其优化策略和目标平台的特性来对代码进行重排和重新优化,这可能会导致程序的行为发生变化。

happens-before

Rust内存模型中的“happens-before”原则指的是,如果一个操作A happens before 另一个操作B,那么A在时间上先于B执行,而且A对内存的影响对于B是可见的。这个原则被用来解决多线程环境下的数据竞争问题,确保程序的执行顺序是有序的,避免出现未定义的行为。

具体来说,Rust内存模型中的happens-before原则包括以下几个方面:

  • 内存同步操作:Rust的内存同步操作(如acquire、release、acqrel、seq_cst)会创建一个happens-before的关系,保证该操作之前的所有内存访问对该操作之后的内存访问都是可见的。
  • 锁机制:Rust的锁机制(如Mutex、RwLock)也会创建happens-before的关系,保证锁内的操作是有序的,避免数据竞争问题。
  • 线程的启动和结束:Rust的线程启动函数(如thread::spawn)会创建happens-before的关系,保证线程启动之前的所有内存访问对于该线程中的所有操作都是可见的。线程结束时也会创建happens-before的关系,保证该线程中的所有操作对于其他线程都是可见的。
  • Atomics:Rust的原子类型(如AtomicBool、AtomicUsize)也会创建happens-before的关系,保证对于同一个原子变量的多次操作是有序的,避免数据竞争问题。

总之,Rust内存模型中的happens-before原则确保程序的执行顺序是有序的,避免出现未定义的行为,从而帮助开发者避免数据竞争问题。

Relexed Ordering

在 Rust 中,Relaxed Ordering 是一种较弱的内存顺序,它允许线程在不同于程序中写入顺序的顺序中读取或写入数据,但不会导致未定义的行为。

Relaxed Ordering 主要应用于不需要同步的操作,比如单线程的计数器、读取全局配置等场景。使用 Relaxed Ordering 可以避免不必要的内存屏障,提高程序的性能。

在 Rust 中,可以通过 std::sync::atomic::AtomicXXX 类型来使用 Relaxed Ordering。比如下面这个示例:

use std::sync::atomic::{AtomicUsize, Ordering};
fn main() {
    let counter = AtomicUsize::new(0);
    counter.fetch_add(1, Ordering::Relaxed);
    let value = counter.load(Ordering::Relaxed);
    println!("counter: {}", value);
}

在这个示例中,我们使用 AtomicUsize 类型创建了一个计数器 counter,然后使用 fetch_add 方法对计数器进行自增操作,并使用 load 方法读取当前计数器的值。在 fetch_addload 方法中,我们使用了 Ordering::Relaxed 参数,表示这是一个 Relaxed Ordering 的操作,不需要执行额外的内存屏障。

需要注意的是,使用 Relaxed Ordering 时需要保证程序中不存在数据竞争。如果存在数据竞争,就可能会导致内存重排和未定义的行为。因此,建议仅在确信不会出现数据竞争的情况下使用 Relaxed Ordering。

Release 和 Acquire Ordering

在 Rust 中,Release 和 Acquire Ordering 通常用于实现同步原语,例如 Mutex 和 Atomic 原子类型,以确保线程之间的正确同步。

Acquire Ordering 表示一个读取操作所需的同步操作。在读取操作之前,必须确保任何在之前的写入操作都已经完成,并且这些写入操作对其他线程可见。在 Acquire Ordering 中,读取操作前的任何写入操作都不能被重排序到读取操作之后。

Release Ordering 表示一个写入操作所需的同步操作。在写入操作之后,必须确保任何在之后的读取操作都能够看到这个写入操作的结果,并且这个写入操作对其他线程可见。在 Release Ordering 中,写入操作后的任何读取操作都不能被重排序到写入操作之前。

下面是一个简单的示例,说明了如何使用 Release 和 Acquire Ordering 来同步多个线程对共享状态的访问:

use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
static SHARED_STATE: AtomicUsize = AtomicUsize::new(0);
fn main() {
    let mut threads = Vec::new();
    for i in 0..5 {
        let thread = thread::spawn(move || {
            let mut local_state = i;
            // 等待其他线程初始化
            thread::sleep_ms(10);
            // 将本地状态更新到共享状态中
            SHARED_STATE.store(local_state, Ordering::Release);
            // 读取共享状态中的值
            let shared_state = SHARED_STATE.load(Ordering::Acquire);
            println!("Thread {}: Shared state = {}", i, shared_state);
        });
        threads.push(thread);
    }
    for thread in threads {
        thread.join().unwrap();
    }
}

在这个例子中,五个线程将本地状态更新到共享状态中,并读取其他线程更新的共享状态。为了确保正确的同步,我们使用 Release Ordering 来保证写入操作的同步,Acquire Ordering 来保证读取操作的同步。在每个线程中,我们使用 thread::sleep_ms(10) 使所有线程都能够开始执行,并等待其他线程完成初始化。在主线程中,我们使用 join 等待所有线程完成。运行此程序,输出可能类似于以下内容:

Thread 0: Shared state = 3
Thread 1: Shared state = 0
Thread 2: Shared state = 2
Thread 3: Shared state = 1
Thread 4: Shared state = 4

这表明每个线程都能够正确地读取其他线程更新的共享状态,并且写入操作在读取操作之前完成。

SeqCst Ordering

SeqCst是Rust内存模型中的一种内存顺序,它保证了所有的操作都按照顺序执行。SeqCst可以用于实现最严格的同步,因为它确保了所有线程都看到相同的执行顺序,因此被广泛用于实现同步原语。 如果无法确认使用哪种排序的话,可以直接使用SeqCst

使用SeqCst内存顺序时,读操作和写操作的执行顺序都是全局可见的,因此可以避免数据竞争和其他问题。但是SeqCst内存顺序会导致一些性能问题,因为它要求所有线程都同步执行,这可能会导致一些线程被阻塞。

以下是一个简单的示例,展示了如何在Rust中使用SeqCst内存顺序:

use std::sync::atomic::{AtomicBool, Ordering};
fn main() {
    let val = AtomicBool::new(false);
    val.store(true, Ordering::SeqCst);
    let result = val.load(Ordering::SeqCst);
    println!("Result: {}", result);
}

在这个示例中,我们创建了一个AtomicBool类型的变量val,并将其初始值设置为false。然后,我们使用store方法将其值设置为true,并使用load方法读取它的值。在这里,我们使用了SeqCst内存顺序,以确保所有线程都按顺序执行。在这个例子中,程序会输出Result: true,因为我们使用了SeqCst内存顺序,保证了所有线程都看到相同的执行顺序。

以上就是Rust Atomics and Locks内存序Memory Ordering详解的详细内容,更多关于Rust Atomics and Locks内存序的资料请关注我们其它相关文章!

(0)

相关推荐

  • Rust Atomics and Locks并发基础理解

    目录 Rust 中的线程 线程作用域 所有权共享 借用和数据竞争 内部可变 rust 中的线程安全 Send 和 Sync 线程阻塞和唤醒 Rust 中的线程 在 Rust 中,线程是轻量级的执行单元,可以并行执行多个任务.Rust 中的线程由标准库提供的 std::thread 模块支持,使用线程需要在程序中引入该模块.可以使用 std::thread::spawn() 函数创建一个新线程,该函数需要传递一个闭包作为线程的执行体.闭包中的代码将在新线程中执行,从而实现了并发执行.例如: use

  • Rust 所有权机制原理深入剖析

    目录 what's ownership? Scope (作用域) ownership transfer(所有权转移) move clone copy References and Borrowing(引用和借用) Mutable References(可变引用) Dangling References(悬垂引用) what's ownership? 常见的高级语言都有自己的 Garbage Collection(GC)机制来管理程序运行的内存,例如 Java.Go 等.而 Rust 引入了一种全

  • Rust Atomics and Locks 源码解读

    目录 正文 load 和 store 使用 AtomicBool实现通知线程停止的案例 正文 在 Rust 中,原子性操作是指在多线程并发环境下对共享数据进行操作时,保证操作的原子性,即不会出现数据竞争等问题.Rust 提供了原子类型和原子操作来支持多线程并发编程. Rust 的原子类型包括 AtomicBool.AtomicIsize.AtomicUsize.AtomicPtr 等.这些类型的实现都使用了底层的原子操作指令,保证了它们的读写操作是原子的,不会被其他线程中断. 在 Rust 中,

  • 向Rust学习Go考虑简单字符串插值特性示例解析

    目录 fmt.Printf 或 fmt.Sprintf 写拼装字符串业务 简单字符串插值 其他语言例子 Swift Kotlin C Rust 争论矛盾点 总结 fmt.Printf 或 fmt.Sprintf 写拼装字符串业务 在日常开发 Go 工程中,我们经常会用 fmt.Printf 或 fmt.Sprintf 去写类似的拼装字符串的业务. 如下代码: fmt.Printf("Hello Gopher %s, you are %d years old and you're favorite

  • 从迷你todo 命令行入门Rust示例详解

    目录 一个迷你 todo 应用 需要安装的依赖 文件目录组织 主文件 读取文件 状态处理工厂函数 Trait(特征) Create trait Get trait Delete trait Edit trait 导出 trait 为 struct 实现 trait Pending Done 导出 struct Process 输入处理 最后 一个迷你 todo 应用 该文章将使用 Rust 从零去做一个入门级别的 TODO 命令行应用 你将学会什么? 基本的命令行操作 文件读写和文件结构组织 我

  • Rust Atomics and Locks内存序Memory Ordering详解

    目录 Rust内存序 重排序和优化 happens-before Relexed Ordering Release 和 Acquire Ordering SeqCst Ordering Rust内存序 Memory Ordering规定了多线程环境下对共享内存进行操作时的可见性和顺序性,防止了不正确的重排序(Reordering). 重排序和优化 重排序是指编译器或CPU在不改变程序语义的前提下,改变指令的执行顺序.在单线程环境下,重排序可能会带来性能提升,但在多线程环境下,重排序可能会破坏程序

  • Java基础之内存泄漏与溢出详解

    一.浅析 内存泄露( memory leak):是指程序在申请内存后,无法释放已申请的内存空间,多次内存泄露堆积后果很严重,内存迟早会被占光.内存泄漏最终会造成内存溢出. 内存溢出(out of memory) :是指程序在申请内存时,没有足够的内存空间供其使用 JVM中有一下几种内存空间: 栈内存(Stack):每个线程私有的. 堆内存(Heap):所有线程公用的. 方法区(Method Area):有点像以前常说的"进程代码段",这里面存放了每个加载类的反射信息.类函数的代码.编译

  • C++ 操作系统内存分配算法的实现详解

    目录 一.实验目的 二.实验内容 三.实验要求 四.代码实现 五.测试样例 一.实验目的 通过本实验帮助学生理解在动态分区管理方式下应怎样实现主存空间的分配和回收. 二.实验内容 在动态分区管理方式下采用不同的分配算法实现主存分配和实现主存回收. 三.实验要求 (1)可变分区方式是按作业需要的主存空间大小来分割分区的.当要装入一个作业时,根据作业需要的主存量查看是否有足够的空闲空间,若有,则按需要量分割一个分区分配给该作业:若无,则作业不能装入.随着作业的装入.撤离.主存空间被分成许多个分区,有

  • C++中整形与浮点型如何在内存中的存储详解

    目录 1 数据类型 1.1 类型的基本归类 2 整形在内存中的存储 2.1 二进制的三种形式 2.2 大小端字的介绍 3 浮点数在内存中的存储 3.1 浮点数存储规则 1 数据类型 前面我们已经知道了基本的内置类型: 类型的意义: 1. 使用这个类型开辟内存空间的大小(大小决定了使用范围). 2. 如何看待内存空间的视角. 1.1 类型的基本归类 整形家族: char unsigned char signed char short unsigned short [int] signed shor

  • Performance 内存监控使用技巧详解

    目录 Performance 介绍 使⽤ 内存问题的具体体现 监控内存的⼏种⽅式 TimeLine Performance 介绍 为什么使⽤Performance呢?GC 的⽬的是为了实现内存空间的良性循环,⽽良性循环的基⽯是合理的使⽤内存空间. 由于 ECMAScript 并没有提供操作内存的 API,所以内存分配是否合理我们不可知.Performance 提供了多种⽅式,在程序运⾏时可以时时监控,确定内存分配是否合理. 使⽤ 具体步骤 打开浏览器输⼊⽬标⽹址 进⼊开发⼈员⼯具⾯板 开启录制功

  • 关于Java变量的声明、内存分配及初始化详解

    实例如下: class Person { String name; int age; void talk() { System.out.println("我是: "+name+", 今年: "+age+"岁"); } } public class TestJava2_1 { public static void main(String args[]) { Person p; if (p == null) { p = new Person(); }

  • Linux共享内存实现机制的详解

    Linux共享内存实现机制的详解 内存共享: 两个不同进程A.B共享内存的意思是,同一块物理内存被映射到进程A.B各自的进程地址空间.进程A可以即时看到进程B对共享内存中数据的更新,反之亦然.由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以. 效率: 采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝.对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据[1]: 一次从输入文件到

  • C/C++ 双链表之逆序的实例详解

    C/C++ 双链表之逆序的实例详解 一.结点结构 双向链表的数据结构定义如下: typedef struct node { ElemType data; struct node *prior struct node *next; }list; 其中,ElemType可以是任意数据类型如int.float或者char等,在算法中,规定其默认为int类型. 二.带头结点 本文描述的是双向链表逆序,链表逆序需要维护3个指针,分别指向前一个节点.当前节点和下一个节点,具体代码如下: list *reve

  • PHP内存溢出优化代码详解

    相信很多人做大批量数据导出和数据导入的时候,经常会遇到PHP内存溢出的问题,在解决了问题之后,总结了一些经验,整理成文章记录下. 优化点 1.优化SQL语句,避免慢查询,合理的建立索引,查询指定的字段,sql优化这块在此就不展开了. 2.查询的结果集为大对象时转数组处理,框架中一般有方法可以转,如Laravel中有toArray(),Yii2中有asArray(). 3.对于大数组进行数据切割处理,PHP函数有array_chunk().array_slice(). 4.对于大型的字符串和对象,

随机推荐