Sorunun Tarihçesi
Rust’ın tür sistemi yaşam sürelerini “erken bağlı” veya “geç bağlı” olarak kategorize eder. Erken bağlı yaşam süreleri, tanım veya uygulama noktasında çözülür ve öğenin varlığı süresince somut ve sabit hale gelir. HRTB'deki for<'a> söz dizimiyle tanıtılan geç bağlı yaşam süreleri, gerçek kullanım noktasına kadar polimorfik kalır ve bir işlev veya özellik sınırının herhangi bir olası yaşam süresi üzerinde tekdüze çalışmasına izin verir. Bu ayrım, geri çağırmaları veya kendisi borç alınan verileri manipüle eden closures'ı kabul eden gerçek üst düzey işlevleri destekleme ihtiyacından doğmuştur—bu, çağırıcıyı her çağrıda tek bir belirli yaşam süresine zorlamadan yapılır.
Sorun
Bir üst düzey işlev, fn process<'a, F: Fn(&'a Data)>(f: F) gibi imzasında açık bir yaşam süresi parametresi tanımladığında, yaşam süresi 'a erken bağlı hale gelir. Bu, derleyicinin çağrı noktasındaki bağlama göre belirli bir yaşam süresi 'a seçmesi anlamına gelir ve closure türü F, yalnızca o belirli 'a için Fn(&'a Data) koşulunu sağlamalıdır. Sonuç olarak, closure, sonraki çağrılarda farklı yaşam sürelerine sahip verilerle tekrar kullanılamaz ve onu yaşam süresinin daha kısa veya daha uzun olduğu bir bağlama geçirmeye çalışmak, bir yaşam süresi uyuşmazlığı hatasına yol açar. Bu sınırlama, geçici borçları işlemek zorunda olan esnek, yeniden kullanılabilir soyutlamaların (örneğin, thread havuzları veya olay dağıtıcıları) oluşturulmasını etkili bir şekilde engeller.
Çözüm
HRTB, yaşam süresi parametresini özellik sınırının kendisine alarak bunu çözer: fn process<F: for<'a> Fn(&'a Data)>(f: F). Burada, for<'a> ifadesi, tür F'nin her olası yaşam süresi 'a için özellik uygulanmasını garanti eder. Bu, yaşam süresini geç bağlı hale getirir; derleyici, closure'ın evrensel olarak polimorfik olduğunu kontrol eder ve bu, onu işlev gövdesindeki her ayrık çağrı noktasında herhangi bir yaşam süresine sahip referanslar kabul etmesine olanak tanır. Bu mekanizma, geri çağırmanın depolanmasını verinin ömründen ayırarak, çeşitli yürütme bağlamlarında ödünç alınan verileri güvenli bir şekilde işleyen sıfır maliyetli soyutlamalar sağlar.
// Erken bağlı: 'a çağrı noktasında sabitlenir, esnekliği sınırlar fn bad_process<'a, F>(f: F) where F: Fn(&'a str) -> usize, { let local = String::from("temp"); // HATA: local, erken bağlı 'a kadar yaşamaz // f(&local); } // Geç bağlı: HRTB, 'a'nın her çağrıda herhangi bir yaşam süresi olmasına izin verir fn good_process<F>(f: F) where F: for<'a> Fn(&'a str) -> usize, { let local = String::from("temp"); // TAMAM: 'a, bu çağrı için yalnızca &local'ın yaşam süresi olarak oluşturulur println!("{}", f(&local)); } fn main() { let count_fn = |s: &str| s.len(); good_process(count_fn); }
Sorun Tanımı
Yüksek frekanslı bir ticaret motoru için sıfır kopyalı bir olay dağıtım sistemi tasarlarken, ekip bir strateji işleyici kaydı ihtiyaç duydu. Bu işleyiciler, sahipliği almadan piyasa verisi paketlerini inceleyen closure'lardı ve mikro saniye düzeyinde işleme yapıyordu. Merkez dağıtıcı, bu işleyicileri HashMap<String, Box<dyn Handler>> içinde saklamak ve gelen ağ tamponlarının geçici görünüşleri ile çağırmak zorundaydı. Zorluk, ağ tamponlarının son derece kısa, kapsam bağlı yaşam sürelerine sahip olmasıydı; oysa dağıtıcı uzun ömürlü bir singleton'dı. Eğer işleyici özelliği belirli bir yaşam süresine bağlı olursa, dağıtıcı bu yaşam süresi parametresini gerektirecek ve küresel durumda saklamak veya farklı ticaret oturumları arasında hayatta kalmak mümkün olmayacaktı.
Çözüm A: Yaşam Süresi Parametrelemeli Statik Dağıtım
Bir yaklaşım, dağıtıcıyı 'a üzerinde genel yapmak ve Box<dyn Handler<'a>> depolamaktı. Bu, tüm dağıtıcı yapısının yaşam süresi 'a taşımayı gerektirecekti, bu da onu ağ tamponunun kapsamına bağlı kısa ömürlü bir nesne haline getirecekti. Artıları sıfır maliyetli soyutlamalar ve çalışma zamanı yükü olmamasıydı. Ancak, mimari engeller vardı: dağıtıcı lazy_static! içinde saklanamaz veya bağımsız yaşam sürelerine sahip diğer iş parçacıklarına gönderilemezdi ve bu, oturum yönetim mantığının tamamen yeniden tasarlanmasını zorunlu kılmaktaydı.
Çözüm B: Erased Lifetimes ile 'static Sınırları
Başka bir seçenek, işleyicilere geçirilen tüm verilerin 'static olması gerektiğini talep etmek veya işleyicilerin sahip olunan verileri almasını (örn., Vec<u8>) zorlamak oldu. Bu, işleyicilerin Box<dyn Handler + 'static> olarak saklanmasına izin verdi. Artıları basitlik ve saklama kolaylığıydı. Ancak, her ağ paketinin 'static hale getirilmesi veya sahip olunan durum kazandırmak için bir tahsis ve kopyalama gerektirdiğinden ciddi performans cezaları vardı; bu, mikro saniye gecikme gereksinimlerini yok ediyor ve yüksek verim sırasında bellek baskısını artırıyordu.
Çözüm C: Üst Dereceli Özellik Sınırları (HRTB)
Seçilen çözüm, işleyici özelliğini HRTB kullanarak tanımladı: trait Handler { fn handle(&self, data: &Packet); } F: for<'a> Fn(&'a Packet) olarak uygulandı. Bu, Box<dyn Handler> depolamaya (herhangi bir yaşam süresi için çalışacağına dair söz verdiğinden dolaylı olarak 'static) ve handle çağrısı sırasında ağ tamponlarının geçici borçlarını geçirmeye izin verdi. Artıları sıfır kopyalı performansı korumak ve işleyicileri uzun ömürlü, küresel durumda saklamak oldu. Ancak, artan karmaşıklık ve işleyicilerin for<'a> sözleşmesini ihlal eden çevresinden referansları yanlışlıkla yakalamamasını sağlama ihtiyacı gibi bazı dezavantajları vardı.
Sonuç
Ticaret motoru, paket verileri için tahsis yapmadan saniyede milyonlarca olayı başarıyla işledi. HRTB tabanlı mimari, ekibin farklı modüllerden işleyicileri karıştırıp eşleştirmesine olanak tanıdı—bazılarını yığın, diğerlerini iş parçacığına özgü arenalardan ödünç alarak—derleyici, hiçbir işleyicinin eriştiği geçici verilerden daha uzun süre yaşamayacağını garanti etti, bu da veri yarışlarını ve kullanımdan sonra serbest bırakma sorunlarını önledi.
Neden Box<dyn Fn(&'a T)> içindeki yaşam süresi parametresini içeren yapıyı zorlar, oysa Box<dyn for<'a> Fn(&'a T)> zorlamaz?
İlk durumda, yaşam süresi 'a, özellik nesnesinin kendisinin somut bir tür parametresidir. dyn Fn(&'a T) türü, dolaylı olarak 'a sınırını taşır, bu da özellik nesnesinin yalnızca o özel yaşam süresi için geçerli olduğu anlamına gelir. Bu nedenle, bunu içeren herhangi bir yapı, closure'ın yakalayabileceği veya kabul edebileceği referanslardan daha uzun yaşamayacağını kanıtlamak için <'a> bildirmelidir. for<'a> ile, özellik nesnesi closure'ın tüm yaşam süreleri için çalıştığını iddia eder, bu da içerik türünün 'a üzerindeki belirli bağımlılığı siler. Bu, yapının 'static olmasına olanak tanır; çünkü evrensel geçerlilik vaadi taşır, belirli bir borcu ilişkilendirmez.
HRTB ile, borç alınan girdi referanslarını döndürmeye çalışan closures arasındaki etkileşimler nelerdir?
Adaylar genellikle F: for<'a> Fn(&'a T) -> &'a U yazmaya çalışıyor ve çıktının yaşam süresinin girdiyle eşleşmesini bekliyor. Ancak, standart Fn özelliğinin ilişkili türü Output, 'a üzerinde genel değildir; closure türü için sabittir. Bu nedenle, HRTB yalnızca Fn özellikleri ailesindeki giriş argümanıyla bağlı yaşam süresine sahip bir dönüş türünü ifade edemez. Bunu başarmak için, HRTB ile birleşik Genel İlişkili Türler (GAT) kullanmak gerekiyor; özel bir özellik tanımlamak gerekecek: trait Processor { type Output<'a>; fn process<'a>(&self, input: &'a T) -> Self::Output<'a>; }. Bu sınırlamanın farkında olmadan, adaylar genellikle “dönüş türü, yeterince uzun yaşamıyor” şeklinde derleyici hataları ile mücadele ederler ve HRTB'nin standart closures'taki dönüş yaşam süresi sorununu çözeceğini yanlışlıkla düşünürler.
Fonksiyon üzerindeki bir erken bağlı yaşam süresi ile, bir özellik sınırındaki geç bağlı yaşam süresi arasındaki monomorfizasyon açısından temel farklılık nedir?
Bir işlev kendi yaşam süresini tanımladığında, fn foo<'a, F: Fn(&'a T)> gibi, yaşam süresi 'a erken bağlıdır. Çağrı noktasındaki monomorfizasyon veya tür kontrolü sırasında, derleyici bu belirli çağrı için tüm kısıtlamaları karşılayan tek bir belirli 'a seçer. F bu somut 'a ile kontrol edilir. Bununla birlikte, fn foo<F: for<'a> Fn(&'a T)> ile, derleyici F'nin tüm olası yaşam süreleri için sınırı karşıladığını kontrol eder. Bu, foo içinde, closure'ı birden fazla kez farklı yaşam sürelerine sahip argümanlarla çağırabileceğiniz anlamına gelir; oysa erken bağlı versiyonda, foo içindeki tüm çağrılar, foo çağrıldığında seçilen tek 'a ile sınırlı kalır. Adaylar genellikle, işlevlerdeki erken bağlı yaşam sürelerinin o çağrı için "derleme zamanı sabitleri" gibi davrandığını, oysa HRTB'deki geç bağlı yaşam sürelerinin "herhangi bir instantiation için geçerli evrensel olarak nicelendirilen değişkenler" gibi davrandığını atlarlar.