rust 原子 Package 器与原语

qoefvg9y  于 2023-02-04  发布在  其他
关注(0)|答案(1)|浏览(110)

我试图理解std::sync::atomic::Atomic*结构体和原语(如i32usizebool)在多线程范围内的一些区别。
第一个问题,另一个线程会看到另一个线程对非原子类型的更改吗?

fn main() {
    let mut counter = 0;

    std::thread::scope(|scope| {
        scope.spawn(|| counter += 1)
    });

    println!("{counter}");
}

我能确定在另一个线程将这个值写入计数器之后计数器将是1吗?或者线程可以缓存这个值?如果不能,它是否只能在原子类型下工作?

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

    std::thread::scope(|scope| {
        scope.spawn(|| counter.store(1, Ordering::Release))
    });

    println!("{}", counter.load(Ordering::Acquire)); // Ordering::Acquire to prevent from reordering previous instructions
}

第二个问题,Ordering类型是否会影响store中的值何时在其他线程中可见,或者即使应用了Ordering::Relaxed,它何时在store之后可见?例如,相同的代码,但具有Ordering::Relaxed且没有指令重新排序,是否会在计数器中显示1

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

    std::thread::scope(|scope| {
        scope.spawn(|| counter.store(1, Ordering::Relaxed))
    });

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

我理解原子和非原子写同一个变量之间的区别,我只对另一个线程是否会看到更改感兴趣,即使这些更改不一致。

tjrkku2a

tjrkku2a1#

第一个问题,另一个线程会看到另一个线程对非原子类型的更改吗?
是的。原子变量和非原子变量的区别在于,你可以使用共享引用&AtomicX来改变原子变量,而不仅仅是使用可变引用&mut X。这意味着它们可以在不同的线程中并行地改变。对于原语,编译器将拒绝这种尝试,例如:

fn main() {
    let mut counter = 0;

    std::thread::scope(|scope| {
        scope.spawn(|| counter += 1);
        scope.spawn(|| counter += 1);
    });

    println!("{counter}");
}

或者甚至是下面的例子,我们在主线程上使用变量,但是在派生线程被连接之前:

fn main() {
    let mut counter = 0;

    std::thread::scope(|scope| {
        scope.spawn(|| counter += 1);
        counter += 1;
    });

    println!("{counter}");
}

而对于原子,这将工作:

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

    std::thread::scope(|scope| {
        scope.spawn(|| counter.store(1, Ordering::Relaxed));
        scope.spawn(|| counter.store(1, Ordering::Relaxed));
    });

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

第二个问题,Ordering类型是否会影响存储中的值何时在其他线程中可见,还是在存储后立即可见,即使应用了Ordering::Relaxed?例如,相同的代码,但使用Ordering::Relaxed且没有指令重新排序,是否会在计数器中显示1?

    • 不,Ordering不会改变其他线程使用此变量 * 观察到的结果。**因此,您对ReleaseAcquire的使用是错误的。

另一方面,由于其他原因,这里的Relaxed就足够了。
无论使用什么顺序,都可以保证在代码中看到值1,因为std::thread::scope()在退出时隐式地联接所有派生的线程,并且联接线程在该线程中所做的一切与联接之后的代码之间形成了 * happens-before * 关系。您可以保证线程中所做的一切(包括存储到counter)都将在您加入线程后所做的一切(包括读取counter)之前发生。
例如,如果在以下代码中没有联接:

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

    std::thread::scope(|scope| {
        scope.spawn(|| counter.store(1, Ordering::Release));
        scope.spawn(|| println!("{}", counter.load(Ordering::Acquire)));
    });
}

那么不管ReleaseAcquire的顺序如何,你都不能保证读到更新后的值,可能会发生这种情况,也可能会发生你读到旧值的情况。
排序对于创建具有不同变量和代码的happens-before关系非常有用,但这是一个复杂的主题,我推荐阅读this book(由Rust libs团队成员编写)。

相关问题