Extensible Concurrency with the Sync and Send Traits
One of the interesting aspects of Oxide's concurrency story is that the language defines the concurrency primitives quite minimally. Nearly all concurrency features we've talked about are part of the standard library, not the language itself.
However, two concurrency concepts are embedded in the language: the Send and Sync traits.
Send and the Transferable Across Threads
The Send marker trait indicates that ownership of values of the type implementing Send can be transferred between threads. Almost every Oxide type is Send, but there are some exceptions, such as Rc<T>, which is not Send.
Rc<T> is not Send because of how it maintains reference counts. When you clone an Rc<T>, the reference count is incremented without using atomic operations. If you sent an Rc<T> to another thread and that thread cloned it while the original thread was also cloning it, the reference count could be corrupted.
Most basic types are Send: integers, floats, booleans, strings, and most collections built from Send types. Types that contain raw pointers are generally not Send because it's unsafe to send a raw pointer to another thread.
Examples of Send Types
// All of these are Send
let x: Int = 5
let s: String = "hello".toString()
let v: Vec<Int> = vec![1, 2, 3]
// This is Send because it contains only Send types
struct SendStruct {
value: Int,
text: String
}
Examples of Non-Send Types
// Rc is not Send (reference counting is not atomic)
import std.rc.Rc
let rcValue = Rc.new(5)
// thread.spawn move { println!(\(rcValue)) } // Error: Rc is not Send
Sync and Thread-Safe Shared References
The Sync marker trait indicates that a type is safe to share by reference between threads. In other words, a type T is Sync if &T is Send. A reference is safe to send to another thread if the type implements Sync.
For example, i32 is Sync because references to i32 are safe to share with threads (integers are thread-safe). Cell<T>, on the other hand, is not Sync because it uses interior mutability without synchronization.
Why These Traits Matter
These traits help enforce thread safety at compile time:
import std.sync.Mutex
import std.sync.Arc
import std.thread
fn main() {
let safeCounter = Arc.new(Mutex.new(0))
let counterClone = Arc.clone(&safeCounter)
thread.spawn move {
var count = counterClone.lock().unwrap()
*count += 1
}
// Code continues...
}
This compiles because:
Arc<T>isSendandSyncwhenTis bothMutex<Int>is bothSendandSync- The compiler verifies ownership can be safely transferred
Implementing Send and Sync Manually
Most of the time, you don't need to implement Send and Sync manually. Oxide automatically derives these traits for structs and enums composed entirely of Send and Sync types.
However, in rare cases where you're working with raw pointers or other unsafe code, you might need to manually implement these traits:
// Only do this if you're sure your type is actually safe!
// This is unsafe to implement incorrectly.
struct MyType {
ptr: *const Int
}
// UNSAFE: only implement if you know what you're doing
unsafe extension MyType: Send {}
unsafe extension MyType: Sync {}
As you can see, implementing Send and Sync requires the unsafe keyword. This is because the compiler can't verify that your type is actually safe to send or share across threads; you're promising it with the unsafe extension.
Common Patterns
Arc<Mutex<T>> is Send and Sync
When T is Send and Sync, Arc<Mutex<T>> is both:
import std.sync.Arc
import std.sync.Mutex
import std.thread
fn main() {
let counter = Arc.new(Mutex.new(0))
// Works because Arc<Mutex<Int>> is Send + Sync
for _ in 0..5 {
let c = Arc.clone(&counter)
thread.spawn move {
var n = c.lock().unwrap()
*n += 1
}
}
}
Rc<T> is Neither Send nor Sync
Rc<T> is not Send because the reference counting isn't atomic. It's also not Sync because &Rc<T> is not Send.
import std.rc.Rc
import std.thread
fn main() {
let rc = Rc.new(5)
// This won't compile
// thread.spawn move {
// println!(\(rc)) // Error: Rc is not Send
// }
}
If you need shared ownership across threads, use Arc<T> instead of Rc<T>.
Cell<T> is Sync but Not Send
Cell<T> provides interior mutability using dynamic borrowing instead of locks. It's Sync because it's safe to share references, but not Send because it's not safe to move across threads:
import std.cell.Cell
import std.thread
fn main() {
let cell = Cell.new(5)
// Cell is Sync, so we can share a reference safely
// (but this requires a reference, not an owned value)
// Cannot do this: Cell is not Send
// thread.spawn move {
// cell.set(10) // Error: Cell is not Send
// }
}
Rust Comparison
The Send and Sync traits work identically between Rust and Oxide:
| Concept | Rust | Oxide | Notes |
|---|---|---|---|
| Send trait | Built-in | Built-in | Indicates safe to transfer ownership |
| Sync trait | Built-in | Built-in | Indicates safe to share by reference |
| Manual impl | unsafe extension T: Send {} | unsafe impl Send for T {} | Oxide uses extension |
| Arc is Send+Sync | When T is | When T is | For thread-safe shared ownership |
| Rc is not Send | Correct | Correct | Reference counting is not atomic |
The behavior and semantics are identical. Both languages use these traits to provide compile-time verification of thread safety without runtime overhead.
Summary
Send- A marker trait indicating that a type can be safely transferred between threadsSync- A marker trait indicating that a type is safe to share by reference between threads- Types composed of
Send/Synctypes are automaticallySend/Sync Arc<T>isSendandSyncwhenTis bothRc<T>is neitherSendnorSync- You can manually implement these traits with
unsafe extension, but should only do so when you're certain of thread safety - The compiler uses these traits to prevent data races at compile time