Skip to content
Lektion 74 von 1150/115 abgeschlossen
Modul 17 — Asynchrone Programmierung

Warum Async? Das Problem mit synchronen Threads

Das C10K-Problem

Stell dir vor, dein Server soll 10.000 gleichzeitige Verbindungen bedienen. Mit synchronen Threads brauchst du 10.000 OS-Threads — das ist nicht skalierbar:

  • Speicher: Jeder Thread braucht ~2MB Stack → 20GB RAM nur für Stacks
  • Performance: Thread-Latience (Context Switching) wird zur Flaschenhals
  • Limits: Die meisten Systeme erlauben nur ~1000-10000 Threads

Die Lösung: Asynchrone I/O

Async I/O löst das Problem durch Non-blocking Operations:

  • Der Thread blockiert nicht, während auf I/O gewartet wird
  • Stattdessen: "Ich will Daten lesen — ruf mich an, wenn sie da sind"
  • Ein einzelner Thread kann Tausendeasync Operationen gleichzeitig managen
// Synchronous: blockiert den Thread
let data = file.read().unwrap(); // Thread wartet hier

// Asynchronous: gibt Kontrolle zurück
let data = file.read().await; // Thread kann andere Tasks bearbeiten

Async ≠ Parallel

Asynchron bedeutet: eine Operation gibt den Thread frei, während sie auf Externes (I/O, Timer) wartet. Parallel bedeutet: mehrere Dinge gleichzeitig ausführen (multi-Core).

Async ist kooperatives Multitasking: Tasks geben freiwillig die CPU ab. Parallel ist präemptives Multitasking: das OS entreißt Tasks die CPU.

Beide können kombiniert werden: async Tasks auf mehreren Threads.

Rusts Async-Modell

Rust verwendet Futures und async/await:

  • async fn definiert eine asynchrone Funktion
  • .await wartet auf das Ergebnis eines Futures
  • Keine Callbacks — Code liest sich sequentiell

📝 Schnellprüfung

Was ist der Hauptvorteil von Async I/O?

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 75 von 1150/115 abgeschlossen
Modul 17 — Asynchrone Programmierung

Futures: Das Versprechen eines Ergebnisses

Ein Future ist ein Wert, der (vielleicht) irgendwann bereit sein wird. Es ist träg (lazy): nichts passiert, bis du aktiv darauf wartest.

Das Future Trait

pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

pub enum Poll<T> {
    Ready(T),   // Ergebnis ist da
    Pending,    // Noch nicht bereit
}
  • poll() wird wiederholt aufgerufen
  • Wenn Pending: der Task wird Later" gespeichert
  • Wenn Ready(T): das Ergebnis ist verfügbar

Futures sind träg

Ein Future tut nichts, wenn du es nur erstellst. Du musst einen Executor benutzen, der das Future pollt und vorantreibt.

// Dieser Code tut nichts - kein Executor!
let future = async {
    println!("Ich werde nie ausgeführt!");
};

Futures erstellen

1. async-Blöcke

let future = async {
    // Dieser Code läuft, wenn das Future gepollt wird
    let result = some_async_operation().await;
    result + 1
};

2. async-Funktionen

async fn fetch_data() -> Result<String, Error> {
    let client = reqwest::Client::new();
    let response = client.get("https://api.example.com").send().await?;
    response.text().await
}

Futures kombinieren

Die futures Crate bietet Helfer zum Kombinieren:

use futures::future;

async fn fetch_all() -> Vec<String> {
    let future1 = fetch_url("https://api1.com");
    let future2 = fetch_url("https://api2.com");
    
    // infrequent: beide Futures parallel ausführen
    let (result1, result2) = future::join(future1, future2).await;
    vec![result1, result2]
}

Übung: Einfaches Future erstellen

🧩 Übung m17-l2-e1

🔮 Vorhersage: Was passiert beim Ausführen?

Editor wird geladen…
KI-Tutor (sokratisch — keine Komplettlösungen)

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 76 von 1150/115 abgeschlossen
Modul 17 — Asynchrone Programmierung

async/await: Syntax und Semantik

async-Funktionen

async fn my_async_function(param: u32) -> String {
    // Körper ist ein Future
    format!("Ergebnis: {}", param)
}
  • Return-Type ist immer impl Future<Output = T>
  • Der Body ist träg — wird erst bei poll() ausgeführt
  • Struktur: fnasync fn

.await: Warten auf Futures

async fn example() {
    let result1 = first_operation().await;
    let result2 = second_operation(result1).await;
    
    println!("Ergebnisse: {}, {}", result1, result2);
}
  • .await blockiert nicht den Thread
  • Gibt Kontrolle an den Executor zurück
  • Wird nur im async-Kontext verwendet

.await vs .unwrap()

  • .unwrap(): Panik, wenn Result/Option Err/None ist — synchron
  • .await: Warten auf Future — asynchron, gibt Thread frei
// ❌ Falsch: würde blockieren
let result = future.unwrap();

// ✅ Richtig: asynchrones Warten
let result = future.await;

Async Blocks

let future = async {
    let x = operation1().await;
    let y = operation2().await;
    x + y  // Letzter Ausdruck = Future Output
};

Fehlerbehandlung mit ? in Async

async fn fetch() -> Result<String, Error> {
    let response = reqwest::get("https://api.example.com").await?;
    let text = response.text().await?;
    Ok(text)
}
  • ? funktioniert auch mit .await
  • Convertiert Result<Future, Error> automatisch

Übung: Async Funktion mit ?

🧩 Übung m17-l3-e1

🔮 Vorhersage: Was passiert beim Ausführen?

Editor wird geladen…
KI-Tutor (sokratisch — keine Komplettlösungen)

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 77 von 1150/115 abgeschlossen
Modul 17 — Asynchrone Programmierung

Executors: Futures ausführen

Futures tun nichts, bis ein Executor sie pollt und vorantreibt.

Beliebte executors

Tokio (empfohlen)

Der De-facto-Standard für async Rust:

#[tokio::main]
async fn main() {
    let result = my_async_function().await;
    println!("{}", result);
}

Das #[tokio::main] Makro:

  • Erstellt einen Tokio-Runtime
  • Führt den async main-Body aus
  • Blockiert den main-Thread, bis alle Tasks fertig sind

Async-std

Alternative Runtime:

#[async_std::main]
async fn main() {
    let result = my_async_function().await;
    println!("{}", result);
}

Manuelles Polling (selten nötig)

use futures::executor::block_on;

fn main() {
    let future = async {
        println!("Running...");
        42
    };
    
    let result = block_on(future);
    println!("Result: {}", result);
}

Warnung: block_on blockiert den aktuellen Thread! Nur für Tests und einfa

Runtime Selection

Wähle dein Runtime basierend auf deinen Anforderungen:

  • Tokio: Volle Features, hohe Performance, am weitesten verbreitet
  • async-std: Einfacher, basiert auf libstd, weniger Abhängigkeiten
  • Custom Runtime: Für spezielle Anforderungen (z. B. embedded)

Tokio Features

Tasks spawning

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        // Läuft auf einem Worker-Thread
        "Hello from Tokio"
    });
    
    let result = handle.await.unwrap();
    println!("{}", result);
}

Timeouts

use std::time::Duration;

#[tokio::main]
async fn main() {
    let result = tokio::time::timeout(
        Duration::from_secs(5),
        long_operation()
    ).await;
    
    match result {
        Ok(value) => println!("Fertig: {}", value),
        Err(_) => println!("Timeout!"),
    }
}

Übung: Tokio Task spawning

🧩 Übung m17-l4-e1

🔮 Vorhersage: Was passiert beim Ausführen?

Editor wird geladen…
KI-Tutor (sokratisch — keine Komplettlösungen)

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 78 von 1150/115 abgeschlossen
Modul 17 — Asynchrone Programmierung

Concurrency mit Async: Multiple Tasks gleichzeitig

Viele Futures gleichzeitig ausführen

tokio::join! Makro

Führt mehrere Futures gleichzeitig aus und wartet auf alle:

#[tokio::main]
async fn main() {
    let (result1, result2, result3) = tokio::join! {
        fetch_data("url1"),
        fetch_data("url2"),
        fetch_data("url3"),
    };
    
    println!("Ergebnisse: {}, {}, {}", result1, result2, result3);
}

futures::future::join_all

Für dynamische Listen von Futures:

use futures::future;

#[tokio::main]
async fn main() {
    let urls = vec!["url1", "url2", "url3"];
    let futures: Vec<_> = urls.into_iter()
        .map(|url| fetch_data(url))
        .collect();
    
    let results = future::join_all(futures).await;
    
    for result in results {
        println!("Got: {}", result);
    }
}

Task Priorisierung

select! - Erstes Futures, das fertig ist

use tokio::sync::oneshot;

#[tokio::main]
async fn main() {
    let (tx1, rx1) = oneshot::channel();
    let (tx2, rx2) = oneshot::channel();
    
    tokio::spawn(async {
        tokio::time::sleep(Duration::from_secs(1)).await;
        tx1.send("Slow").unwrap();
    });
    
    tokio::spawn(async {
        tokio::time::sleep(Duration::from_millis(100)).await;
        tx2.send("Fast").unwrap();
    });
    
    tokio::select! {
        result = rx1 => {
            println!("rx1 finished first: {:?}", result);
        }
        result = rx2 => {
            println!("rx2 finished first: {:?}", result);
        }
    }
}

Kooperatives vs Präemptives Scheduling

Tokio verwendet kooperatives Scheduling:

  • Tasks müssen .await verwenden, damit andere Tasks laufen können
  • Keine präemptives Unterbrechen → keine unvorhersehbaren Pausen
  • Verantwortung beim Entwickler: Tasks nicht zu lange blockieren

Verglichen mit OS-Threads:

  • OS-Threads: präemptiv, Context Switches jederzeit möglich
  • Tokio Tasks: kooperativ, nur bei .await Context Switch

Work Stealing

Tokio verwendet Work Stealing für Lastbalancierung:

  • Jeder Worker-Thread hat eine lokale Task-Queue
  • Wenn ein Thread leer ist, "stiehlt" er Tasks von anderen Queues
  • Reduziert Contention und verbessert Cache-Lokalität

Übung: Parallel fetchen mit join!

🧩 Übung m17-l5-e1

🔮 Vorhersage: Was passiert beim Ausführen?

Editor wird geladen…
KI-Tutor (sokratisch — keine Komplettlösungen)

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 79 von 1150/115 abgeschlossen
Modul 17 — Asynchrone Programmierung

Streams: Asynchrone Sequenzen

Ein Stream ist ein Future, das mehrere Werte über die Zeit liefert.

Das Stream Trait

pub trait Stream {
    type Item;
    fn poll_next(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>
    ) -> Poll<Option<Self::Item>>;
}
  • Ähnlich wie Future, aber liefert Option<Item>
  • None signalisiert Stream-Ende
  • Some(item) liefert den nächsten Wert

Streams erstellen

Aus einer Collection

use futures::stream::{self, StreamExt};

#[tokio::main]
async fn main() {
    let stream = stream::iter(vec![1, 2, 3, 4, 5]);
    
    let sum = stream
        .filter(|x| *x % 2 == 0)  // Nur gerade Zahlen
        .fold(0, |acc, x| acc + x)
        .await;
    
    println!("Summe der geraden Zahlen: {}", sum);
}

Asynchrone Generierung

async fn generate_numbers() -> impl Stream<Item = i32> {
    futures::stream::iter(0..10)
        .map(|i| async move {
            tokio::time::sleep(Duration::from_millis(100)).await;
            i * 2
        })
        .buffer_unordered(5)  // Maximal 5 Futures gleichzeitig
}

Streams verarbeiten

for await Loop

#[tokio::main]
async fn main() {
    let mut stream = generate_data();
    
    while let Some(item) = stream.next().await {
        println!("Received: {}", item);
    }
}

Streams kombinieren

use futures::stream;

#[tokio::main]
async fn main() {
    let stream1 = stream::iter(1..=3);
    let stream2 = stream::iter(4..=6);
    
    // Zwei Streams zusammenführen (abwechselnd)
    let merged = stream::select(stream1, stream2);
    
    // Oder: zuerst stream1, dann stream2
    let chained = stream1.chain(stream2);
}

Backpressure

Streams können Backpressure automatisch handhaben:

  • Wenn der Consumer langsamer ist als der Producer
  • Der Producer wird automatisch verlangsamt
  • Verhindert Speicherüberlauf
// Ohne Backpressure: Producer könnte Consumer überfluten
let stream = stream::iter(0..1000000);

// Mit Backpressure: Producer wartet auf Consumer
let processed = stream
    .map(|x| async move { /* langsame Verarbeitung */ x * 2 })
    .buffer_unordered(100);  // Maximal 100 in Flight

Übung: Stream Processing

🧩 Übung m17-l6-e1

🔮 Vorhersage: Was passiert beim Ausführen?

Editor wird geladen…
KI-Tutor (sokratisch — keine Komplettlösungen)

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 80 von 1150/115 abgeschlossen
Modul 17 — Asynchrone Programmierung

Shared State: Mutex und RwLock in Async

Das Problem: Shared Mutable State in Async

In synchronem Code:

let mut data = vec![1, 2, 3];
// ... kann einfach mutieren

In async Code: Daten könnten von mehreren Tasks gleichzeitig zugreifen → Data Race

Tokio's Async Mutex

use tokio::sync::Mutex;

#[tokio::main]
async fn main() {
    let shared_data = Mutex::new(Vec::new());
    
    let handle1 = tokio::spawn(async move {
        let mut guard = shared_data.lock().await;
        guard.push(1);
    });
    
    let handle2 = tokio::spawn(async move {
        let mut guard = shared_data.lock().await;
        guard.push(2);
    });
    
    handle1.await.unwrap();
    handle2.await.unwrap();
    
    let data = shared_data.lock().await;
    println!("Data: {:?}", *data); // [1, 2] oder [2, 1]
}
  • Mutex::lock() ist async — gibt Thread frei während des Wartens
  • Kein Deadlock möglich (Tokio erkennt es)

RwLock: Mehrere Reader, ein Writer

use tokio::sync::RwLock;

#[tokio::main]
async fn main() {
    let data = RwLock::new(42);
    
    // Mehrere Reader gleichzeitig
    let handle1 = tokio::spawn(async move {
        let guard = data.read().await;
        println!("Reader 1: {}", *guard);
    });
    
    let handle2 = tokio::spawn(async move {
        let guard = data.read().await;
        println!("Reader 2: {}", *guard);
    });
    
    // Exklusiver Writer
    let handle3 = tokio::spawn(async move {
        let mut guard = data.write().await;
        *guard = 100;
    });
    
    handle1.await.unwrap();
    handle2.await.unwrap();
    handle3.await.unwrap();
}

Arc in Async Kontext

use std::sync::Arc;
use tokio::sync::Mutex;

#[tokio::main]
async fn main() {
    // Arc für gemeinsamen Besitz
    let shared = Arc::new(Mutex::new(0));
    
    let shared_clone = Arc::clone(&shared);
    let handle = tokio::spawn(async move {
        let mut guard = shared_clone.lock().await;
        *guard += 1;
    });
    
    handle.await.unwrap();
    
    let guard = shared.lock().await;
    println!("Count: {}", *guard);
}

Arc + Mutex vs Arc + RwLock

  • Arc<Mutex<T>>: Exklusiver Zugriff, gut für häufige Schreiboperationen
  • Arc<RwLock<T>>: Mehrere Leser, ein Schreiber, gut für lese-intensive Workloads

Beide sind async-aware in Tokio.

Übung: Thread-safe Counter mit Mutex

🧩 Übung m17-l7-e1

🔮 Vorhersage: Was passiert beim Ausführen?

Editor wird geladen…
KI-Tutor (sokratisch — keine Komplettlösungen)

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Modul-Checkpoint

80 % bestehen das Modul. Der Versuch zählt — bestehende Fragen werden nicht angezeigt, erst nach dem Absenden gibt es Auflösung und Erklärung.

Checkpoint: Modul 17 — Async

Bestehensgrenze: 80 %. Der Versuch wird bewertet und zählt für das Modul-Gate.

This site uses essential cookies for Stripe payments. No tracking cookies.