A Rust hatheti release-ciklusa idén is pontosan araszol tovább: 2023. január 26-án jelent meg a Rust 1.67, alig három nappal azután, hogy a legtöbbünk visszaszokott a munkarendbe az ünnepek után. Ez a kiadás nem hoz látványos, marketingszempontból "sexy" nyelvi feature-t, de két olyan apró finomítást tartalmaz, ami a mindennapi kódolás minőségét emeli — pont az a fajta változás, amiért a Rust csapatot szeretjük.

Megjegyzés

Ha kimaradtál az elmúlt hónapok híreiből: a Rust már stabilan tudja a GAT-okat (Generic Associated Types) és a let-else szintaxist is, szóval ha még nem próbáltad ki őket, ideje bepótolni.

#[must_use] async fn-eken

Eddig, ha egy sima függvény #[must_use] attribútummal volt megjelölve, a fordító szólt, ha az eredményét figyelmen kívül hagytad. Ez klasszikusan a Result-nál fájt a legjobban — senki nem akarja véletlenül elnyelni egy hibát.

A gond az volt, hogy async fn esetén ez az attribútum korábban nem viselkedett konzisztensen: az async fn valójában egy impl Future-t ad vissza, és ha ezt a Future-t soha nem .await-eled, akkor egyszerűen semmi nem történik — a kód lefordul, csendben, és a hívó azt hiheti, hogy lezajlott valami, miközben a jövő sosem futott le.

A Rust 1.67-től kezdve az async fn-ekre tett #[must_use] megbízhatóan figyelmeztet, ha a visszaadott Future-t nem használod:

#[must_use]
async fn fetch_config() -> Result<String, std::io::Error> {
    // képzeljük el, hogy ez tényleg beolvas valamit
    Ok("config-adat".to_string())
}

async fn hasznal() {
    fetch_config(); // figyelmeztetést kapunk: a Future eredménye nincs felhasználva

    let _ = fetch_config().await; // ez rendben van
}

Ha lefuttatod ezt cargo build-del, a fordító szépen rászól, hogy a fetch_config() hívás eredménye (a Future) figyelmen kívül lett hagyva. Ez különösen hasznos olyan async kódnál, ahol könnyű elfelejteni egy .await-et — típusosan copy-paste hiba, vagy amikor egy async lezáró (closure) belsejéből hiányzik egy sor.

Tipp

Ha saját async API-t írsz és fontos, hogy a hívó ténylegesen lefuttassa a visszaadott Future-t (pl. egy tranzakciót commitoló async metódusnál), tedd rá bátran a #[must_use]-t — mostantól garantáltan működik.

Újraírt mpsc csatorna implementáció

A másik nagy belső változás a std::sync::mpsc modult érinti. A többtermelős, egyfogyasztós (multi-producer, single-consumer) csatorna a Rust egyik legrégebbi, legtöbbet használt konkurencia-eszköze, ugyanakkor az eredeti implementációja évek óta cipelt magával néhány kellemetlen, jól dokumentált hibát — például a recv_timeout időnként pontatlanul viselkedett, és voltak edge case-ek, amik race condition-höz vezethettek szélsőséges terhelés alatt.

A Rust 1.67-ben ezt a belső implementációt teljesen újraírták, egy modernebb, jobban tesztelt algoritmusra alapozva. A jó hír, hogy ez teljesen transzparens változás: a publikus API (channel(), sync_channel(), Sender, Receiver) egy karakternyit sem változott, viszont a viselkedés korrektebb és a teljesítmény is jobb lett, különösen sok termelő esetén.

use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();

    for i in 0..4 {
        let tx = tx.clone();
        thread::spawn(move || {
            tx.send(format!("üzenet a(z) {i}. szálról")).unwrap();
        });
    }

    drop(tx); // az eredeti sender-t is el kell engedni

    while let Ok(msg) = rx.recv_timeout(Duration::from_millis(500)) {
        println!("{msg}");
    }
}

Ez a kód 1.67 alatt fordul és fut, és most már a recv_timeout is megbízhatóbban viselkedik pontos időzítés mellett, mint a korábbi verziókban.

Figyelem

Ha korábban valamilyen "trükközéssel" kerülted meg a régi mpsc implementáció ismert hibáit (pl. saját timeout-logikát írtál helyette), most érdemes visszatérni a standard megoldásra — kevesebb kód, kevesebb hiba.

Mit jelent ez a gyakorlatban?

Ha async kóddal dolgozol — akár saját executor-ral, akár egy külső runtime-mal, mint a tokio —, az #[must_use] async fn-eken azonnal segít elkapni azokat a hibákat, ahol egy Future-t elindítasz, de elfelejted .await-elni. Ez a fajta hiba különösen alattomos, mert a kód lefordul és futni is látszik, csak épp a várt mellékhatás nem történik meg.

use std::sync::mpsc;

#[must_use]
async fn kuld_uzenetet(tx: mpsc::Sender<i32>, ertek: i32) {
    tx.send(ertek).unwrap();
}

A mpsc újraírás pedig azoknak jó hír, akik konkurenciakritikus, sok szálas rendszereket építenek: a standard könyvtár csatornája most már megbízhatóbb alap, nem kell azon agyalni, hogy inkább egy külső crate-et (pl. crossbeam-channel) használj-e helyette "biztonság kedvéért".

Összességében ez a release pont azt a Rust-filozófiát testesíti meg, hogy a nyelv és a standard könyvtár folyamatosan finomodik anélkül, hogy a meglévő kódodat eltörné — a channel() API ugyanaz maradt, csak a motorháztető alatt lett jobb.

Hogyan frissíts?

A frissítés a megszokott módon, egyetlen paranccsal elintézhető:

rustup update stable

Ezután érdemes ellenőrizni, hogy tényleg az 1.67-es verziót futtatod-e:

rustc --version
# rustc 1.67.0 (fc594f156 2023-01-24)

Ha CI-ban rögzített toolchain verziót használsz (pl. rust-toolchain.toml fájlban), ne felejtsd el frissíteni azt is, különben a fejlesztői gépeden más viselkedést kapsz, mint a build szerveren.

Jó tudni

A jelenlegi edition továbbra is a 2021-es — az 1.67 nem hoz edition-váltást, szóval a Cargo.toml-odban nincs teendőd ezen a fronton.

Összefoglalás

A Rust 1.67 nem forradalmi kiadás, de két olyan javítást hoz, amik közvetlenül a mindennapi kód minőségét emelik: az async fn-ek #[must_use] figyelmeztetése segít elkerülni a néma, el nem indított Future-öket, az újraírt mpsc csatorna pedig szilárdabb alapot ad a standard könyvtári konkurenciakezeléshez. Ha eddig halogattad a frissítést, most van itt az ideje egy gyors rustup update-nek — a régi kódod garantáltan tovább fog fordulni, csak jobban fog viselkedni.