TL;DR: Was ist neu in Rust 1.80 beim kooperativen Scheduling?

  • Verbesserte Mechanismen zur Aufgabenübergabe
  • Bessere Integration mit asynchronen Laufzeiten wie Tokio
  • Erhöhte Fairness bei der Ausführung von Aufgaben
  • Neue APIs für eine feinere Steuerung der Aufgabenplanung

Das Rätsel des kooperativen Schedulings

Bevor wir ins Detail gehen, lassen Sie uns unser Gedächtnis auffrischen, worum es beim kooperativen Scheduling geht. In der asynchronen Welt von Rust geben Aufgaben freiwillig die Kontrolle ab, damit andere Aufgaben ausgeführt werden können. Es ist wie eine Gruppe höflicher britischer Warteschlangen, in der jeder andere vorlässt, wenn er noch nicht bereit ist.

In früheren Versionen von Rust führte diese Höflichkeit jedoch manchmal zu unangenehmen Situationen. Lang andauernde Aufgaben konnten die Aufmerksamkeit auf sich ziehen und andere wichtige Operationen warten lassen. Rust 1.80 tritt auf die Bühne und bringt eine Reihe von Verbesserungen mit, um diesen Tanz eleganter zu gestalten.

Die Neuen im Block: Verbesserte Übergabemechanismen

Rust 1.80 führt ausgefeiltere Übergabemechanismen ein, die es Aufgaben ermöglichen, rücksichtsvollere Nachbarn zu sein. Hier ein kurzer Blick darauf, wie Sie diese neuen Funktionen nutzen können:


use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

struct YieldingTask {
    yielded: bool,
}

impl Future for YieldingTask {
    type Output = ();

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll {
        if !self.yielded {
            self.yielded = true;
            cx.waker().wake_by_ref();
            Poll::Pending
        } else {
            Poll::Ready(())
        }
    }
}

Dieses Beispiel zeigt eine Aufgabe, die einmal übergibt, bevor sie abgeschlossen wird. Die neue Methode wake_by_ref() ist effizienter und vermeidet unnötige Klone des Wakers.

Tokio und Rust 1.80: Ein perfektes Paar in der asynchronen Welt

Wenn Sie Tokio verwenden (und wer tut das nicht?), erwartet Sie eine Freude. Die Verbesserungen von Rust 1.80 passen perfekt zu Tokios Laufzeit. So können Sie diese Synergie nutzen:


use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let task1 = tokio::spawn(async {
        for i in 1..=5 {
            println!("Task 1: {}", i);
            sleep(Duration::from_millis(100)).await;
        }
    });

    let task2 = tokio::spawn(async {
        for i in 1..=5 {
            println!("Task 2: {}", i);
            sleep(Duration::from_millis(100)).await;
        }
    });

    let _ = tokio::join!(task1, task2);
}

Dieses Beispiel zeigt, wie Tokios Laufzeit jetzt noch besser mit dem kooperativen Scheduling von Rust 1.80 harmoniert und eine faire Ausführung zwischen Aufgaben gewährleistet.

Fairness: Nicht nur für Streitigkeiten auf dem Spielplatz

Eines der herausragenden Merkmale von Rust 1.80 ist die verbesserte Fairness bei der Ausführung von Aufgaben. Keine Aufgaben-Mobber mehr, die die gesamte CPU-Zeit beanspruchen! Die Laufzeit verteilt die Ressourcen jetzt besser unter den Aufgaben, was für Microservices unter hoher Last entscheidend ist.

Betrachten Sie dieses Szenario:


use tokio::time::{sleep, Duration};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};

#[tokio::main]
async fn main() {
    let counter = Arc::new(AtomicUsize::new(0));
    
    let tasks: Vec<_> = (0..100).map(|i| {
        let counter = Arc::clone(&counter);
        tokio::spawn(async move {
            loop {
                counter.fetch_add(1, Ordering::SeqCst);
                if i % 10 == 0 {
                    sleep(Duration::from_millis(1)).await;
                }
            }
        })
    }).collect();

    sleep(Duration::from_secs(5)).await;

    for task in tasks {
        task.abort();
    }

    println!("Total increments: {}", counter.load(Ordering::SeqCst));
}

In diesem Beispiel erstellen wir 100 Aufgaben, die jeweils einen gemeinsamen Zähler inkrementieren. Einige Aufgaben (jede 10.) schlafen kurz, um I/O-Operationen zu simulieren. Mit der verbesserten Fairness von Rust 1.80 werden Sie eine ausgewogenere Verteilung der Inkremente über die Aufgaben hinweg bemerken, selbst unter dieser künstlichen Last.

Feinsteuerung: Ihre neue Superkraft

Rust 1.80 gibt Ihnen mit neuen APIs mehr Kontrolle über die Aufgabenplanung. Es ist, als hätten Sie einen Zauberstab für Ihren asynchronen Code. Hier ein Vorgeschmack darauf, was Sie tun können:


use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

struct ControlledYield {
    yields_left: usize,
}

impl Future for ControlledYield {
    type Output = ();

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll {
        if self.yields_left > 0 {
            self.yields_left -= 1;
            cx.waker().wake_by_ref();
            Poll::Pending
        } else {
            Poll::Ready(())
        }
    }
}

async fn controlled_task(yields: usize) {
    ControlledYield { yields_left: yields }.await;
    println!("Task completed after {} yields", yields);
}

Dieses ControlledYield-Future ermöglicht es Ihnen, genau anzugeben, wie oft eine Aufgabe übergeben werden soll, bevor sie abgeschlossen wird. Es ist, als hätten Sie einen präzisen Steuerknopf für das kooperative Verhalten jeder Aufgabe.

Die Fallstricke: Vorsicht!

Obwohl die Verbesserungen des kooperativen Schedulings in Rust 1.80 großartig sind, sind sie kein Allheilmittel. Hier sind einige Fallstricke, auf die Sie achten sollten:

  • Übermäßiges Übergeben kann zu unnötigen Kontextwechseln und reduzierter Leistung führen.
  • Unzureichendes Übergeben bei CPU-intensiven Aufgaben kann immer noch zu Latenzspitzen führen.
  • Zu starkes Vertrauen in die Fairness der Laufzeit kann zugrunde liegende Designprobleme in Ihrer Microservices-Architektur verschleiern.

Alles zusammenfügen: Ein praxisnahes Szenario

Schauen wir uns ein realistischeres Beispiel an, wie diese Verbesserungen in einem Microservice unter hoher Last angewendet werden können:


use tokio::time::{sleep, Duration};
use std::sync::Arc;
use tokio::sync::Semaphore;

async fn process_request(id: u32, semaphore: Arc) {
    let _permit = semaphore.acquire().await.unwrap();
    println!("Processing request {}", id);
    // Simulate some work
    sleep(Duration::from_millis(100)).await;
    println!("Completed request {}", id);
}

#[tokio::main]
async fn main() {
    let semaphore = Arc::new(Semaphore::new(10)); // Begrenzung der gleichzeitigen Verarbeitung
    let mut handles = vec![];

    for i in 0..1000 {
        let sem = Arc::clone(&semaphore);
        handles.push(tokio::spawn(async move {
            process_request(i, sem).await;
        }));
    }

    for handle in handles {
        handle.await.unwrap();
    }
}

In diesem Beispiel simulieren wir einen Microservice, der 1000 Anfragen gleichzeitig verarbeitet, aber die tatsächliche gleichzeitige Verarbeitung auf 10 gleichzeitig begrenzt, indem wir ein Semaphor verwenden. Die verbesserte kooperative Planung von Rust 1.80 stellt sicher, dass selbst unter dieser hohen Last jede Aufgabe eine faire Chance auf Ausführung erhält und verhindert, dass eine einzelne Anfrage die Ressourcen monopolisiert.

Die Quintessenz: Den kooperativen Geist annehmen

Die Verbesserungen des kooperativen Schedulings in Rust 1.80 sind ein Wendepunkt für Microservices, die unter hoher Last arbeiten. Durch die Nutzung dieser Verbesserungen können Sie:

  • Latenzspitzen reduzieren, indem Sie eine faire Aufgabenverteilung sicherstellen
  • Die Gesamtreaktionsfähigkeit des Systems verbessern
  • Ihren asynchronen Code für optimale Leistung feinabstimmen
  • Robustere Microservices entwickeln, die Verkehrsspitzen elegant bewältigen können

Denken Sie daran, dass der Schlüssel zum Beherrschen dieser neuen Funktionen in der Praxis und im Experimentieren liegt. Scheuen Sie sich nicht, einzutauchen und zu sehen, wie sie Ihre Microservices-Architektur transformieren können.

Denkanstoß

"In der Welt der Microservices ist Kooperation nicht nur nett zu haben – sie ist überlebenswichtig."

Wenn Sie diese neuen Muster des kooperativen Schedulings implementieren, fragen Sie sich:

  • Wie kann ich Engpässe in meinen aktuellen Microservices identifizieren, die von verbessertem Scheduling profitieren könnten?
  • Welche Metriken sollte ich überwachen, um sicherzustellen, dass ich das Beste aus diesen neuen Funktionen heraushole?
  • Wie kann ich mein Team über diese Verbesserungen aufklären und Best Practices in der asynchronen Rust-Entwicklung fördern?

Indem Sie diese Fragen kontinuierlich stellen und die Möglichkeiten von Rust 1.80 erkunden, sind Sie auf dem besten Weg, Microservices zu entwickeln, die nicht nur unter Druck überleben, sondern gedeihen.

Gehen Sie nun voran und kooperieren Sie wie nie zuvor! Ihre Microservices (und Ihre Benutzer) werden es Ihnen danken.