Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Async İçin Trait’lere Daha Yakından Bakmak

Bölüm boyunca Future, Stream ve StreamExt trait’lerini farklı biçimlerde kullandık. Çoğu günlük Rust kodu için bunların ayrıntılarına derinlemesine girmeniz gerekmez. Ama bazen Pin türü ve Unpin trait’i ile birlikte bu trait’lerin bazı ayrıntılarını anlamanız gerekir. Bu bölümde, tam da o tür senaryolarda işinize yarayacak kadar derine ineceğiz.

Future Trait’i

Önce Future trait’ine yakından bakalım. Rust onu kabaca şöyle tanımlar:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {
    type Output;

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

Bu tanımda birkaç yeni tür ve yeni sözdizimi var. Önce parçaları ayıralım.

İlk olarak Output, future tamamlandığında hangi değerin üretileceğini söyler. Bu, Iterator trait’indeki Item ile benzer rol oynar. İkinci olarak Future, özel imzalı bir poll metoduna sahiptir. Bu metod self için Pin<&mut Self> alır, ayrıca Context türüne değiştirilebilir referans bekler ve Poll<Self::Output> döndürür.

Şimdilik Pin ve Context ayrıntılarını bir kenara bırakalım; dönüş türü olan Poll ile başlayalım:

#![allow(unused)]
fn main() {
pub enum Poll<T> {
    Ready(T),
    Pending,
}
}

Poll, Option’a biraz benzer: değer taşıyan bir varyantı vardır (Ready(T)) ve değer taşımayan bir varyantı vardır (Pending). Ama anlamı başkadır. Pending, future’ın hâlâ işi olduğunu ve daha sonra yeniden yoklanması gerektiğini söyler. Ready ise future’ın işini bitirdiğini ve sonucun hazır olduğunu belirtir.

await kullanan kod gördüğünüzde, Rust bunu perde arkasında poll çağrılarına çevirir. Örneğin 17-4 numaralı listedeki “sayfa başlığını al” örneği, kaba bir fikir vermesi açısından aşağıdakine benzer bir koda dönüşür:

match sayfa_basligi(url).poll() {
    Ready(baslik) => match baslik {
        Some(title) => println!("{url} için başlık {title} idi"),
        None => println!("{url} için başlık yoktu"),
    },
    Pending => {
        // Burada daha sonra yeniden denemek gerekir
    }
}

Eğer future hâlâ Pending ise onu yeniden yoklamamız gerekir. Ama bunu sonsuz döngüyle körlemesine yapmak istemeyiz; aksi halde await bloklayıcı hale gelir. Bunun yerine Rust, future hazır değilse denetimi çalışma zamanına geri verecek şekilde kod üretir. Çalışma zamanı da daha sonra uygun olduğunda future’ı yeniden poll eder.

17-2 numaralı bölümde alici.recv() çağrısını beklediğimizi görmüştük. recv bir future döndürür. Çalışma zamanı, poll sonucu Pending ise future’ın hazır olmadığını anlar; Ready(Some(message)) ya da Ready(None) döndüğünde ise ilerleyebileceğini bilir.

Pin Türü ve Unpin Trait’i

17-13 numaralı listede trpl::join! ile üç future’ı beklemiştik. Ama bazen çalışma zamanında sayısı belli olacak bir future koleksiyonuyla uğraşırız. 17-23 numaralı listedeki kod, üç future’ı bir vektöre koyup trpl::join_all çağırmayı deniyor; ama bu haliyle derlenmiyor.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::block_on(async {
        let (gonderici, mut alici) = trpl::channel();

        let gonderici1 = gonderici.clone();
        let gonderici1_gelecegi = async move {
            let degerler = vec![
                String::from("merhaba"),
                String::from("-den"),
                String::from("gelecek"),
                String::from("icinden"),
            ];

            for deger in degerler {
                gonderici1.send(deger).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        };

        let alici_gelecegi = async {
            while let Some(value) = alici.recv().await {
                println!("alındı '{value}'");
            }
        };

        let gonderici_gelecegi = async move {
            // --snip--
            let degerler = vec![
                String::from("daha"),
                String::from("fazla"),
                String::from("mesaj"),
                String::from("sana"),
            ];

            for deger in degerler {
                gonderici.send(deger).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        };

        let gelecekler: Vec<Box<dyn Future<Output = ()>>> =
            vec![Box::new(gonderici1_gelecegi), Box::new(alici_gelecegi), Box::new(gonderici_gelecegi)];

        trpl::join_all(gelecekler).await;
    });
}
Listing 17-23: Bir koleksiyon içindeki future’ları beklemek

Her future’ı Box içine koyuyoruz; böylece onları trait nesnesi haline getiriyoruz. Bunun faydası şu: async blokların her biri farklı adsız türler üretse de, hepsi Future<Output = ()> uyguladığı için hepsini aynı koleksiyona koyabiliyoruz.

Yine de kod derlenmiyor. Hata mesajının özeti şu:

error[E0277]: `dyn Future<Output = ()>` cannot be unpinned
...
= note: consider using the `pin!` macro

Mesaj bize pin! makrosunu kullanmamızı söylüyor. Yani değerleri Pin içine alarak bellekte taşınamayacaklarını garanti etmemiz gerekiyor. Çünkü burada dyn Future<Output = ()> türü Unpin uygulamıyor.

Bu ilk bakışta garip görünebilir. Doğrudan await ederken böyle bir şey gerekmemişti. Sebebi şu: await, future’ı örtük biçimde pin’ler. Ama burada future’ı doğrudan beklemiyoruz; önce onları başka bir future oluşturan join_all içine veriyoruz. join_all ise koleksiyon içindeki her öğenin uygun future olmasını bekliyor ve Box<T> ancak T, gerektiğinde pinlenip güvenle taşınabiliyorsa bu şartı sağlayabiliyor.

Future trait’indeki poll metoduna yeniden bakarsanız sebebi görünür:

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;

Future’ı poll etmek için self üzerinde Pin<&mut Self> gerekiyor. Yani future’ın bellekteki yerinin sabit olduğuna dair garanti verilmesi lazım.

Bu neden gerekli? Çünkü bölüm boyunca söylediğimiz gibi, await noktaları derleme sırasında bir durum makinesine dönüşür. Derleyici, bir await noktasından ötekine kadar hangi verilerin yaşaması gerektiğine bakar ve buna göre varyantlar üretir. Bazı async bloklarda oluşan bu durum makinesi, kendi içinde kendisine referanslar taşıyabilir. Böyle türler kendine referanslı yapılar olur.

Kendine referanslı bir yapıyı bellek içinde taşımak tehlikelidir. Çünkü içindeki referanslar eski adrese bakmaya devam eder. Bu da geçersiz referanslara ve çok zor hatalara yol açabilir. Pin, tam burada devreye girer: bir değeri Pin ile sardığınızda, o değerin artık bellekte taşınmayacağını garanti edersiniz.

Önemli nokta şu: Pin<Box<SomeType>> yazdığınızda aslında sabitlenen şey Box işaretçisinin kendisi değil, işaret ettiği SomeType değeridir. Box yine taşınabilir; ama içindeki veri taşınmaz. Bize gereken güvence de tam olarak budur.

Peki her tür için böyle bir sabitleme gerekli midir? Hayır. Sayılar, bool değerleri, çoğu Vec ve günlük Rust türü kendi içine referans taşımaz; bunları taşımak güvenlidir. İşte Unpin trait’i tam burada devreye girer.

Unpin, 16. bölümde gördüğümüz Send ve Sync gibi bir işaretleyici trait’tir. Kendi başına davranış taşımaz. Yalnızca, “Bu tür için pinleme güvencesi özel olarak korunmak zorunda değil; gerektiğinde taşınabilir” bilgisini derleyiciye iletir.

Derleyici, güvenli olduğunu kanıtlayabildiği türler için Unpin’i otomatik olarak uygular. Yani Unpin normal durumdur; !Unpin ise özel durumdur. Örneğin String, Pin içine alınabilir; ama zaten Unpin olduğu için taşınması güvenlidir. Buna karşılık async blokların ürettiği bazı future’lar Unpin olmayabilir.

Bu yüzden 17-23 numaralı listedeki future’ları açıkça pinlememiz gerekir. 17-24 numaralı liste, her future tanımlandığı yerde pin! kullanarak sorunu çözer.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::pin::{Pin, pin};

// --snip--

use std::time::Duration;

fn main() {
    trpl::block_on(async {
        let (gonderici, mut alici) = trpl::channel();

        let gonderici1 = gonderici.clone();
        let gonderici1_gelecegi = pin!(async move {
            // --snip--
            let degerler = vec![
                String::from("merhaba"),
                String::from("-den"),
                String::from("gelecek"),
                String::from("icinden"),
            ];

            for deger in degerler {
                gonderici1.send(deger).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        });

        let alici_gelecegi = pin!(async {
            // --snip--
            while let Some(value) = alici.recv().await {
                println!("alındı '{value}'");
            }
        });

        let gonderici_gelecegi = pin!(async move {
            // --snip--
            let degerler = vec![
                String::from("daha"),
                String::from("fazla"),
                String::from("mesaj"),
                String::from("sana"),
            ];

            for deger in degerler {
                gonderici.send(deger).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        });

        let gelecekler: Vec<Pin<&mut dyn Future<Output = ()>>> =
            vec![gonderici1_gelecegi, alici_gelecegi, gonderici_gelecegi];

        trpl::join_all(gelecekler).await;
    });
}
Listing 17-24: Future’ları vektöre taşıyabilmek için pinlemek

Bu sürüm artık derlenir ve çalışır. Future’ları çalışma zamanında vektöre ekleyip çıkarabilir, sonra da hepsini birlikte bekleyebiliriz.

Pin ve Unpin, günlük uygulama kodundan çok daha alt seviyeli kütüphaneler veya çalışma zamanı yazarken önemlidir. Ama hata mesajlarında karşınıza çıktıklarında, artık neye işaret ettiklerini daha iyi biliyorsunuz.

Stream Trait’i

Artık Future, Pin ve Unpin hakkında biraz daha derin fikir sahibi olduğumuza göre, şimdi Stream trait’ine dönelim. Bölümün önceki kısımlarında gördüğünüz gibi akışlar, eşzamansız yineleyicilere benzer. Ama Iterator ile Futureun aksine, bu yazı yazılırken Stream standart kütüphanede yer almıyor; ekosistemde en yaygın kullanılan tanım futures crate’inden geliyor.

Iterator ile Future tanımlarını bir araya getirerek akışı şöyle düşünebiliriz:

  • Iterator::next, Option<Self::Item> üretir.
  • Future::poll, Poll<Self::Output> üretir.

Zaman içinde hazır hale gelen bir öğe dizisini ifade etmek için bunları birleştiren bir Stream trait’i tanımlarız:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

trait Stream {
    type Item;

    fn poll_next(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>
    ) -> Poll<Option<Self::Item>>;
}
}

Stream trait’indeki Item, akışın ürettiği öğelerin türünü belirtir. Bu yönü ile Iteratora benzer; çünkü sıfır ya da daha fazla öğe üretebilir. Futuredan ayrıldığı nokta da budur; Future her zaman tek bir Output üretir.

poll_next metodunun dönüş türü olarak Poll<Option<_>> kullanması da çok anlamlıdır. Dıştaki Poll, öğenin henüz hazır olup olmadığını; içteki Option ise daha fazla öğe kalıp kalmadığını gösterir.

Bölümde akışlarla çalışırken doğrudan poll_next çağırmadık. Onun yerine next metodunu ve StreamExt trait’ini kullandık. Çünkü StreamExt, Stream üstüne daha rahat API’ler ekler. İstersek poll_next çağrılarıyla elle durum makinesi de yazabilirdik; ama await ile çalışmak çok daha rahattır.

StreamExt içindeki next metodunun bir örneği şöyledir:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

trait Stream {
    type Item;
    fn poll_next(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
    ) -> Poll<Option<Self::Item>>;
}

trait StreamExt: Stream {
    async fn next(&mut self) -> Option<Self::Item>
    where
        Self: Unpin;

    // diger metotlar...
}
}

StreamExt, akışlarla kullanılabilecek ilginç yardımcı metodların yuvasıdır. Ekosistemdeki çoğu tür için, bir tür Stream uygularsa StreamExt de otomatik olarak onun için erişilebilir olur. Bu sayede temel trait sabit kalırken kullanışlı yardımcı API’ler topluluk tarafından daha hızlı geliştirilebilir.

Kullandığımız trpl sürümünde StreamExt, yalnızca next metodunu tanımlamakla kalmaz; aynı zamanda Stream::poll_next ayrıntılarını doğru biçimde ele alan bir varsayılan uygulama da sunar. Bu da şu anlama gelir: kendi akış türünüzü yazmanız gerekirse yalnızca Stream trait’ini uygulamanız yeterlidir. Sonra o türü kullanan herkes, StreamExt metodlarını otomatik olarak kullanabilir.

Bu trait’lerin alt seviye ayrıntıları için şimdilik bu kadar yeterli. Bölümü kapatmadan önce future’ların, görevlerin ve iş parçacıklarının nasıl birlikte oturduğunu son kez bir araya getirelim.