3

Rust中的并发性:Sync 和 Send Traits - 睡觉谁叫

 4 months ago
source link: https://www.cnblogs.com/programmingBB/p/18168144
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
3070683-20240430154015721-1143778883.jpg

在并发的世界中,最常见的并发安全问题就是数据竞争,也就是两个线程同时对一个变量进行读写操作。但当你在 Safe Rust 中写出有数据竞争的代码时,编译器会直接拒绝编译。那么它是靠什么魔法做到的呢?

这就不得不谈 Send 和 Sync 这两个标记 trait 了,实现 Send 的类型可以在多线程间转移所有权,实现 Sync 的类型可以在多线程间共享引用。但它们内部都是没有任何方法声明以及方法体的,二者仅仅是作为一个类型约束的标记信息提供给编译器,帮助编译器拒绝线程不安全的代码。

pub unsafe auto trait Send { }

pub unsafe auto trait Sync { }

本文将深入探讨 SyncSend traits,了解为什么某些类型实现这些 traits,而另一些则没有,并讨论 Rust 中并发编程的最佳实践。

The Sync Trait

Sync trait 表示一个类型可以安全地被多个线程同时访问。这里的访问指的是只读共享安全。Rust 中几乎所有的原始类型都实现了 Sync trait

let x = 5; // i32 is Sync

i32 类型实现了 Sync ,所以在线程间共享 i32 值是安全的。

另一方面,提供内部可变性的类型(内部可变性指的是在拥有不可变引用的时候,依然可以获取到其内部成员的可变引用,进而对其数据进行修改。),如 Mutex<T> ,其中 T 未实现 Sync trait。

#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<T: ?Sized + Send> Send for Mutex<T> {}
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}

因为 Mutex 使用锁来保护对内部数据的访问,如果多个线程同时访问它,可能会导致数据竞争或死锁。

举例来说:

use std::sync::Mutex;

let m = Mutex::new(5); //Mutex<i32> is not Sync

Mutex<i32> 类型没有实现 Sync ,所以跨线程共享是不安全的。

为在多个线程安全地访问非 Sync 类型(如 Mutex<i32> ),我们必须使用适当的同步操作,如获取锁,执行操作和释放锁,在本文后面看到使用互斥锁和其他线程安全类型的示例。

支持 Sync 的类型

Rust 中的 Sync trait 确保了对同一数据的多个引用(无论是可变的还是不可变的)可以安全地从多个线程并发访问。任何实现 Sync trait 的类型 T 都可以被认为是“线程安全”的。

Rust 中的 Sync 类型的一些例子是:

  • 原始类型,如 i32boolchar 等。
  • 简单的聚合类型,如元组 (i32, bool)
  • 原子类型,如 AtomicBool

另一方面,非同步类型不能同时使用多个引用,因为这可能导致数据竞争。非同步类型的一些示例包括:

  • Mutex<i32> - 在访问内部 i32 之前需要锁定互斥体。
  • RefCell<i32> - 在访问内部值之前需要借用 RefCell。
  • Rc<i32> - 共享了内部 i32 的所有权,所以多个可变借用是不安全的。

非 Sync 类型多线程访问

Mutex

为在多个线程安全地访问非同步类型,我们需要使用同步原语,如互斥锁。若仅仅使用 Mutex 而不使用 Arc ,可使用像作用域线程(crossbeam),例如:

3070683-20240430154017888-1831807594.png

3070683-20240430154018431-942805122.png

这里,我们使用 Mutex<i32> 来安全地从多个线程中修改和读取内部 String。 lock() 方法获取锁,阻止其他线程访问互斥体。

Atomic

AtomicU64 这样的原子类型也可以使用像 fetch_add() 这样的原子操作从多个线程安全地访问。例如:

3070683-20240430154021849-502181897.png

因此,总而言之,要在 Rust 中跨线程共享数据,数据必须:

  • 类型为 Sync (原始/不可变类型)
  • 封装在互斥或原子类型中(Mutex、RwLock、Atomic*)
  • 使用像通道这样的消息传递技术来跨线程传递数据的所有权。

The Send Trait

Rust 中的 Send trait 表示类型可以安全地跨线程边界传输。如果一个类型实现了 Send ,这意味着该类型的值的所有权可以在线程之间转移。

例如,像 i32bool 这样的原始类型是 Send

3070683-20240430154022778-740205247.png

因为它们在线程之间共享时没有任何内部引用或可变而导致问题:

3070683-20240430154023700-1109715342.png

3070683-20240430154024312-1928854098.png

然而,像 Rc<i32> 这样的类型未实现 Send ,因为它的引用计数在内部发生了变化,并且多个线程改变相同的引用计数可能会导致内存不安全:

3070683-20240430154025258-1874178948.png
3070683-20240430154025931-172135298.png

Rc<T> 这样的非 Send 类型不能跨线程传输,但它们仍然可以在单个线程中使用。当线程需要共享一些数据时,非 Send 类型可以被包装在像 Arc<T> 这样的线程安全的包装器中,Arc使用原子操作来管理引用计数,并允许内部类型在线程之间共享。

总结一下,关于 Send 的几个关键点是:

  • 类型 Send 可以在线程之间转移所有权
  • i32bool 这样的原始类型是 Send
  • 具有内部可变的类型(如 Rc<T> )通常不是 Send
  • Send 类型仍然可以在单个线程中使用,或者在包装在像 Arc<T> 这样的线程安全的容器中时在线程之间共享
  • 跨线程传输非 Send 类型会导致未定义的行为和内存不安全

自定义实现 Sync 和 Send

要创建自定义类型 SyncSend ,您只需实现类型的 SyncSend trait。

这里有一个 持有裸指针*const u8MyBox 结构体, 由于只要复合类型中有一个成员不是 Send 或者 Sync,那么该类型也就不是 Send 或 Sync。裸指针*const u8 均未实现 SendSync TraitMyBox 复合类型也不是 SendSync

若给 MyBox 实现了 Send 和 Sync 则借助 Arc 可在线程间传递和共享数据。当然建议自己不要轻易去实现 Sync 和 Send Trait ,一旦实现就要为被实现类型的线程安全性负责。这件事本来就是一件很难保证的事情。

3070683-20240430154027397-1102374399.png

3070683-20240430154027864-245649535.png

有些类型是不可能生成SyncSend的,因为它们包含非Sync/非Send类型或允许多线程的可变。例如, Rc<T>不能被设置为Send,因为引用计数需要被原子地更新,而RefCell<T>不能被设置为  Sync,因为它的借用检查不是线程安全的。

同步/发送规则和最佳实践

重要的是要记住混合Sync/Send  和非Sync/非Send类型的规则。一些需要遵守的关键规则:

类型必须是Send才能在线程之间移动。这意味着像Rc<T>这样的类型不能跨线程共享,因为它们不是Send 。

  • 如果一个类型包含一个非Send类型,那么外部类型不能是Send。例如Option<Rc<i32>>不是  Send ,因为Rc<i32>不是Send 。
  • Sync类型可以通过共享引用从多个线程并发使用。非Sync类型不能同时使用它们的值,并且一次只能在一个线程中可变。
  • 如果一个类型包含一个非Sync类型,那么外部类型不能是Sync。例如Mutex<Rc<i32>>不是Sync ,因为Rc<i32>不是Sync 。

并发 Rust 代码的一些最佳实践:

  • 尽可能避免可变。支持不可变的数据结构和逻辑。
  • 当需要修改时,使用同步原语(如Mutex<T>)来安全地从多个线程进行。
  • 使用消息传递在线程之间进行通信,而不是直接共享内存。这有助于避免数据竞争和未定义的行为。
  • 尽可能地限制为修改锁定数据的范围。持有锁太长时间会影响性能和吞吐量。
  • 根据它们是否实现SyncSend仔细选择类型。例如,在线程之间共享时,首选Arc<T>而不是  Rc<T> 。
  • 使用atomic类型进行简单的并发访问原语类型。它们允许从多个线程访问而不加锁。

Concurrency in Rust: The Sync and Send Traits | by Technocrat | CoderHack.com | Medium

[基于 Send 和 Sync 的线程安全 - Rust 语言圣经(Rust Course)](https://course.rs/advance/concurrency-with-threads/send-sync.html "基于 Send 和 Sync 的线程安全 - Rust 语言圣经(Rust Course "基于 Send 和 Sync 的线程安全 - Rust 语言圣经(Rust Course)")")

Rust 中的 Arc 和 Mutex|关键在于 --- Arc and Mutex in Rust | It's all about the bit

Rust 入门与实践


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK