If T is Send, does it necessarily imply that T is Copy?

My question can also be seen as: If T is sent between two threads, is something like memcpy invoked?

String is Send but not Copy

2 Likes

Proof by contradiction, I like it

Context switching occurs, but the memory doesn't have to change position... that makes sense

For more information, Send means you can send the ownership of this type between threads. Practically most types are Send, like the String allocated in this thread, sent to that thread, and can be deallocated there. One example of the non-Send type is Rc. Rc modifies its reference counter in non-atomic way, so if two instance of Rc that points to same memory got cloned in two different threads concurrent, it triggers data race.

2 Likes

memcpy's involvement is an implementation detail, and does not play (much of) a role in interpreting semantics of Copy when using the language.

Another case is memcpy can still occur even under move semantics, as long as it looks like a move at the higher level.

EDIT: memcpy and Copy are not very related is what I was trying to say poorly.

2 Likes

In Rust, move is semantically memcpy() out the value, invalidating previous one at compile time if it's not Copy.

2 Likes

Here's a way to think about the difference between these two markers:

  • A thing that is Send has only one owner at a time, but ownership can be transferred across threads
  • A thing that is Copy can be duplicated as well as borrowed, so that multiple owners can each have their own and simplify management.

Phrased as above, there's very little relationship between the two. The one with a more meaningful relationship to Send is Sync, which is (roughly) about borrowing across threads.

This copy is semantically the same as memcpy() in that the constraint is that no other code needs to be run (that's Clone and Drop) and the basic memory image can just be copied and later freed with no side-effects. For more: C - Derivable Traits - The Rust Programming Language

The common case is that Copy is typically used for 'trivial' basic types (like integers) used in assignments or passed as in-practice-immutable arguments to functions, to avoid forcing the programmer to always make them immutable references rather than lose ownership.

You can mark your own more complex types as Copy (subject to various constraints) but in practice you very rarely need or want to. For the most part, you can think of it as something that enables better optimisations both in the compiler and in code readability.

More accurately, Sync means that a value can be shared across threads.

1 Like

Send is best understood by a negative definition: a type is Send unless:

  • it is a shared reference (e.g., &T ) to a non-Sync type; since T : !Sync, T may not be sound to share across threads, and since sending a shared reference to a T (a &T) to another thread would allow to share the T across threads, it is forbidden.

    • note that &T is not the only "shared reference" to a T that can exist, &&T is also a shared reference to a T, as well as Rc<T> and Arc<T>, etc.
  • its internal Drop logic involves some thread-local state (quite rare to be honest, but not impossible), or its very existence involves some thread-local property (e.g., a thread-local unique integer).

So the real question ends up being: when is a type [not] Sync?

A type cannot be Sync if it is unsound to share it across threads. This is the case, for instance, when:

  • it offers "unchecked" / unsynchronised interior mutability / aliased mutability.

    • A good example of it is Cell<_>: if you have a &Cell<i32>, then this reference acts pretty much as a C++ int32_t & reference (notice the lack of const): it can be mutated despite the pointer being aliased, which is not multithread-safe since nothing at runtime protects the mutation from being racy. That's what the Sync trait is then for: a compile-time check that forbids (in Rust) to have such references exist in multiple threads.

    • RefCell<_> and RcBox<_> (the heap structure with counter metadata an Rc points to) both use an underlying Cell, so they are transitively not Sync either.

      • Since Rc<T> is a form of shared reference to a RcBox<_>, and the latter is not Send, Rc<_> is neither Send nor Sync,
    • a counter-example of this Sync counter-example would be something like Mutex<_>: this wrapper does offer mutation of the wrappee through a shared / aliased reference &Mutex<_>, but it does so by using a lock that guarantees at runtime that the critical section where the mutation takes place has exclusive access, so no data race here. Thus Mutex<T> : Sync (when T : Send).

  • the type's shared logic (API using &T) involves some thread-local state (again, quite rare).


Finally, as with any marker trait, trait object type erasure (dyn ...) can lead to the Send / Sync properties of the type being "forgotten" by the compiler. For instance, Box<i32> is a type that is both Displayable and Sendable across threads, but if Box<i32> gets coerced to Box<dyn Display>, the latter is not Sendable across threads (if both traits are important, it "should" be coerced to Box<dyn Display + Send>).

6 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.