async-trait kütüphanesi, async fn yöntemlerini Pin<Box<dyn Future<Output = T> + Send + 'static>> döndüren senkron yöntemlere dönüştürmek için prosedürel bir makro kullanır. Bu dönüşüm, async bloğu tarafından üretilen somut gelecek türünü silerek, bir vtable aracılığıyla dinamik dispatch yapmayı mümkün kılar ve trait'in nesne güvenliğini korur. Spesifik çalışma zamanı maliyeti, geleceği depolamak için her yöntem çağrısında Box için bir yığın tahsisi ve dyn trait nesne dispatch'i ile ilgili dolaylı işlev çağrısı aşamasını içerir. Ayrıca, 'static sınırı, geleceğin statik olmayan verileri ödünç almasını engeller, böylece tüm yakalanan referansların sahipliğine sahip ya da 'static ömrüne sahip olmalarını zorunlu kılar.
Mühendislik ekibimiz, dinamik bağlantı işleyicileri yükleme için bir eklenti mimarisine ihtiyaç duyan yüksek performanslı bir TCP sunucusu inşa ediyordu. I/O işlemlerini işlemek için async fn handle(&mut self, stream: TcpStream) içeren bir ConnectionHandler trait'ine ihtiyacımız vardı, ancak Rust 1.70 sürümü trait'lerde yerel async fn’i desteklemiyordu.
async fn yerine impl Future dönüş türleri ile genel trait'ler kullanmak, yığın tahsisi olmadan sıfır maliyetli bir soyutlama sundu ve monomorfizasyon aracılığıyla agresif derleyici optimizasyonları sağladı. Ancak, bu yaklaşım temel olarak dinamik dispatch'i engelleyerek Vec<Box<dyn ConnectionHandler>> içinde heterojen işleyicileri depolamayı ya da çalışma zamanında paylaşılan kütüphanelerden dinamik olarak yüklemeyi imkânsız hale getiriyordu, bu da eklenti mimarimizin özüydü.
async-trait kütüphanesini benimsemek, yerel async fn ile aynı olan temiz bir sözdizimi sağlarken, Box<dyn ConnectionHandler> aracılığıyla dinamik dispatch'i destekliyordu. Ana dezavantaj, geleceği kutulamak için zorunlu olan yöntem başına yığın tahsisi ile birlikte 'static ömür gereksinimiydi, bu da await noktaları arasında statik olmayan verileri ödünç almayı engelleyerek ek veri kopyalamalarını zorlayabilirdi.
Makroyu kullanmadan, Pin<Box<dyn Future>> döndürerek trait’i manuel olarak uygulamak, Send sınırları üzerinde tam kontrol sağladı ve prosedürel makro derleme zamanı aşamasını ortadan kaldırdı. Ne yazık ki, bu, son derece uzun bir tekrarlama gerektirdi, Pin::new_unchecked kullanarak manuel unsafe pinleme işlemleri gerektiriyordu ve await noktaları arasında karmaşık ömür kısıtlamalarıyla başa çıkarken son derece hata yapmaya açıktı, bu da geliştirme hızını önemli ölçüde yavaşlattı.
Sonuç olarak, yığın tahsis yükü kabul edilebilir olduğu için async-trait kütüphanesini çözüm olarak seçtik, çünkü sunucu esasen I/O-baskın olduğundan CPU-baskın değildi ve ergonomik yararları geliştirme hızını önemli ölçüde hızlandırdı. Eklenti sistemi, modüllerin yeniden derleme olmaksızın sıcak değiştirilmesini sağlayarak Box<dyn ConnectionHandler> ile sorunsuz çalıştı, bu da mimari gereksinimlerimizi karşıladı.
Kod tabanını Rust 1.75'e geçirdikten sonra, dinamik dispatch gerektirmeyen yerlerde native async fn ile sistematik olarak async-trait yerine koyduk, aynı temiz API yüzeyini korurken çağrı başına yığın tahsisini ortadan kaldırdık. Performans profilleme, kutulama yükü eski sürümde mevcut olsa da, bunun ağ gecikmeleriyle karşılaştırıldığında önemsiz olduğunu doğruladı, bu da başlangıçtaki teknik kararımızı geçerli kıldı.
'static sınırı neden gereklidir ve bu kısıtlama await noktaları arasında ödünç almayı nasıl etkiler?**
'static sınırı, async-trait geleceği Box<dyn Future + Send + 'static> içerisine silindiğinden doğar ve Rust’taki trait nesnelerinin, tüm olası yürütme bağlamlarını kapsayacak bir tanımlı yaşam süresine sahip olması gerekir. Yürütücü, geleceği, iş parçacığı sınırları arasında sonsuz süre tutuyor veya iç kuyruklarda saklıyorsa, derleyici, geleceğin tüm yakalanan verilerinin sahipliğine sahip olmasını veya yalnızca 'static referansları tutmasını gerektirir. Bu da, böyle referansların yığın çerçevesine bağlı olan, 'static olmayan ömürlere sahip olacağı için, yığın yerel değişkenlerin await noktaları arasında ödünç alınmasını engeller. Adaylar genellikle bunun, trait nesneleri için tür silme ile ilgili temel bir kısıtlama olduğunu ve yalnızca kütüphane yazarlarının keyfi bir kısıtlama koymadığını gözden kaçırırlar.
Pin<Box<dyn Future>> dönüş türü, çok iş parçacıklı yürütücülerde Send gereksinimi ile nasıl etkileşime girer ve temel gelecek Send değilse hangi derleme hatası ortaya çıkar?**
async-trait, kutulanmış geleceğe (Pin<Box<dyn Future + Send + 'static>>) Send sınırlarını otomatik olarak ekleyerek, görevlerin yürütme sırasında iş parçacıkları arasında hareket edebileceği iş çalma yürütücüleri, örneğin Tokio ile uyumlu olmasını sağlar. Geleceğin Send olabilmesi için, async bloğu tarafından yakalanan tüm verilerin Send uygulaması gerekir. Eğer gelecek, Rc veya ham işaretçiler gibi Send olmayan türleri yakalarsa, derleyici, geleceğin güvenli bir şekilde iş parçacıkları arasında iletilemediğini belirten bir hata mesajı üretir çünkü !Send içerir. Adaylar sıklıkla Send sınırının, çok iş parçacıklı bağlamlarda iş parçacığı güvenliği için gerekli olduğunu ve async-trait'in bu sınırı varsayılan olarak koyduğunu gözden kaçırırlar, hatta yürütücü teorik olarak tek iş parçacıklı olsa bile.
Native async fn ile async-trait emülasyonu arasında nesne güvenliği ve dinamik dispatch ile ilgili temel mimari ayrım nedir?
Native async fn içinde trait'ler, her uygulama için opak bir impl Future türü döndüren Return Position Impl Trait In Traits (RPITIT) kullanır. Bu yaklaşım sıfır maliyetlidir ve monomorfizasyon yoluyla statik olarak dağıtılır, ancak impl Trait vtable girişi için gerekli somut türü gizlediğinden trait'in nesne güvenliğini ihlal eder. Dolayısıyla, native async fn ile Box<dyn Trait> oluşturamazsınız, eğer döndürmeleri manuel olarak Box<dyn Future>> içine sarmadıysanız. Buna karşın, async-trait, geleceği hemen Pin<Box<dyn Future>> içerisine kutulayarak nesne güvenliğini sağlamakta, bu ise bilinen bir boyuta sahip olup bir vtable'da depolanabilir, dinamik dispatch'i yığın tahsis maliyeti ile mümkün kılmaktadır. Adaylar sıklıkla iki yaklaşımı karıştırarak, native async fn'nin otomatik olarak Box<dyn Trait> desteklediğini veya async-trait'in yalnızca sözdizimsel şeker olduğunu varsayıyorlar; bu, nesne güvenliği ve tahsis stratejisi ile ilgili mimari farklılıklar olmaksızın.