RustAtomicsandLocks并发基础理解

2023-02-27 11:02:37 理解 并发 基础

Rust 中的线程

在 Rust 中,线程是轻量级的执行单元,可以并行执行多个任务。Rust 中的线程由标准库提供的 std::thread 模块支持,使用线程需要在程序中引入该模块。可以使用 std::thread::spawn() 函数创建一个新线程,该函数需要传递一个闭包作为线程的执行体。闭包中的代码将在新线程中执行,从而实现了并发执行。例如:

use std::thread;
fn main() {
    // 创建一个新线程
    let handle = thread::spawn(|| {
        // 在新线程中执行的代码
        println!("Hello from a new thread!");
    });
    // 等待新线程执行完毕
    handle.join().unwrap();
    // 主线程中的代码
    println!("Hello from the main thread!");
}

上面的代码创建了一个新线程,并在新线程中打印了一条消息。在主线程中,调用了 handle.join() 方法等待新线程执行完毕。在新线程执行完毕后,程序会继续执行主线程中的代码。

需要注意的是,Rust 的线程是“无法共享堆栈”的。也就是说,每个线程都有自己的堆栈,不能直接共享数据。如果需要在线程之间共享数据,可以使用 Rust 的线程安全原语,例如 Mutex、Arc 等。

线程作用域

在 Rust 中,std::thread::scope 是一个函数,它允许在当前作用域中创建一个新的线程作用域。在这个作用域中创建的线程将会在作用域结束时自动结束,从而避免了手动调用 join() 方法的麻烦。

std::thread::scope 函数需要传递一个闭包,该闭包中定义了线程的执行体。与 std::thread::spawn 不同的是,该闭包中可以访问其父作用域中的变量。

下面是一个简单的例子,展示了如何使用 std::thread::scope

use std::thread;
fn main() {
    let mut vec = vec![1, 2, 3];
    thread::scope(|s| {
        s.spawn(|_| {
            vec.push(4);
        });
    });
    println!("{:?}", vec);
}

在这个例子中,我们使用 thread::scope 创建了一个新的线程作用域。在这个作用域中,我们创建了一个新的线程,并在其中向 vec 向量中添加了一个新元素。由于线程作用域在闭包执行完毕时自动结束,因此在 println! 语句中打印出的 vec 向量中并没有包含新添加的元素。

需要注意的是,在使用 thread::scope 创建线程时,闭包的参数类型必须是 &mut std::thread::Scope,而不是 &mut 闭包中所访问的变量的类型。这是因为 thread::scope 函数需要传递一个可变引用,以便在作用域结束时正确释放线程的资源。

所有权共享

在 Rust 中,所有权共享是一种允许多个变量同时拥有同一值的所有权的方式。这种方式被称为“所有权共享”,因为它允许多个变量共享对同一值的所有权。这是 Rust 的一项重要特性,可以帮助避免内存泄漏和数据竞争等问题。

在 Rust 中,有三种方式可以实现所有权共享:静态变量(Statics)、内存泄漏(Leaking)和引用计数(Reference Counting)。

  • 静态变量(Statics)

静态变量是指在程序运行期间一直存在的变量。在 Rust 中,可以使用 static 关键字来定义静态变量。静态变量在程序运行期间只会被初始化一次,且只有一个实例,所以多个变量可以共享对同一静态变量的所有权。

以下是一个示例:

static mut COUNTER: i32 = 0;
fn main() {
    unsafe {
        COUNTER += 1;
        println!("Counter: {}", COUNTER);
    }
}

在这个例子中,我们定义了一个名为 COUNTER 的静态变量,并使用 static mut 来表示它是一个可变的静态变量。然后,在 main 函数中,我们通过 unsafe 代码块来访问 COUNTER 变量,并将其加一。需要注意的是,在 Rust 中,访问静态变量是不安全的操作,所以必须使用 unsafe 代码块来进行访问。

  • 内存泄漏(Leaking)

内存泄漏是指在程序运行期间分配的内存没有被释放的情况。在 Rust 中,可以使用 Box::leak 方法来实现内存泄漏。Box::leak 方法会返回一个指向堆上分配的值的指针,但不会释放这个值的内存。这样,多个变量就可以共享对同一堆分配的值的所有权。

以下是一个示例:

use std::mem::forget;
fn main() {
    let value = Box::new("Hello, world!".to_string());
    let pointer = Box::leak(value);
    let reference1 = &*pointer;
    let reference2 = &*pointer;
    forget(pointer);
    println!("{}", reference1);
    println!("{}", reference2);
}

在这个例子中,我们使用 Box::new 创建一个新的堆分配的值,并将其赋值给 value 变量。然后,我们使用 Box::leak 方法来讲 value 的所有权泄漏到堆上,并返回一个指向堆上分配的值的指针。接着,我们使用 &* 来将指针解引用,并将其赋值给 reference1reference2 变量。最后,我们使用 std::mem::forget 函数来避免释放

  • 引用计数

引用计数是一种在 Rust 中实现所有权共享的方式,它允许多个变量共享对同一值的所有权。在 Rust 中,引用计数使用 Rc<T>(“引用计数”)类型来实现。Rc<T> 类型允许多个变量共享对同一值的所有权,但是不能在运行时进行修改,因为 Rc<T> 类型不支持内部可变性。

以下是一个示例:

use std::rc::Rc;
fn main() {
    let value = Rc::new("Hello, world!".to_string());
    let reference1 = value.clone();
    let reference2 = value.clone();
    println!("{}", reference1);
    println!("{}", reference2);
}

在这个例子中,我们使用 Rc::new 创建一个新的 Rc<String> 类型的值,并将其赋值给 value 变量。然后,我们使用 value.clone() 方法来创建 value 的两个引用,并将它们分别赋值给 reference1reference2 变量。最后,我们打印 reference1reference2 变量,以显示它们都引用了同一个值。

需要注意的是,Rc<T> 类型只能用于单线程环境,因为它不是线程安全的。如果需要在多线程环境下实现引用计数,可以使用 Arc<T>(“原子引用计数”)类型。Arc<T> 类型是 Rc<T> 的线程安全版本,它使用原子操作来实现引用计数。

借用和数据竞争

在 Rust 中,借用是一种通过引用来访问值而不获取其所有权的方式。借用是 Rust 中非常重要的概念,因为它可以帮助避免数据竞争的问题。

数据竞争指的是多个线程同时访问同一个变量,且至少有一个线程正在写入该变量。如果没有采取适当的同步措施,数据竞争会导致未定义的行为,例如程序崩溃或产生意外的结果。

在 Rust 中,编译器使用所有权和借用规则来防止数据竞争。具体来说,编译器会检查每个引用的生命周期,以确保在引用仍然有效的情况下进行访问。如果编译器发现了潜在的数据竞争问题,它会在编译时发出错误。

以下是一个简单的例子,说明如何使用借用来避免数据竞争问题:

use std::thread;
fn main() {
    let mut data = vec![1, 2, 3];
    let handle1 = thread::spawn(move || {
        let reference = &data;
        println!("Thread 1: {:?}", reference);
    });
    let handle2 = thread::spawn(move || {
        let reference = &data;
        println!("Thread 2: {:?}", reference);
    });
    handle1.join().unwrap();
    handle2.join().unwrap();
}

在这个例子中,我们创建了一个可变的 Vec<i32> 类型的值,并将其赋值给 data 变量。然后,我们在两个线程中使用 thread::spawn 方法,每个线程都获取对 data 的共享引用,并打印该引用。由于我们使用了共享引用,所以不会发生数据竞争问题。

需要注意的是,如果我们尝试将 data 的可变引用传递给两个线程中的一个或多个线程,编译器将会在编译时发出错误,因为这可能会导致数据竞争。在这种情况下,我们可以使用 Mutex<T>RwLock<T>Cell<T> 等同步原语来避免数据竞争。

内部可变

在 Rust 中,内部可变性是指在拥有不可变引用的同时,可以修改被引用的值。Rust 提供了一些内部可变性的实现方式,包括 Cell<T>RefCell<T> 类型。

Cell<T> 类型提供了一种在不可变引用的情况下,修改其所持有的值的方法。它通过在不可变引用中封装值,并使用 getset 方法来实现内部可变性。以下是一个示例:

use std::cell::Cell;
fn main() {
    let number = Cell::new(42);
    let reference = &number;
    let value = reference.get();
    number.set(value + 1);
    println!("The new value is: {}", reference.get());
}

在这个例子中,我们创建了一个 Cell<i32> 类型的值,并将其赋值给 number 变量。然后,我们获取了一个 &Cell<i32> 类型的不可变引用,并通过 get 方法获取了 number 所持有的值。接着,我们通过 set 方法来修改 number 所持有的值。最后,我们打印了 number 所持有的新值。

RefCell<T> 类型提供了一种更灵活的内部可变性实现方式。它通过在可变和不可变引用中封装值,并使用 borrowborrow_mut 方法来实现内部可变性。以下是一个示例:

use std::cell::RefCell;
fn main() {
    let number = RefCell::new(42);
    let reference1 = &number.borrow();
    let reference2 = &number.borrow();
    let mut reference3 = number.borrow_mut();
    *reference3 += 1;
    println!("The new value is: {:?}", number.borrow());
}

在这个例子中,我们创建了一个 RefCell<i32> 类型的值,并将其赋值给 number 变量。然后,我们获取了两个不可变引用,并通过 borrow_mut 方法获取了一个可变引用。接着,我们通过可变引用来修改 number 所持有的值。最后,我们打印了 number 所持有的新值。

需要注意的是,Cell<T>RefCell<T> 类型都不是线程安全的。如果需要在多线程环境下使用内部可变性,可以使用 Mutex<T>RwLock<T> 等同步原语。 在 Rust 中,为了保证多线程并发访问共享数据的安全性,可以使用同步原语,例如 Mutex 和 RwLock。

Mutex 是一种互斥,它允许只有一个线程访问被保护的共享数据。在 Rust 中,可以通过标准库中的 std::sync::Mutex 类型来实现 Mutex。以下是一个示例:

use std::sync::Mutex;
fn main() {
    let data = Mutex::new(0);
    let mut handles = vec![];
    for _ in 0..10 {
        let handle = std::thread::spawn(move || {
            let mut data = data.lock().unwrap();
            *data += 1;
        });
        handles.push(handle);
    }
    for handle in handles {
        handle.join().unwrap();
    }
    println!("Result: {}", *data.lock().unwrap());
}

在这个例子中,我们创建了一个 Mutex<i32> 类型的值,并将其赋值给 data 变量。然后,我们创建了 10 个线程,并在每个线程中获取 data 的可变引用,并通过加 1 的方式修改其所持有的值。最后,我们等待所有线程执行完毕,并打印 data 所持有的值。

RwLock 是一种读写锁,它允许多个线程同时读取共享数据,但只允许一个线程写入共享数据。在 Rust 中,可以通过标准库中的 std::sync::RwLock 类型来实现 RwLock。以下是一个示例:

use std::sync::RwLock;
fn main() {
    let data = RwLock::new(0);
    let mut handles = vec![];
    for _ in 0..10 {
        let handle = std::thread::spawn(move || {
            let data = data.read().unwrap();
            println!("Thread {}: read data {}", std::thread::current().id(), *data);
        });
        handles.push(handle);
    }
    let handle = std::thread::spawn(move || {
        let mut data = data.write().unwrap();
        *data += 1;
        println!("Thread {}: write data {}", std::thread::current().id(), *data);
    });
    handles.push(handle);
    for handle in handles {
        handle.join().unwrap();
    }
}

在这个例子中,我们创建了一个 RwLock<i32> 类型的值,并将其赋值给 data 变量。然后,我们创建了 10 个线程,并在每个线程中获取 data 的不可变引用,并打印其所持有的值。接着,我们创建了一个新的线程,并获取 data 的可变引用,并通过加 1 的方式修改其所持有的值。最后,我们等待所有线程执行完毕。

需要注意的是,在使用 Mutex 和 RwLock 时,需要使用 unwrap() 方法来处理锁的获取失败的情况。如果在获取锁时发生了死锁,程序会阻塞在该位置。因此,在使用锁时需要注意避免死锁的情况。 在 Rust 中,为了保证线程安全和内存安全,访问可变的共享数据通常需要使用同步原语,例如 Mutex 和 RwLock,或者通过引用计数等方式。然而,有时候我们需要在 Rust 中使用一些类似 C 的指针操作,这时就需要使用 unsafe 关键字来打破 Rust 的内存安全限制。

Rust 标准库中提供了一个 Cell<T> 类型,它允许在不使用 Mutex 或 RwLock 的情况下,在多个线程之间共享可变数据。但是,由于 Cell<T> 不是线程安全的,因此在多线程环境下使用它会导致数据竞争和内存安全问题。

为了解决这个问题,Rust 提供了 UnsafeCell<T> 类型,它可以安全地包含不可变类型 T 或可变类型 &mut T,并且可以用于实现线程安全的数据结构。使用 UnsafeCell<T> 需要使用 unsafe 关键字,并遵循 Rust 的内存安全规则。

以下是一个示例,演示如何使用 UnsafeCell<T> 来实现一个线程安全的计数器:

rustCopy code
use std::sync::atomic::{AtomicUsize, Ordering};
use std::cell::UnsafeCell;
use std::thread;
struct Counter {
    count: UnsafeCell<usize>,
    sync: AtomicUsize,
}
impl Counter {
    fn new() -> Counter {
        Counter {
            count: UnsafeCell::new(0),
            sync: AtomicUsize::new(0),
        }
    }
    fn inc(&self) {
        let old_sync = self.sync.load(Ordering::SeqCst);
        let new_sync = old_sync.wrapping_add(1);
        while self.sync.compare_and_swap(old_sync, new_sync, Ordering::SeqCst) != old_sync {
            old_sync = self.sync.load(Ordering::SeqCst);
            new_sync = old_sync.wrapping_add(1);
        }
        let count = unsafe { &mut *self.count.get() };
        *count += 1;
        self.sync.fetch_add(1, Ordering::SeqCst);
    }
    fn get(&self) -> usize {
        let old_sync = self.sync.load(Ordering::SeqCst);
        let new_sync = old_sync.wrapping_add(1);
        while self.sync.compare_and_swap(old_sync, new_sync, Ordering::SeqCst) != old_sync {
            old_sync = self.sync.load(Ordering::SeqCst);
            new_sync = old_sync.wrapping_add(1);
        }
        let count = unsafe { &*self.count.get() };
        let result = *count;
        self.sync.fetch_add(1, Ordering::SeqCst);
        result
    }
}
fn main() {
    let counter = Counter::new();
    let mut handles = vec![];
    for _ in 0..10 {
        let handle = thread::spawn(move || {
            for _ in 0..10000 {
                counter.inc();
            }
        });
        handles.push(handle);
    }
    for handle in handles {
        handle.join().unwrap();
    }
    println!("Result: {}", counter.get());
}

在这个例子中,我们创建了一个 Counter 结构体,它包含了一个 UnsafeCell<usize> 类型的字段 count,以及一个 AtomicUsize 类型的字段 syncUnsafeCell<T> 类型的作用是允许对其内部的值进行修改,即使是在不可变引用的情况下。AtomicUsize 是一个原子类型,它可以在多个线程之间安全地共享一个整数值。

Counter 结构体实现了 inc 方法和 get 方法,分别用于增加计数器的值和获取计数器的值。这些方法通过对 sync 字段进行 CAS 操作来实现线程安全,以避免竞争条件。同时,它们也使用了 UnsafeCell 来获取计数器的可变引用。 需要注意的是,使用 UnsafeCell 时需要遵循 Rust 的内存安全规则。如果你不小心在多个线程之间访问了同一个 UnsafeCell,那么就可能会出现数据竞争和其它的内存安全问题。因此,一定要谨慎地使用 UnsafeCell,确保正确地处理内存安全问题。

rust 中的线程安全 Send 和 Sync

在 Rust 中,线程安全是一个很重要的概念,因为 Rust 的并发模型是基于线程的。为了确保线程安全,Rust 提供了两个 trait,分别是 SendSync

Send trait 表示一个类型是可以安全地在线程间传递的。具体来说,实现了 Send trait 的类型可以被移动到另一个线程中执行,而不会出现数据竞争或其它的线程安全问题。对于基本类型(如整数、浮点数、指针等)和大多数标准库类型,都是 Send 的。对于自定义类型,只要它的所有成员都是 Send 的,那么它也是 Send 的。

Sync trait 表示一个类型在多个线程间可以安全地共享访问。具体来说,实现了 Sync trait 的类型可以被多个线程同时访问,而不会出现数据竞争或其它的线程安全问题。对于大多数标准库类型,都是 Sync 的。对于自定义类型,只要它的所有成员都是 Sync 的,那么它也是 Sync 的。

需要注意的是,SendSync trait 是自动实现的,也就是说,如果一个类型的所有成员都是 SendSync 的,那么它就是 SendSync 的,无需手动实现这两个 trait。不过,如果一个类型包含了非 Send 或非 Sync 的成员,那么它就无法自动实现这两个 trait,需要手动实现。

  • 在实际使用中,SendSync trait 通常用于泛型类型约束和函数签名中,以确保类型的线程安全性。比如,一个函数的参数必须是 Send 类型的,才能被跨线程调用;一个泛型类型的参数必须是 Sync 类型的,才能被多个线程同时访问。

线程阻塞和唤醒

在 Rust 中,线程的阻塞和唤醒是通过操作系统提供的原语来实现的。操作系统提供了一些系统调用(如 pthread_cond_waitpthread_cond_signal 等),可以让线程进入睡眠状态,并在条件满足时被唤醒。这些系统调用通常被封装在 Rust 的标准库中,以便于使用。

除了操作系统提供的原语外,Rust 还提供了一个名为 parking_lot 的库,用于实现线程的阻塞和唤醒。parking_lot 库提供了两种阻塞和唤醒线程的机制,分别是 MutexCondvar

Mutex 是一种常见的同步原语,用于保护共享资源的访问。当一个线程想要获取一个被 Mutex 保护的资源时,如果该资源已经被其它线程占用,那么该线程就会被阻塞,直到该资源被释放。Mutex 的实现通常使用了操作系统提供的原语,以确保线程的阻塞和唤醒是正确的。

Condvar 是一种条件变量,用于在特定条件满足时唤醒等待的线程。当一个线程想要等待一个条件变量时,它会先获取一个 Mutex,然后调用 wait 方法等待条件变量。如果条件变量未满足,该线程就会被阻塞。当条件变量满足时,另一个线程会调用 notify_onenotify_all 方法来唤醒等待的线程。Condvar 的实现通常也使用了操作系统提供的原语,以确保线程的阻塞和唤醒是正确的。

需要注意的是,parking_lot 库虽然是 Rust 标准库的一部分,但它并不是操作系统提供的原语,而是使用了自己的算法实现的。因此,虽然 parking_lot 库提供了比标准库更高效的同步机制,但在某些特定的场景下,操作系统提供的原语可能会更加适合。在选择同步机制时,需要根据实际的需求和性能要求来进行选择。

以上就是Rust Atomics and Locks并发基础理解的详细内容,更多关于Rust 并发基础的资料请关注其它相关文章!

相关文章