Rust Atomics and Locks并发基础理解
目录
- Rust 中的线程
- 线程作用域
- 所有权共享
- 借用和数据竞争
- 内部可变
- rust 中的线程安全 Send 和 Sync
- 线程阻塞和唤醒
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
的所有权泄漏到堆上,并返回一个指向堆上分配的值的指针。接着,我们使用 &*
来将指针解引用,并将其赋值给 reference1
和 reference2
变量。最后,我们使用 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
的两个引用,并将它们分别赋值给 reference1
和 reference2
变量。最后,我们打印 reference1
和 reference2
变量,以显示它们都引用了同一个值。
需要注意的是,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>
类型提供了一种在不可变引用的情况下,修改其所持有的值的方法。它通过在不可变引用中封装值,并使用 get
和 set
方法来实现内部可变性。以下是一个示例:
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>
类型提供了一种更灵活的内部可变性实现方式。它通过在可变和不可变引用中封装值,并使用 borrow
和 borrow_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
类型的字段 sync
。 UnsafeCell<T>
类型的作用是允许对其内部的值进行修改,即使是在不可变引用的情况下。AtomicUsize
是一个原子类型,它可以在多个线程之间安全地共享一个整数值。
Counter
结构体实现了 inc
方法和 get
方法,分别用于增加计数器的值和获取计数器的值。这些方法通过对 sync
字段进行 CAS 操作来实现线程安全,以避免竞争条件。同时,它们也使用了 UnsafeCell
来获取计数器的可变引用。 需要注意的是,使用 UnsafeCell
时需要遵循 Rust 的内存安全规则。如果你不小心在多个线程之间访问了同一个 UnsafeCell
,那么就可能会出现数据竞争和其它的内存安全问题。因此,一定要谨慎地使用 UnsafeCell
,确保正确地处理内存安全问题。
rust 中的线程安全 Send 和 Sync
在 Rust 中,线程安全是一个很重要的概念,因为 Rust 的并发模型是基于线程的。为了确保线程安全,Rust 提供了两个 trait,分别是 Send
和 Sync
。
Send
trait 表示一个类型是可以安全地在线程间传递的。具体来说,实现了 Send
trait 的类型可以被移动到另一个线程中执行,而不会出现数据竞争或其它的线程安全问题。对于基本类型(如整数、浮点数、指针等)和大多数标准库类型,都是 Send
的。对于自定义类型,只要它的所有成员都是 Send
的,那么它也是 Send
的。
Sync
trait 表示一个类型在多个线程间可以安全地共享访问。具体来说,实现了 Sync
trait 的类型可以被多个线程同时访问,而不会出现数据竞争或其它的线程安全问题。对于大多数标准库类型,都是 Sync
的。对于自定义类型,只要它的所有成员都是 Sync
的,那么它也是 Sync
的。
需要注意的是,Send
和 Sync
trait 是自动实现的,也就是说,如果一个类型的所有成员都是 Send
或 Sync
的,那么它就是 Send
或 Sync
的,无需手动实现这两个 trait。不过,如果一个类型包含了非 Send
或非 Sync
的成员,那么它就无法自动实现这两个 trait,需要手动实现。
- 在实际使用中,
Send
和Sync
trait 通常用于泛型类型约束和函数签名中,以确保类型的线程安全性。比如,一个函数的参数必须是Send
类型的,才能被跨线程调用;一个泛型类型的参数必须是Sync
类型的,才能被多个线程同时访问。
线程阻塞和唤醒
在 Rust 中,线程的阻塞和唤醒是通过操作系统提供的原语来实现的。操作系统提供了一些系统调用(如 pthread_cond_wait
、pthread_cond_signal
等),可以让线程进入睡眠状态,并在条件满足时被唤醒。这些系统调用通常被封装在 Rust 的标准库中,以便于使用。
除了操作系统提供的原语外,Rust 还提供了一个名为 parking_lot
的库,用于实现线程的阻塞和唤醒。parking_lot
库提供了两种阻塞和唤醒线程的机制,分别是 Mutex
和 Condvar
。
Mutex
是一种常见的同步原语,用于保护共享资源的访问。当一个线程想要获取一个被 Mutex
保护的资源时,如果该资源已经被其它线程占用,那么该线程就会被阻塞,直到该资源被释放。Mutex
的实现通常使用了操作系统提供的原语,以确保线程的阻塞和唤醒是正确的。
Condvar
是一种条件变量,用于在特定条件满足时唤醒等待的线程。当一个线程想要等待一个条件变量时,它会先获取一个 Mutex
,然后调用 wait
方法等待条件变量。如果条件变量未满足,该线程就会被阻塞。当条件变量满足时,另一个线程会调用 notify_one
或 notify_all
方法来唤醒等待的线程。Condvar
的实现通常也使用了操作系统提供的原语,以确保线程的阻塞和唤醒是正确的。
需要注意的是,parking_lot
库虽然是 Rust 标准库的一部分,但它并不是操作系统提供的原语,而是使用了自己的算法实现的。因此,虽然 parking_lot
库提供了比标准库更高效的同步机制,但在某些特定的场景下,操作系统提供的原语可能会更加适合。在选择同步机制时,需要根据实际的需求和性能要求来进行选择。
以上就是Rust Atomics and Locks并发基础理解的详细内容,更多关于Rust 并发基础的资料请关注我们其它相关文章!