Kısıtlama, Rust'ın senkronerden asenkron modelere evrimi ile ilgili. async/await Rust 1.39'da kararlileştirildiğinde, dil, Future türlerinin işçi havuzlarında hareket edebilmesi için Send olması gerektiği zorunluluğunu getirdi. std::sync::Mutex asenkron ekosistemden önce ortaya çıkmış ve kilit mülkiyetini belirli çekirdek thread'lerine bağlayan işletim sistemi yerel ilkelere, örneğin pthread_mutex_t'ye sarılır. Çünkü MutexGuard, thread'e özel senkronizasyon durumuna bir işaretçi içerir; bunu bir iş çalma yürütücüsü aracılığıyla başka bir iş parçacığına taşımak, sistem düzeyinde güvenlik garantilerini ihlal eder, bu da serbest bırakma sırasında tanımsız davranışa neden olabilir. Sonuç olarak, derleyici MutexGuard'ın !Send olmasını ve çok iş parçacıklı asenkron bağlamlarda await noktalarında varlığını yasaklamasını zorunlu kılar ve bu da veri yarışlarını ve sistem düzeyindeki bozulan durumu önler.
Bir Rust uygulamasında yüksek verimli bir web hizmeti geliştiriyorduk. Axum ve Tokio kullanarak bir işleyici, bir dış doğrulama hizmetine asenkron HTTP isteği yaparken paylaşılan bir bellek içi önbelleği güncellemeye ihtiyaç duydu. İlk uygulama, doğrulama verilerini alırken std::sync::Mutex koruyucusunu bir await noktasında tutmaya çalıştı. Bu, Future'ın Send uygulamadığını belirten karmaşık bir hata iletisi ile derlemeyi hemen başarısız kıldı. Hata, özellikle MutexGuard'ın güvenli bir şekilde iş parçacıkları arasında gönderilemeyeceğini vurguladı, bu da senkron kilitleme ilkerleri ile asenkron yürütme modelleri arasında temel bir çelişkiyi ortaya koydu.
İlk seçenek, kritik bölümü yeniden yapılandırmak ve tüm senkron önbellek okumalarını önce gerçekleştirmek, ardından herhangi bir await öncesinde MutexGuard'ı açık bir şekilde serbest bırakmak ve ardından veriler zaten çıkarıldıktan sonra asenkron G/Ç gerçekleştirmekti. Bu yaklaşım, kilit içerme süresini nanosenye kadar düşürerek kilit çatışmalarını en aza indirdi ve asenkron çalışma zamanının değerli iş parçacıklarını engellemesini önledi, ancak doğrulama mantığının, dış çağrı sırasında önbelleğe değişken erişim gerektirmediğinden emin olmak için dikkatli bir yeniden yapılandırma gerektirdi. Bu, işletim sistemi düzeyindeki mutex ilkelere uyum sağlar ve iş çalma yürütücülerinin Send gereksinimlerine sıkı bir şekilde uyuldu.
İkinci çözüm, std::sync::Mutex yerine tokio::sync::Mutex kullanmayı önermiştir. Bu, bekleme noktalarında tutulmak üzere özel olarak tasarlanmıştır, çünkü koruyucu Send'i G/Ç görev zamanlayıcısı ile koordine ederek uygular. Bu, işlemlerin sırasını değiştirmek zorunda kalmadan, orijinal kod yapısının korunmasına izin verse de, kısa bir bellek güncellemesi için önemli bir ek yük getirdi ve doğrulama hizmeti yavaş yanıt verirse asenkron aç kalma riski oluşturdu, çünkü kilit üzerinde bekleyen tüm görevler verilecekti. Ayrıca, kritik bölümlerin asenkron kodda kısa tutulması ilkesini ihlal etti ve yüksek çoklu görev altında genel sistem verimliliğini düşürme potansiyeline sahipti.
Üçüncü seçenek, sisteme synchron kullanıcı işlemlerinin tamamını kapsayan spawn_blocking kullanarak tüm senkron mutex işlemini, G/Ç dahil, etkili bir şekilde, engelleyici mantığı asenkron çalışma zamanının olay döngüsünden çıkarmaktır. Ancak, bu yaklaşım, ağ isteği süresince engelleyici havuzdan değerli bir OS iş parçacığı tüketecekti ve asenkron programlamanın ölçeklenebilirlik faydalarını ortadan kaldırıyordu, bu da yüksek yük altında iş parçacığı havuzunu tükenmesine yol açma potansiyeline sahipti. Engelleyici soyutlama ile dış HTTP çağrısının temel olarak engellenmeyen doğası arasında bir anlamsal uyumsuzluk temsil ediyordu.
Sonuçta, await'den önce koruyucuyu bırakmak için ilk çözümü seçtik. Çünkü bu, mutex'in yalnızca kısa bellek mutasyonunu koruduğunu ve uzun ağ işlemine bağlı olmadığını doğru bir şekilde modelleyen kaynak yaşam döngüsünü sağladı. Bu karar, sistem verimliliğini ve doğruluğu kod kolaylığından önde tutarak, std::sync::Mutex'ın rekabetsiz erişim için asenkron karşıtına göre çok daha hızlı olduğunu göz önünde bulundurdu. Derlemeyle zamanlama bürosunu kaçınarak, derleme zamanında güvenliği garanti eden, Rust'ın sıfır maliyetli soyutlama felsefesi ile uyumlu hale geldi.
Ortaya çıkan uygulama, Send sınırları sağlanarak sorunsuz bir şekilde derlendi, önbellek kilidi ile yavaş dış hizmetler arasında potansiyel ölü kilitleri ortadan kaldırdı, ağ G/Ç sırasında diğer görevlerin önbelleğe erişmesine izin vererek yük üzerindeki istek gecikmesini artırdı. Benchmarklar, tokio::sync::Mutex yaklaşımına kıyasla algılanan gecikmede %40'lık bir azalmayı gösterdi, bu da Send ve await noktaları arasındaki etkileşimi anlamanın yüksek performanslı asenkron Rust hizmetleri için kritik olduğunu doğruladı. Düzeltme, altyapı yeteneklerinin farkında olmanın, hem derleme hatalarını hem de çalışma zamanı verimsizliklerini önlediğini gösterdi.
Derleyici hatası özellikle Future'ın Send olmadığını, neden MutexGuard'ın await boyunca tutulamayacağını belirtmiyor?
Hata, Send sınır başarısı olarak ortaya çıkar çünkü Tokio'nun spawn yöntemi (ve çoklu iş parçacıkları yürütücüleri) F: Future + Send + 'static gerektirir. Future durum makinesi bir MutexGuard içerdiğinde, derleyici, oluşturulan yapının Send'ini kanıtlamaya çalışır ama başarısız olur çünkü MutexGuard !Send uygular. Teşhis zinciri bunu, std::sync::MutexGuard'ın Send gereksinimini karşılamadığını ortaya çıkararak Future'a kadar devam eder. Başlangıçta yer alanlar, async bloklarının anonim yapılar haline dönüştüğünü ve await noktaları boyunca yaşayan tüm yerel değişkenlerin bu yapının alanları haline geldiğini، ayrıca diğer iş parçacığı verilerine yönelik aynı özellik sınırlarına tabi olduklarını sıklıkla gözden kaçırır.
std::sync::Mutex ile kapsamlı koruyucular kullanmak ile aynı kritik bölge için tokio::sync::Mutex kullanmak arasındaki kritik performans farkı nedir?
std::sync::Mutex, parçalanmış zamanlarda iş parçacıklarını park eden işletim sistemi uygun futex ilkelerini kullanır ve bu, nanosecond ölçeğindeki gecikmelerde, çok ağır taşınmalıdır. tokio::sync::Mutex ise tamamen kullanıcı alanında atomik işlemler ve görev kuyruklaması aracılığıyla çalışır; bu, iş parçacıklarının engellenmesini önler, ancak Future anketi ve yürütme zamanlayıcısı ile koordinasyon nedeniyle önemli ölçüde yüksek temel bir yük getirmektedir. Adaylar genellikle, uzun await işlemleri (veritabanı sorguları gibi) sırasında bir tokio::sync::Mutex koruyucusu tutmanın, o mutex'i bekleyen tüm diğer görevleri diziselleştirdiğini gözden kaçırıyor. Buna karşılık std::sync::Mutex, await noktalarını dışarıda tutacak şekilde düzgün bir şekilde kapsamlıdır, böylece diğer iş parçacıkları, asenkron G/Ç süresine bakılmaksızın, kilit alanı sona erdikten hemen sonra ilerleyebilir.
Future trait'in Pin sözleşmesi, MutexGuard'ın Drop uygulamasıyla, kendine referans veren asenkron durum makineleri düşünüldüğünde, nasıl etkileşir?
Bir Future anket edildiğinde, kendine referans veren yapılar için bellekte sabitlenir. MutexGuard kendine referans vermez, ancak bir işletim sistemi ile thread'e özgü bir sözleşmenin tanığıdır. Eğer Future bellek içinde taşınırsa (ki Pin, ancak Send'in iş parçacıkları arasında geçiş yapmasına izin vermesi), MutexGuard bellek adresi açısından geçerli kalır, ancak thread bağlılığı açısından geçersiz olur. Daha kritik olarak, eğer asenkron görev, bir await noktasında koruyucuyu tutarken iptal edilirse (silinirse), Drop, mevcut olan herhangi bir thread bağlamında çalışır ve bunun kilitli thread ile eşleşmesi gerekir. Adaylar genellikle Send ve Pin'in ortogonal kısıtlamalar olduğunu anlamayı gözden kaçırır: Pin anket sırasında bellek hareketini engellerken, Send anketler arasında thread geçişine izin verir ve MutexGuard ikincisini ihlal eder ancak birincisini değil, iptal güvenliği ve thread güvenliği arasındaki ince bir ayrım oluşturur.