Bu sorunun tarihi, Rust 1.36'da std::task::Waker'ın istikrara kavuşmasına kadar uzanıyor; bu, yürütücüler için geleceklere hazır olma bildiriminde bulunmak için standartlaştırılmış bir mekanizma tanıttı. Bunun öncesinde, asenkron çerçeveler kutu içindeki kapanışlara veya özel bildirim özelliklerine dayanıyordu; bu da tahsisat yükü getiriyordu ve C kütüphaneleri ile sorunsuz entegrasyonu engelliyordu. RawWaker API'si, geliştiricilerin ham işaretçilerden ve işlev işaretçi tablolarından (RawWakerVTable) Waker örnekleri oluşturmalarını sağlamak için sıfır maliyetli soyutlamalar destekleyecek şekilde tasarlandı; bu, C++'nın sanal tablolarını yansıtıyor ancak Rust'ın güvenlik gereksinimleriyle uyumlu.
Sorun, RawWaker inşasının Rust'ın sahiplik ve ödünç verme sistemini tamamen atlamasından kaynaklanıyor. Programcı, tüm Waker klonlarının (sadece orijinal değil) yaşam süresi boyunca veri işaretçisinin geçerli kalmasını ve dört vtable işlevinin (clone, wake, wake_by_ref, drop) tek bir iş parçacığı olsa bile Send ve Sync olmasını sağlamak için manuel olarak dört kritik invariyanta dikkat etmelidir. Ayrıca, clone işlevinin aynı temel görev durumunu referans alan yeni bir RawWaker döndürmesi gerekir. Ayrıca, vtable'nın extern "C" ABI kullanması gerekmektedir; bu, FFI uyumluluğunu ve Rust versiyonları boyunca istikrarlı çağrı sözleşmelerini sağlamak için.
Çözüm, unsafe invariyantlarına sıkı bir şekilde uymayı gerektirir. Veri işaretçisi genellikle 'static veriye referans vermeli veya klonlar arasında paylaşılan sahipliği yönetmek için Arc içine sarılmalıdır. Vtable işlevleri, referans sayım semantiklerini doğru bir şekilde uygulamalıdır: clone sayıyı artırmalı, drop azaltmalı ve wake bildirimden sonra azaltmalıdır (Waker'ı tüketerek). ABI sözleşmesini ihlal etmek - örneğin, Rust çağrı sözleşmelerini kullanmak yerine extern "C" kullanmak - yürütücü bu işaretçileri çağırdığında tanımsız davranışa yol açar; bu, yığın bozulması, argüman hizalaması hatası veya geçersiz bellek adreslerine atlama gibi sonuçlar doğurur.
use std::sync::Arc; use std::task::{RawWaker, RawWakerVTable, Waker}; struct TaskState { id: u64, } unsafe fn clone_waker(data: *const ()) -> RawWaker { let arc = Arc::from_raw(data as *const TaskState); let _ = Arc::clone(&arc); let _ = Arc::into_raw(arc); // Düşmekten kaçınmak için sızdır RawWaker::new(data, &VTABLE) } unsafe fn wake_waker(data: *const ()) { let arc = Arc::from_raw(data as *const TaskState); drop(arc); // Referansı serbest bırakarak Arc'ı düşür } unsafe fn wake_by_ref(data: *const ()) { let arc = Arc::from_raw(data as *const TaskState); // Burada uyanma mantığı, ardından geri sızdır let _ = Arc::into_raw(arc); } unsafe fn drop_waker(data: *const ()) { let _ = Arc::from_raw(data as *const TaskState); // İkincil düşüş, belleği serbest bırakır } static VTABLE: RawWakerVTable = RawWakerVTable::new( clone_waker, wake_waker, wake_by_ref, drop_waker, ); fn create_waker(state: Arc<TaskState>) -> Waker { let ptr = Arc::into_raw(state) as *const (); unsafe { Waker::from_raw(RawWaker::new(ptr, &VTABLE)) } }
Düşünün ki yüksek frekanslı bir ticaret sistemi geliştiriyorsunuz ve bir Rust asenkron çalışma zamanı, bir miras C++ piyasa veri akış kütüphanesi ile arayüz oluşturmak zorundasınız. C++ kütüphanesi, fiyat güncellemeleri geldiğinde geri çağrıyı çağıran bir işlev işaretçisi ve void* bağlam alan bir kayıt işlevi sağlar. Mühendislik zorluğu, bu C++ geri çağırma mekanizması ile Rust geleceklere köprü kuran bir Waker oluşturmaktır; çünkü gecikme gereksinimleri alt-mikrosaniyeli uyanma sürelerini talep etmektedir.
Bir çözüm, Waker'ın veri işaretçisinin bir Box<dyn Fn() + Send> kapanışı olarak saklanmasıydı. Bu yaklaşım, Rust'ın sahiplik sistemi aracılığıyla bellek güvenliği sundu, ancak her piyasa verisi aboneliği için kabul edilemez yığın tahsisat gecikması ve sistemin sıfır-kopya mimarisini ihlal eden sanal dağıtım yüküyle sonuçlandı. Ayrıca, kutulu kapanışın yaşam süresini FFI sınırında yönetmek tehlikeli oldu; çünkü C++ kütüphanesinin asenkron temizliği, Rust tarafı Waker'ı bırakmadan önce kalan işaretçileri bırakacak şekilde ifşa edebilirdi.
Alternatif bir yaklaşım, bir tam sayı ID'sini görev işleyicilerine eşleyen küresel bir statik karma tablosu kullanarak void* bağlamdan ID'yi geçirdi. Bu, tahsisatları ortadan kaldırdı ve uyanma işlemleri sırasında O(1) arama sağladı. Ancak, bu, görevlerin beslemeden kaydolmadan tamamlandığı durumda bir bellek sızıntısı tehlikesi yarattı ve statik harita, yüksek piyasa veri akışı altında bir iç engel haline gelen Mutex senkronizasyonunu gerektiriyordu; bu durum, tüm CPU çekirdekleri arasında uyanma bildirimlerini etkili bir şekilde seri hale getiriyordu.
Seçilen çözüm, veri işaretçisinin C++ geri çağırma bağlamını ve bir tamamlama bayrağını içeren Arc<TaskState> tutan özel bir RawWaker uyguladı. RawWakerVTable işlevleri, void*'ı güvenli bir şekilde geri döndürerek Arc işaretçilerine dönüştüren unsafe extern "C" formlarında uygulandı; bu, FFI sınırında uygun referans sayımını sağladı. Bu tasarım, Arc yapısını yeniden kullanarak ileti başına tahsisatları ortadan kaldırdı, Arc'ın atomik işlemleri aracılığıyla iş parçacığı güvenliğini sürdürdü ve son Waker klonunun düşmesini sağladığı sürece bellek güvenliğini garanti altına aldı. Sonuç, Miri'nin tanımsız davranış tespit testlerini ve milyonlarca eşzamanlı fiyat güncellemesi içeren stres testlerini başarıyla geçerken, alt-mikrosaniyeli uyanma gecikmelerini sağladı.
Neden RawWakerVTable işlevleri tek bir işleme yürütücü olsa bile iş parçacığı güvenli olmalıdır (Send + Sync)?
Waker türü Clone, Send ve Sync uygular, böylece yürütücünün iş parçacığı modeli ne olursa olsun, iş parçacığı sınırları arasında geçiş yapabilir. Bir gelecek, Waker içeren bir itme işlevine veya std::sync::mpsc kanalına geçerse, Waker, yaratıldığı iş parçacığından farklı bir iş parçacığından çağrılabilir. Eğer vtable işlevleri tek iş parçacığını varsayıyorsa - örneğin, Rc veya senkronize edilmemiş statik değişken kullanarak - wake() eşzamanlı olarak çağrıldığında veri yarışları yaratır. Ayrıca, Tokio veya async-std gibi asenkron çalışma süreleri, yük dengelemesi için görevleri işçi iş parçacıkları arasında taşıyabilir; bu, Waker'ın, yaratıldığı yerden farklı iş parçacıklarında klonlanması ve bırakılması anlamına gelir. İş parçacığı güvenliği gereksinimi, bildirim mekanizmasının, program içinde Waker'ın nasıl paylaşıldığına bakılmaksızın geçerli olmasını sağlar.
Eğer clone işlevi orijinalinden farklı bir vtable döndürürse ne tür bir felaket başarısızlığı ortaya çıkar?
Waker sözleşmesi, bir Waker'ın tüm klonlarının aynı temel görevi temsil etmesi ve çağrıldığında aynı şekilde davranması gerektiğini gerektirir. Eğer clone, farklı bir göreve veya boş işlev işaretçilerine işaret eden bir RawWaker döndürürse, yürütücü, görevi bilgilendirmek için yanlış uyanma mantığını çağırabilir. Bu, ya alakasız bir görevi uyandırmakla (mantıksal bozulma) ya da geçersiz belleğe atlamakla (segmentation fault) sonuçlanır. Özellikle, yürütücü tipik olarak Waker klonlarını dahili kuyruklarda depolar; bir olay meydana geldiğinde, depolanmış işlemlere wake() çağırır. Uyuşmayan bir vtable, veri işaretçisinin (görev bağlamı) yanlış işlev tanımlarına göre yorumlanmasına neden olur; bu, vtable işlevleri işaretçiyi yanlış bir türe dönüşürmeye çalıştığında veya alanlara yanlış offsetlerde eriştiğinde hemen tanımsız davranışa yol açar.
Neden extern "C" ABI, vtable işlevleri için zorunludur?
RawWakerVTable, FFI uyumluluğunu ve ABI istikrarını garanti etmek için extern "C" işlev işaretçilerini belirtir. Rust ABI'sı, derleyici sürümleri veya optimizasyon seviyeleri arasında istikrarlı değildir; işlev imzaları, derleyici iç nasıl etkileşime gireceğine göre, inlining kararlarına göre veya hedef mimarilere göre değişebilir. extern "C" kullanmak, çağrı sözleşmesinin platformun C standardını izlemesini sağlar, vtable'ı C kodu ile uyumlu hale getirir ve yürütücü wake() veya clone() çağırdığında tanımsız davranış önler. Ayrıca, extern "C" ABI belirli kayıt kullanımı ve yığın temizleme kurallarını zorunlu kılarak Waker'ın dil sınırları arasında güvenli bir şekilde geçmesini sağlar. Bu kısıt olmadan, dinamik kütüphanelere bağlanmak veya Rust derleyicisini yükseltmek, işlev çağrı sözleşmesini değiştirebilir; bu da yürütücünün wake() veya clone() çağırdığında yığın bozulması veya argüman hizalaması hatasına neden olur.