In this lesson, you will learn how to safely share self-referential structures across thread boundaries when pinning is involved.
The Conflict of Self-Reference and Movement
As you know, pinning is essential for self-referential types—data structures that contain pointers to their own internal fields. Because moving such a struct in memory would invalidate those internal pointers, we use Pin<T> to guarantee the data remains stationary.
When moving from single-threaded code to concurrency, a new challenge arises: what happens if you try to Send a pinned object to another thread?
Thread Safety and !Unpin
By default, most types in Rust are Unpin, meaning it is safe to move them even if they are pinned. However, self-referential types (like those generated by async blocks) are often !Unpin. If a type is !Unpin, it cannot be moved once pinned.
If you want to send a pinned, self-referential object to another thread (e.g., across a channel), the type must satisfy the Send trait. This requires that:
- The inner data is inherently
Send. - The pinning mechanism does not assume a specific memory address that is only valid on the originating thread.
Consider a case where you are using a Pin<Box<T>> for a self-referential struct. Because Box<T> is a heap-allocated pointer, moving the Box itself does not move the data on the heap. Thus, a Pin<Box<T>> is Send if T is Send.
use std::pin::Pin;
// A self-referential struct that must stay put
struct MyPinnedTask {
data: String,
ptr: *const String,
}
// If we put it in a Box, moving the Box doesn't move the data.
// As long as MyPinnedTask is Send, this is safe to pass across threads.
let pinned_task: Pin<Box<MyPinnedTask>> = Box::pin(my_struct);
// Sending this through a channel is safe because the data's
// address remains fixed on the heap during the transfer.
tx.send(pinned_task).unwrap();
Why this matters for Concurrency
When building concurrent systems, you often need to move stateful tasks between threads. If those tasks contain local variables that refer to one another, you must ensure the container—usually Box or Arc—provides the stability necessary to satisfy Send. Without the Pin wrapper, the compiler would prevent you from creating the self-reference in the first place; with it, you bridge the gap between memory stability and multi-threaded movement.
Key Takeaway: A pinned, self-referential type is safe to transfer between threads if its container (like Box) keeps the data at a stable address and the underlying type implements Send.