< 返回版块

golovo 发表于 2023-11-23 12:25

如果数据不是被多个线程的指针指向,只是在单线程中使用,比如Rc,为什么还需要上锁才能写?什么情况会产生数据竞争呢? 同样的,&mut T生命周期内,如果有&T也是不行的,可是如果数据只存在于单线程又怎么会有数据竞争?

评论区

写评论
北海 2023-12-21 11:27

你质疑的是“可变不共享,共享不可变”这个基础法则,Rc/Arc属于共享的范畴,所以原则上要不可变,除非unsafe。要想可变,就得靠锁获得唯一能改变它的排它权利,保证安全地修改。这个法则在单线程内也是要遵守的。

单线程内虽不存在数据竞争,但是会发生逻辑错误,两个地方都在修改,而你却不知情,比如下面这个例子,就很好:

fn compute(input: &u32, output: &mut u32) {
    if *input > 10 {
        *output = 1;
    }
    if *input > 5 {
        *output *= 2;
    }
}

fn main() {
    let i = 20;
    let mut o = 5;
    compute(&i, &mut o);  // o = 2

    // 假如违反“可变不共享,共享不可变”,你将能写下bug代码,并且可能毫无察觉
    let mut u = 20;
    compute(&u, &mut u);  // u会是几,符合程序原本的预期不?所以,单线程也要借用检查
}

上面的例子还算比较简单,要是程序逻辑上离得远的两个文件里,东边一个“可变借用”,西边一个“不可变借用”,它们借用的还是同一块内存,那西边的“不可变借用”就惨了,而且该问题很难“瞪眼法”发现,到时候就苦了!

所以,还是得感谢Rust的严格,“可变不共享,共享不可变”就算在单线程下也有意义。

TinusgragLin 2023-12-16 00:28

Rc只是引用计数啦,如果lz说锁的话是RwLock, Mutex这些哦。这些锁是为了保证某种访问需求的,比如说某一段程序里我想要某个值一直不变,要不然变来变去不好对这个值做一些很基本的推断,比如我刚设置它为42,总不能下一行它就是另一个值吧!那我还设置它干嘛!如果lz有学过计算机组成原理/操作系统的话就会记得现在的计算机要同时运行很多程序一个方法就是这个进程运行一阵子,再另一个进程运行一阵子这样,所以刚才说的“上一行设为42,下一行就不是”这种情况就会很有可能的啦!那这样程序员就会直接骂娘啦!所以会有锁啊什么的。

TinusgragLin 2023-12-16 00:07

我记得一个原因是,当一个 &'some_lifetime T 存在时,编译器可以在 some_lifetime 期间缓存解引用的结果并安全地使用这个缓存,而不用因为担心这期间引用指向的值被改变而每次都要解引用(因为它存在的时候不会有另一个 &mut T 和它共存嘛):

fn f_a(r: &mut i32) {}
let mut n = Box::new(42);
n += 12; // 此时我们知道 n = 54,这个值一般已经被cpu算好存在某个寄存器上了。
let r = &n;
// 如果此时允许另一个 &mut n 存在的话,f_a 可能会改变 n 的值!
f_a(&mut n);
println!("{}", *r);-------- n 不一定是 54 喽,得老老实实再来一次解引用!

之前还找到一篇有关的文章,我一直存在收藏夹里没有细看,lz可以看看!

Aya0wind 2023-11-29 10:04

什么时候Rc只有上锁才能写了?需要加一个RefCell才能写差不多,但是RefCell不是锁,它甚至不是Sync的,Rc只能保证所有权被多方正确的共享,但是不能保证被多方正确借用。 或许你把线程安全和和借用规则混淆了,这两个不是一个东西,如果你的程序只在一个线程里运行,是不会有线程安全问题的,但是会存在违反借用规则导致的安全问题,解决线程安全问题才需要用到锁或原子操作,解决借用规则导致的问题则需要正确的处理借用关系,或者在编译器无法完成编译期借用检查的情况下使用运行期借用检查(例如上面的RefCell),而不是锁。典型的违反借用规则造成的问题,例如C++中的迭代器失效问题,这在单线程程序中也是会出现的,而在safe rust里就会通过借用检查避免这写问题。

liusen-adalab 2023-11-28 10:40

这是为了说明一个任务在执行时随时可能交出控制权,再次返回时状态各预期不一致

--
👇
hangj: 这个例子本身是有问题的

random_sleep().await;
let counter = self.counter();
let old = *counter;
tokio::task::yield_now().await;
*counter = old + 1;

--
👇
liusen-adalab: 在同一个线程中也可能出现数据竞争的

hangj 2023-11-27 15:11

这个例子本身是有问题的

random_sleep().await;
let counter = self.counter();
let old = *counter;
tokio::task::yield_now().await;
*counter = old + 1;

--
👇
liusen-adalab: 在同一个线程中也可能出现数据竞争的

liusen-adalab 2023-11-27 12:29

在同一个线程中也可能出现数据竞争的

use std::{cell::UnsafeCell, hint::black_box, rc::Rc, time::Duration};
use tokio::task;

#[tokio::main]
async fn main() {
    let counter = MutableRc {
        count: Rc::new(UnsafeCell::new(0)),
    };
    let counter_clone = counter.clone();

    let local = task::LocalSet::new();
    for _ in 0..100 {
        {
            let mut counter = counter.clone();
            local.spawn_local(async move {
                counter.add_one().await;
            });
        }
        {
            let mut counter = counter.clone();
            local.spawn_local(async move {
                counter.sub_one().await;
            });
        }
    }

    local.await;

    println!("count: {}", unsafe { &*counter_clone.count.get() });
}

/// 一个可变版本的 Rc
///
/// 如果你有一个 &mut MutableRc,那么你可以获取到一个 &mut i32
/// 这样在同一个线程中可能同时获得多个 &mut i32,而对数据的操作可能交替进行,会导致数据不一致
#[derive(Clone)]
struct MutableRc {
    count: Rc<UnsafeCell<i32>>,
}

impl MutableRc {
    async fn add_one(&mut self) {
        black_box({
            random_sleep().await;
            let counter = self.counter();
            let old = *counter;
            tokio::task::yield_now().await;
            *counter = old + 1;
        });
    }

    async fn sub_one(&mut self) {
        black_box({
            random_sleep().await;
            let counter = self.counter();
            let old = *counter;
            tokio::task::yield_now().await;
            *counter = old - 1;
        });
    }

    fn counter(&mut self) -> &mut i32 {
        unsafe { &mut *self.count.get() }
    }
}

async fn random_sleep() {
    let s = (timestamp() % 5 + 1) as u64;
    let sleep = Duration::from_millis(s);
    tokio::time::sleep(sleep).await;
}

fn timestamp() -> u128 {
    let now = std::time::SystemTime::now();
    now.duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_nanos()
}

wukong 2023-11-24 20:33

能否举个例子?

如果没有涉及到并发,我理解应该是不用的

gorust21 2023-11-24 07:09

数据是有变化的,所以需要锁

ribs 2023-11-23 16:33

其实我也不太理解,可能是预期的问题,而不是数据竞争。 一个不可变引用,我们预期他就是永远不会变,如果中途被改变了,那结果就会跟预期不符。 其次,rc内不用加锁,加个refcell内部可变就行

Pikachu 2023-11-23 12:52

rust playground

pub fn main() {
   let mut xs = vec![1, 2, 3];

   let xs_mut = &mut xs;
   let x_ref = &xs[0];

   xs_mut.clear(); 

   dbg!(x_ref); // dangling pointer
}
1 共 11 条评论, 1 页