Marker-Traits sind Traits ohne Methoden — sie tragen keine Verhaltens-Beschreibung, sondern kennzeichnen Eigenschaften eines Typs für den Compiler. Send etwa hat keine Methoden; ein Typ, der Send implementiert, signalisiert: "ich darf über Thread-Grenzen verschoben werden". Der Compiler nutzt diese Marker, um z.B. zu prüfen, ob ein Typ in thread::spawn übergeben werden darf. Die fünf wichtigsten Marker-Traits — Send, Sync, Sized, Copy, Unpin — sind die Sprache des Typ-Systems, mit der Rust Speicher- und Thread-Sicherheit garantiert. Wer sie versteht, versteht die Sicherheits-Architektur von Rust.
Was ein Marker-Trait ist
Ein Marker-Trait ist syntaktisch ein Trait ohne Body (keine Methoden, keine Associated Types).
// Selbstdefinierter Marker
pub trait IstKonstant {}
struct Pi;
struct E;
impl IstKonstant for Pi {}
impl IstKonstant for E {}
// Funktion akzeptiert nur Konstanten
fn akzeptiere_konstante<T: IstKonstant>(_x: T) {
println!("Eine Konstante");
}
fn main() {
akzeptiere_konstante(Pi);
akzeptiere_konstante(E);
// akzeptiere_konstante(42); // Compile-Fehler: i32 ist nicht IstKonstant
}IstKonstant ist ein Marker — Trait ohne Methoden. Der Marker selbst tut nichts; er erlaubt nur, in Bounds zu sagen "dieser Typ ist eine Konstante". Der Compiler erzwingt die Markierung.
Wozu das gut ist: Typ-Klassen ohne Verhalten. Du gruppierst Typen nach einer Eigenschaft (hier: ist eine Konstante), ohne Methoden definieren zu müssen.
Send — Thread-Verschiebbarkeit
Send markiert: ein Wert dieses Typs darf zwischen Threads verschoben werden.
use std::thread;
fn main() {
let daten = vec![1, 2, 3]; // Vec<i32> ist Send
thread::spawn(move || {
println!("{daten:?}"); // OK: Vec<i32> darf in den neuen Thread
}).join().unwrap();
}thread::spawn verlangt, dass die übergebene Closure Send ist. Vec ist Send (i32 ist Send, Vec von Send-Items ist Send), also funktioniert es.
Was ist nicht Send?
use std::rc::Rc;
// Rc<T> ist NICHT Send — Reference-Counting ohne Atomic-Operationen
// ist nicht thread-safe
fn main() {
let rc = Rc::new(42);
// thread::spawn(move || {
// println!("{rc}");
// });
// Compile-Fehler: `Rc<i32>` cannot be sent between threads safely
// note: the trait `Send` is not implemented for `Rc<i32>`
let _ = rc;
}Klassische nicht-Send-Typen:
Rc<T>— Reference-Counter ohne Atomics*const T,*mut T— Raw-Pointers (Sicherheit beim Compiler nicht garantierbar)- Typen, die intern
Cell/RefCellmit non-Send-Inhalt halten
Send ist ein Auto-Trait: der Compiler implementiert es automatisch für Typen, die nur aus Send-Komponenten bestehen. Du musst es nicht manuell implementieren.
Sync — Thread-Referenz-Sicherheit
Sync markiert: ein Wert dieses Typs darf per geteilter Referenz zwischen Threads benutzt werden. Etwas formaler: T ist Sync, wenn &T Send ist.
use std::sync::Arc;
use std::thread;
fn main() {
let daten = Arc::new(vec![1, 2, 3]); // Arc<Vec<i32>> ist Sync
let h1 = {
let daten = Arc::clone(&daten);
thread::spawn(move || {
println!("Thread 1: {daten:?}");
})
};
let h2 = {
let daten = Arc::clone(&daten);
thread::spawn(move || {
println!("Thread 2: {daten:?}");
})
};
h1.join().unwrap();
h2.join().unwrap();
}Arc<T> ist Sync, wenn T Sync ist — geteilte Referenzen auf das selbe Arc dürfen in mehrere Threads. Ohne Sync wäre das ein Datenrennen.
Wichtige nicht-Sync-Typen:
Cell<T>,RefCell<T>— Interior-Mutability ohne LocksRc<T>— Ref-Counter ohne Atomics
Auch Sync ist Auto-Trait: automatisch implementiert, wenn alle Komponenten Sync sind.
Sized — Größe zur Compile-Zeit bekannt
Sized markiert: die Größe des Typs in Bytes ist zur Compile-Zeit bekannt. Die meisten Typen sind Sized — Ausnahmen sind str, [T], und dyn Trait.
// Implizit ist <T: Sized> der Default
fn process<T>(x: T) -> T {
x
}
// Identisch zu: fn process<T: Sized>(x: T) -> T
// Mit ?Sized erlaubst du auch unsized:
fn process_ref<T: ?Sized>(x: &T) {
// funktioniert für str, [u8], dyn Trait etc.
}Jeder Type-Parameter <T> hat implizit den Bound T: Sized. Wenn du explizit auch unsized erlauben willst, musst du ?Sized schreiben (das "?" bedeutet "möglicherweise nicht Sized").
Unsized-Typen können nicht direkt als Wert weitergegeben werden — nur per Referenz, Box, oder andere Container.
// str — die unsized Form von String
let s: &str = "hello"; // Per Referenz
// let s: str = "hello"; // FEHLER: str ist unsized
// [i32] — die unsized Form von Array
let arr: &[i32] = &[1, 2, 3]; // Per Referenz
// let arr: [i32] = [1, 2, 3]; // FEHLER: [i32] ist unsized
// dyn Trait — runtime-polymorphe Werte
// let x: dyn Display = 42; // FEHLER
let x: Box<dyn std::fmt::Display> = Box::new(42); // Per BoxUnsized-Werte brauchen immer eine fat-pointer-Indirektion: &str, Box<dyn Trait>, etc. Daher gehen sie nicht als Funktions-Parameter fn(x: str) — die Stack-Größe wäre unbekannt.
Copy — Bit-für-Bit-Kopierbarkeit
Copy markiert: ein Wert kann durch einfaches Kopieren der Bits dupliziert werden (kein move, kein clone-Aufruf).
// i32 ist Copy
fn main() {
let a: i32 = 42;
let b = a; // Kopie, kein Move
println!("{a}, {b}"); // Beide noch nutzbar
// String ist NICHT Copy
let s1 = String::from("hello");
let s2 = s1; // Move, nicht Copy!
// println!("{s1}"); // FEHLER: s1 wurde gemoved
let _ = s2;
}Copy-Typen haben Wert-Semantik. Bei Zuweisung wird der Wert dupliziert; beide Variablen sind unabhängig nutzbar.
Welche Typen sind Copy?
- Alle primitiven numerischen Typen: i8, i16, ..., f64
- bool, char
- Tuples aus Copy-Typen
- Arrays mit Copy-Inhalten (fester Größe)
- Shared references (&T) — aber nicht &mut T
- Function-Pointer
Welche nicht?
- String, Vec, Box, alle Heap-allozierten Container
- Strukturen, die heap-Daten halten
- Mutable references (
&mut T)
Copy ist ein Supertrait von Clone: trait Copy: Clone. Wenn du Copy implementierst, musst du auch Clone implementieren.
#[derive(Copy, Clone)]
struct Punkt {
x: i32,
y: i32,
}
// Mit derive: Compiler erzeugt Copy + Clone Impls
fn main() {
let p1 = Punkt { x: 1, y: 2 };
let p2 = p1; // Kopie!
println!("p1 = ({}, {})", p1.x, p1.y);
println!("p2 = ({}, {})", p2.x, p2.y);
}Copy für eigene Typen via #[derive(Copy, Clone)]. Nur möglich, wenn alle Felder Copy sind.
Unpin — bewegbar trotz Pin
Unpin ist subtiler — er kommt bei async/await ins Spiel.
Pin<P> (siehe spätere Kapitel) ist ein Pointer-Wrapper, der verhindert, dass der innere Wert im Speicher bewegt wird. Das ist nötig für selbst-referenzierende Strukturen, die in async-Generated-Code vorkommen.
Aber: die meisten Typen sind eigentlich harmlos bewegbar, auch nach Pin. Diese Typen markiert Unpin — "ich darf bewegt werden, auch wenn ich gepinnt bin".
use std::pin::Pin;
// Praktisch alle normalen Typen sind Unpin
fn main() {
let mut x = 42;
let pinned: Pin<&mut i32> = Pin::new(&mut x);
// Weil i32 Unpin ist, ist auch Pin<&mut i32> normal nutzbar
let _ = pinned;
}Wenn ein Typ Unpin ist, sind Pin-bezogene Operationen "ungefährlich" — du kannst die innere Mutable-Ref normal extrahieren.
Wenn ein Typ nicht Unpin ist (klassisch: async-Futures mit Selbst-Referenzen), erlaubt Pin keine ungesicherten Bewegungen — der Speicher-Ort des Wertes muss stabil bleiben.
99% der normalen Typen sind Unpin. Nur in async-Internals und unsafe-Code wirst du dem Unterschied begegnen.
Praxis: Marker-Traits im Alltag
Thread-Spawn mit Send-Anforderung
use std::thread;
use std::sync::Arc;
use std::sync::Mutex;
fn parallel_count(items: Vec<i32>) -> usize {
let shared = Arc::new(Mutex::new(0));
let handles: Vec<_> = items.into_iter().map(|item| {
let shared = Arc::clone(&shared);
thread::spawn(move || { // Closure muss Send sein
let mut count = shared.lock().unwrap();
*count += item.abs();
})
}).collect();
for h in handles {
h.join().unwrap();
}
let result = *shared.lock().unwrap();
result as usize
}thread::spawn verlangt F: Send + 'static. Die Closure muss Send sein (alle ihre Capture-Variablen Send) und 'static-Lifetime haben (keine geliehenen Refs aus dem Spawning-Scope).
Sync-Anforderung bei Shared-State
use std::sync::Arc;
use std::thread;
fn parallel_print<T: std::fmt::Display + Send + Sync + 'static>(value: T) {
let arc = Arc::new(value);
let handles: Vec<_> = (0..3).map(|i| {
let v = Arc::clone(&arc);
thread::spawn(move || {
println!("Thread {i}: {v}");
})
}).collect();
for h in handles {
h.join().unwrap();
}
}parallel_print verlangt T: Send + Sync + 'static. Send für die Move-Operation in den Thread, Sync für die Arc-Sharing-Semantik.
Sized vs unsized in API-Design
use std::fmt::Display;
// Funktion nimmt jede Display-Referenz — auch unsized
pub fn drucke<T: Display + ?Sized>(value: &T) {
println!("{value}");
}
fn main() {
drucke(&42); // T = i32 (Sized)
drucke("hello"); // T = str (unsized)
drucke(&String::from("x")); // T = String (Sized)
let trait_obj: &dyn Display = &42;
drucke(trait_obj); // T = dyn Display (unsized)
}Mit ?Sized machst du eine Funktion maximal flexibel — sie akzeptiert Referenzen auf jeden Display-Typ, inkl. unsized-Typen wie str oder dyn Trait. Klassisches Stdlib-Pattern bei generischen Helpers.
Copy für Wert-Typen
#[derive(Copy, Clone, Debug)]
pub struct Position {
pub x: i32,
pub y: i32,
}
impl Position {
pub fn neu(x: i32, y: i32) -> Self {
Position { x, y }
}
pub fn verschoben(&self, dx: i32, dy: i32) -> Position {
Position { x: self.x + dx, y: self.y + dy }
}
}
fn main() {
let p1 = Position::neu(1, 2);
let p2 = p1; // Kopie, p1 weiterhin nutzbar
let p3 = p1.verschoben(10, 0);
println!("{p1:?}, {p2:?}, {p3:?}");
}Kleine Wert-Strukturen mit primitiven Feldern: ideal für Copy. Wert-Semantik wie bei Numerics.
Custom-Marker für Type-State
// Marker für API-States
pub trait Open {}
pub trait Closed {}
pub struct File<State> {
path: String,
_state: std::marker::PhantomData<State>,
}
pub struct OpenState;
pub struct ClosedState;
impl Open for OpenState {}
impl Closed for ClosedState {}
impl File<ClosedState> {
pub fn neu(path: String) -> Self {
File { path, _state: std::marker::PhantomData }
}
pub fn open(self) -> File<OpenState> {
File { path: self.path, _state: std::marker::PhantomData }
}
}
impl File<OpenState> {
pub fn read(&self) -> String {
format!("(Daten aus {})", self.path)
}
pub fn close(self) -> File<ClosedState> {
File { path: self.path, _state: std::marker::PhantomData }
}
}Marker-Traits als Type-State: nur offene Files können gelesen werden, nur geschlossene können geöffnet werden. Compiler erzwingt die Lifecycle-Reihenfolge.
Send-Auto-Implementation
// Send wird automatisch abgeleitet, wenn alle Felder Send sind
pub struct Wrapper {
value: i32,
name: String,
}
// Wrapper ist automatisch Send + Sync, weil i32 und String es sind
fn check<T: Send + Sync>(_: T) {}
fn main() {
check(Wrapper { value: 1, name: String::new() });
}Send und Sync sind Auto-Traits. Solange du keine "exotischen" Felder (Raw-Pointer, Rc, etc.) hast, sind deine eigenen Strukturen automatisch Send + Sync.
Bound mit mehreren Markern
use std::fmt::Debug;
use std::hash::Hash;
// Eine Funktion, die viele Marker-Bounds kombiniert
pub fn process_in_thread<T>(value: T)
where
T: Send + Sync + Debug + Hash + Eq + Clone + 'static,
{
std::thread::spawn(move || {
println!("Wert: {value:?}");
}).join().unwrap();
}Komplexe Bound-Listen sind in Concurrency-Code üblich. Jeder Marker fügt eine bestimmte Garantie hinzu.
Interessantes
Marker-Trait = Trait ohne Methoden.
Kennzeichnet eine Eigenschaft des Typs, ohne Verhalten zu definieren. Compiler nutzt Marker für Sicherheits-Garantien (Threads, Memory).
Send = darf zwischen Threads verschoben werden.
Voraussetzung für thread::spawn-Closures, tokio::spawn, alle Concurrency-APIs. Auto-Trait: automatisch implementiert, wenn alle Komponenten Send sind. Klassisch nicht-Send: Rc, Raw-Pointer.
Sync = darf per Referenz von mehreren Threads benutzt werden.
T ist Sync ⇔ &T ist Send. Auto-Trait. Klassisch nicht-Sync: Cell, RefCell, Rc — alle Interior-Mutability ohne Locks.
Sized = Größe zur Compile-Zeit bekannt — der Standard.
Jeder generic Parameter hat implizit T: Sized. Mit T: ?Sized erlaubst du auch unsized-Typen (str, [T], dyn Trait), die nur per Referenz benutzbar sind.
Copy = Bit-für-Bit-Kopie statt Move.
Wert-Semantik. Klassisch für primitive Typen (i32, bool, char). Eigene Typen via #[derive(Copy, Clone)], nur möglich wenn alle Felder Copy sind. Copy ist Supertrait von Clone.
Unpin = darf bewegt werden, auch nach Pin.
99% der Typen sind Unpin. Nur async-Futures mit Selbst-Referenzen sind !Unpin. In normalem Code irrelevant; in async/Pin-internals zentrale Frage.
Auto-Traits — automatisch implementiert.
Send, Sync, Unpin sind Auto-Traits: der Compiler implementiert sie automatisch für Typen, deren Komponenten sie haben. Du kannst opt-out mit impl !Send for MyType {} (unsafe-Code-Variante).
Marker-Traits in Bounds — die Concurrency-Sprache.
T: Send + Sync + 'static ist der typische Bound für Werte, die in Threads landen sollen. Mit Marker-Bounds drückst du Sicherheits-Garantien in der Signatur aus.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Fearless Concurrency
- The Rustonomicon – Send and Sync
- std::marker – Marker Traits
- Rust Reference – Special Types and Traits