笑看天下无敌手 发表于 2025-3-1 17:45:09

Rust中的`Sync`与`Send`属性:明白并应用并发安全

在Rust的并发编程中,Sync和Send是两个重要的标准库特性(traits),它们界说了类型在多线程环境下的行为。Rust以其严格的全部权和借用系统确保内存安全,制止了传统并发编程中的很多常见问题。本文将详细解释Sync与Send的含义、利用场景以及常见的混淆点。
1. 什么是Send?

Send是一个标记trait,用于标识一个类型可以在线程之间通报。假如一个类型实现了Send,则可以通过全部权转移(而非引用共享)将其从一个线程通报到另一个线程。在Rust中,全部权转移时会自动处理内存管理,制止了传统并发模子中的数据竞争问题。
Send的关键点



[*]Send表示类型能够在线程之间通报,通常是通过全部权转移。
[*]假如一个类型实现了Send,则可以安全地将其通报给另一个线程。
[*]原生类型(如整数、浮点数、布尔值等)和大多数实现了Send的类型可以在多个线程间通报。
示例

use std::thread;

fn main() {
    let data = vec!;
    let handle = thread::spawn(move || {
      println!("{:?}", data); // 数据被移动到新线程
    });

    handle.join().unwrap();
}
在上面的代码中,data是一个Vec<i32>,它实现了Send,因此可以安全地通报到新线程。这里的move关键字将data的全部权转移到新的线程中,而不会产生数据竞争。
非Send类型

某些类型不能实现Send,尤其是那些包含不可变引用或引用计数的类型。比方,Rc<T>(引用计数指针)和RefCell<T>(可变借用类型)都不能在多个线程之间通报,因为它们依赖于线程局部存储的引用计数或可变借用,这在多线程环境中是不安全的。
use std::rc::Rc;
use std::thread;

fn main() {
    let rc = Rc::new(5);
    let handle = thread::spawn(move || {
      println!("{:?}", rc); // 编译错误,Rc不能被跨线程传递
    });

    handle.join().unwrap();
}
这段代码会编译失败,因为Rc不能在多个线程间通报。
2. 什么是Sync?

Sync是另一个标记trait,用来标识一个类型可以安全地在多个线程之间共享引用。换句话说,假如一个类型实现了Sync,那么它的引用可以在多个线程中共享而不会引发数据竞争。
Sync的关键点



[*]Sync表示类型的引用可以在多个线程间共享。
[*]只有当类型的内部数据能够被多个线程安全地访问时,它才会实现Sync。
[*]假如类型实现了Sync,则该类型的引用(如&T)可以在线程间共享。
示例

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let data = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
      let data = Arc::clone(&data);
      let handle = thread::spawn(move || {
            let mut num = data.lock().unwrap();
            *num += 1;
      });
      handles.push(handle);
    }

    for handle in handles {
      handle.join().unwrap();
    }

    println!("Result: {}", *data.lock().unwrap());
}
在这段代码中,Arc和Mutex都实现了Sync,它们允很多个线程共享对同一数据的引用。Mutex确保同一时间只有一个线程可以访问数据,而Arc则使得可以在多个线程中共享对Mutex的全部权。
如何明白Sync



[*]Sync类型可以在多个线程之间共享,但数据的访问可能需要额外的同步机制(如Mutex或RwLock)。
[*]Sync不意味着类型自己提供内置的线程安全行为,而是指它的引用可以安全地被多个线程同时访问。
3. Send与Sync的关系

Send与Sync的常见误解



[*]Send与Sync常常一起利用:很多类型实现了Send,也会实现Sync。比方,Arc<T>和Mutex<T>都实现了Sync,并且它们在多个线程之间通报时也能保证线程安全。
[*]Send和Sync并不是一回事:Send关注的是如安在多个线程间通报数据(即全部权转移),而Sync关注的是如安在多个线程间共享数据(即引用共享)。
一些容易混淆的例子

Vec<T>是Send但不是Sync

Vec<T>是一个常见的动态数组类型。它实现了Send,因为你可以将Vec<T>的全部权转移到另一个线程,但它并不实现Sync,因为Vec<T>的引用不能在多个线程之间共享。
use std::thread;

fn main() {
    let vec = vec!;
    let handle = thread::spawn(move || {
      println!("{:?}", vec); // vec 被移动到新的线程
    });

    handle.join().unwrap();
}
在这段代码中,Vec<T>的全部权被安全地通报到新线程。但是,Vec<T>自己不能在多个线程中共享引用,因为它并没有提供任何同步机制来保证数据的同等性。
Mutex<T>是Sync但不是Send

Mutex<T>是一个提供互斥访问的数据结构,它允许一个线程锁定并修改数据,但它自己并不总是Send,特殊是当它内部存储的数据类型不是Send时。
use std::sync::Mutex;
use std::rc::Rc;
use std::thread;

fn main() {
    let data = Rc::new(5);
    let mutex = Mutex::new(data);

    let handle = thread::spawn(move || {
      let _locked_data = mutex.lock().unwrap(); // 编译错误,因为Rc不能被跨线程传递
    });

    handle.join().unwrap();
}
由于Rc<T>不是Send的,所以Mutex<Rc<T>>也不能在多个线程之间通报。
Arc<T>是Send和Sync

Arc<T>(原子引用计数)是一个线程安全的智能指针,它允许在多个线程之间共享全部权。Arc<T>实现了Send和Sync,因此它的引用可以在多个线程中安全地共享和通报。
use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(5);
    let handles: Vec<_> = (0..10).map(|_| {
      let data = Arc::clone(&data);
      thread::spawn(move || {
            println!("{}", data); // 共享引用
      })
    }).collect();

    for handle in handles {
      handle.join().unwrap();
    }
}
在这段代码中,Arc<T>使得我们可以跨多个线程共享数据的引用,并确保线程安全。
4. 小结:常见的混淆点



[*]Vec<T>是Send但不是Sync:Vec可以被移动到其他线程,但它不能在多个线程中共享引用。
[*]Mutex<T>是Sync但不是Send:Mutex<T>提供互斥访问,但它不能在多个线程间通报,尤其是当其内部类型不是Send时。
[*]Arc<T>是Send和Sync:Arc<T>是一个线程安全的引用计数指针,可以在多个线程间共享,既能实现Send也能实现Sync。
明白这些眇小的差异对在Rust中编写安全的并发代码至关重要。Rust的全部权和借用系统确保了线程安全,但也要求开发者明确明白每种类型在多线程环境中的行为。通过合理利用Send和Sync,可以制止数据竞争和未界说行为,并写出更加结实的并发代码。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: Rust中的`Sync`与`Send`属性:明白并应用并发安全