【问题标题】:Understanding the Send trait了解发送特征
【发布时间】:2020-04-13 03:14:57
【问题描述】:

我正试图围绕Send + Sync 特征。我得到了Sync 背后的直觉——这是传统的线程安全(就像在C++ 中一样)。该对象执行必要的锁定(如果需要,内部可变性),因此线程可以安全地访问它。

Send 部分有点不清楚。我理解为什么像 Rc 这样的东西只是 Send - 对象可以被赋予不同的线程,但非原子操作使其线程不安全。

  1. Send 背后的直觉是什么?是否意味着对象可以被复制/移动到另一个线程上下文中,并在复制/移动后继续有效?

  2. Sync 但没有Send”的任何示例场景都会有帮助。还请指出这种情况下的任何 rust 库(我发现了几个相反的)

对于 (2),我发现一些线程使用带有指向堆栈/线程本地存储上的数据的指针的结构作为示例。但无论如何这些都是不安全的(同步或其他)。

【问题讨论】:

标签: rust


【解决方案1】:

Sync 允许两个线程 A 和 B 同时使用一个对象。这对于非可变对象来说是微不足道的,但突变需要同步(按顺序执行,所有线程都可以看到相同的顺序)。这通常使用MutexRwLock 来完成,它允许一个线程继续,而其他线程必须等待。通过强制共享更改顺序,这些类型可以将非Sync 对象转换为Sync 对象。生成对象Sync 的另一种机制是使用原子类型,它本质上是Sync 原语。

Send 允许两个线程 A 和 B 在不同时间使用一个对象。线程 A 可以创建和使用一个对象,然后将其发送给线程 B,因此线程 B 可以使用该对象,而线程 A 不能。 Rust 所有权模型可用于强制执行这种非重叠使用。因此所有权模型是 Rust 的Send 线程安全的重要组成部分,这可能是Send 与其他语言相比不如Sync 直观的原因。

使用上面的定义,应该很明显为什么很少有类型是Sync 而不是Send。如果一个对象可以同时被两个线程安全使用(Sync),那么它可以被两个线程在不同时间安全使用(Send)。因此,Sync 通常意味着Send。任何异常都可能与Send 在线程之间的所有权转移有关,这会影响哪个线程运行Drop 处理程序并释放该值。

如果可以保证在不同的时间使用,大多数对象可以被不同的线程安全地使用。因此,大多数类型是Send

Rc 是一个例外。它没有实现SendRc 允许数据拥有多个所有者。如果线程 A 中的一个所有者可以将 Rc 发送给另一个线程,从而将所有权授予线程 B,那么线程 A 中可能还有其他所有者仍然可以使用该对象。由于引用计数是非原子修改的,因此两个线程上的计数值可能会不同步,并且一个线程可能会丢弃指向的值,而另一个线程中有所有者。

Arc 是一个Rc,它使用原子类型作为引用计数。因此,它可以被多个线程使用,而计数不同步。如果Arc指向的数据是Sync,那么整个对象就是Sync。如果数据不是Sync(例如可变类型),则可以使用Mutex 将其设为Sync。因此,Arc<Mutex<T>> 类型在多线程 Rust 代码中的激增。

【讨论】:

  • 很好的答案。只是一个小点; “如果数据不是Sync(例如可变类型),则可以将其设为Sync ...” - 可变类型可以是可变引用,即Sync。例如。 play.rust-lang.org/…
  • @vikram2784 请注意rayon::scope::spawnstd::thread::spawn 具有不同的参数类型。还要注意&mut T 是同步当且仅当&&mut T 是发送,所以它最终在其他线程中是不可变的。见:doc.rust-lang.org/std/marker/trait.Sync.html
【解决方案2】:

Send 意味着一个类型可以安全地从一个线程移动到另一个线程。如果同一类型也实现了Copy,这也意味着从一个线程复制到另一个线程是安全的。

Sync 表示一个类型可以安全地同时从多个线程引用。具体来说,&TSend,如果 TSync,则可以移动/复制到另一个线程。

所以SendSync 捕获了线程安全的两个不同方面:

  • Send 类型只能由单个线程拥有,因为它们不能移动或复制到其他线程。
  • Sync 类型只能由单个线程在任何时候使用,因为它们的引用不能移动或复制到其他线程。如果它们实现了Send,它们仍然可以在线程之间移动。

没有Sync 没有Send 几乎没有意义,因为能够使用来自不同线程的类型通常意味着在线程之间移动所有权也应该是可能的。虽然它们在技术上有所不同,但可以想象某些类型可以是Sync,但不是Send

拥有数据的大多数类型将是Send,因为在少数情况下数据无法从一个线程移动到另一个线程(之后无法从原始线程访问)。 一些常见的例外情况:

  • 原始指针永远不会是 SendSync
  • 在没有线程同步的情况下共享数据所有权的类型(例如Rc)。
  • 借用不是Sync的数据的类型。
  • 来自外部库或操作系统的非线程安全类型。

【讨论】:

  • 谢谢大家。微妙,但 IIUC:如果整个 Struct Foo { } 可以移动,那就是 Send。如果 &struct Foo 可以共享,那就是 Sync。一个人为的 Sync but no Send 示例:(假设作用域线程存在生命周期问题)- Struct Foo{} 使用线程本地存储来保持某些状态,并为该状态公开线程安全的 get/set 方法。现在可以传递 &Foo,但不能传递 Foo 本身。这听起来对吗?
  • This URLO 线程谈论它;提到线程本地存储和奇怪的 ffi 类型。所以是的,您的&Foo 可以传递,但Foo 本身不能传递。 This 是一个奇怪的例子;因为Mutex<T>: Sync where T: Send,你不能绕过&Mutex<Foo>
  • Box 既是同步/发送也有点令人惊讶,因为人们期望它是仅发送,保证唯一的所有权。我的推理是:它就像 C++ 中的 std::unique_ptr 一样,只能移动。同样,只要单个线程拥有/使用指针,原始指针就可以只发送。但我猜原始指针属于不安全/灰色区域
  • Box<T> 本质上可以被视为T。它代表T 的所有权,如果T: Send/T: Sync 也将实现Send/Sync 特征。 Box<T> 在语义上所做的唯一一件事就是将对象移动到堆中并给你一个指向它的指针。这个指针很特别,因为指针(我称之为框)现在拥有该对象。在大多数情况下,您应该将Box<T> 视为与T 相同。
【解决方案3】:

总体

SendSync 的存在有助于在涉及多个线程时考虑类型。在单线程世界中,SendSync 不需要存在。

不要总是将SendSync 视为允许你做某事,或者赋予你权力做某事,这也可能会有所帮助。相反,将!Send!Sync 视为禁止防止你做多线程有问题的事情的方式。

对于SendSync的定义

如果某个类型的XSend,那么如果你有一个拥有的X,你可以将它移到另一个线程中。

  • 如果 X 与多/共享所有权有某种关联,这可能会出现问题。
  • Rc 对此有问题,因为拥有一个Rc 允许您创建更多拥有的Rc(通过克隆它),但您不希望其中任何一个传递到其他线程。问题是许多线程可能同时对Rc 进行更多克隆,并且其中的所有者计数器在多线程情况下无法正常工作——因为即使每个线程都拥有一个Rc,真正的计数器只有一个,访问不会同步。
  • Arc 可能会更好。至少它的所有者柜台能够处理上述情况。所以在这方面,Arc 可以允许Send'ing。但前提是内部类型同时为SendSync。例如,Arc<Rc> 仍然存在问题 - 请记住 Rc 禁止 Send (!Send) - 因为多个线程拥有自己的 Arc<Rc> 克隆仍然可以调用 Rc 自己的“多线程”问题 - Arc 本身无法保护线程不这样做。 Arc<T> 的另一个要求是 Send,也要求 TSync 没什么大不了的,因为如果一个类型已经禁止 Send'ing,它也可能是禁止Sync'ing。
  • 因此,如果某些类型禁止使用 Sending,那么无论您尝试将其包装成什么其他类型,您都无法将其“发送”到另一个线程中。

如果某个类型的XSync,那么如果多个线程碰巧有一个&X,它们都可以安全地使用&X

  • 如果&X 允许内部可变性,并且您想禁止Sync,如果您想防止多个线程具有&X,则这是有问题的。
  • 所以如果XSending的问题,那基本上也会有Syncing的问题。
  • Cell 也有问题 - 实际上并没有禁止 Sending。由于Cell 只通过&Cell 允许内部变异,并且在多线程情况下变异访问并不能保证任何事情,所以它必须禁止Syncing - 即多个线程具有&Cell 的情况必须不允许(一般)。关于它是Send,拥有的Cell 仍然可以移动到另一个线程中,只要在其他任何地方都没有&Cell
  • Mutex 可能会更好。它还允许内部突变,在这种情况下,它知道如何处理许多线程试图这样做 - Mutex 只需要它内部的任何内容都不会禁止 Send'ing - 否则,这是同样的问题Arc 将不得不处理。一切顺利,Mutex 既是 Send 又是 Sync
  • 这不是一个实际的例子,而是一个奇怪的说明:如果我们有一个Mutex<Cell>(这是多余的,但是哦,好吧),其中Cell 本身禁止SyncMutex 能够处理有这个问题,仍然是(或“重新允许”)Sync。这是因为,一旦一个线程访问了Cell,我们知道它不必处理仍在尝试同时访问其他&Cell 的其他线程,因为Mutex 将被锁定并阻止这不会发生。

在多线程中改变一个值

理论上你可以在线程之间共享Mutex
如果您尝试简单地移动拥有的Mutex,您将完成它,但这没有用,因为您希望多个线程同时访问它。
因为它是Sync,所以你可以在线程之间共享一个&Mutex,它的锁方法确实只需要一个&Mutex。 但是尝试这样做是有问题的,假设:您在main 线程中,然后您创建一个Mutex,然后创建一个对它的引用,一个&Mutex,然后创建另一个线程Z 您尝试将&Mutex 传递给。 问题是Mutex 只有一个所有者,即在main 线程内。如果由于某种原因线程Zmain 线程寿命更长,那么&Mutex 将悬空。因此,即使Mutex 中的Sync 没有特别禁止您在线程之间发送/共享&Mutex,出于终身原因,您也可能无法以这种方式完成它。 Arc 救援! Arc 将摆脱这个终身问题。它可以由多线程多拥有,而不是由特定线程中的特定范围拥有。 因此,使用Arc<Mutex> 将允许共同拥有和共享一个值,并在多个线程之间提供内部可变性。总而言之,Mutex 本身重新允许 Syncing 而不是特别禁止 Sending,Arc 在提供共享所有权时也没有特别禁止(避免终身问题)。

类型的小列表

SendSync 类型是既不特别禁止也不禁止的类型:

  • 原语,ArcMutex - 取决于内部类型

Send!Sync 类型是提供(多线程不同步)内部可变性的类型:

  • Cell, RefCell - 取决于内部类型

!Send!Sync 类型是提供(多线程不同步)共同所有权的类型:

  • Rc

我不知道!SendSync 的类型;

【讨论】:

  • !SendSync 的类型例如:RwLockRead(Write)GuardMutexGuard from std::sync
【解决方案4】:

根据 Rustonomicon: Send and Sync

如果可以安全地将其发送到另一个线程,则该类型为 Send。

如果在线程之间共享是安全的,则类型是 Sync(T 是 Sync 当且仅当 &T 是 Send)。

【讨论】:

    猜你喜欢
    • 2012-01-17
    • 1970-01-01
    • 2015-02-25
    • 2018-03-20
    • 2017-10-21
    • 2018-12-23
    • 1970-01-01
    • 1970-01-01
    • 2021-04-02
    相关资源
    最近更新 更多