Rust, her kompozit tür için elle kanıt sağlama ergonomik yükünü çözmek amacıyla Send ve Sync gibi auto traits (otomatik özellikler) tanıtır. Tarihsel olarak, sistem programcıları her yapı için karmaşık eşzamanlılık sözleşmeleri ile not almak zorundaydılar, bu hata yapmaya açık ve ayrıntılı bir süreçti. Derleyici, yalnızca tüm bileşen alanlarının bu özellikleri uygulaması durumunda aggrekat türler (yapılar, enumlar, demetler) için otomatik olarak bu özellikleri uygular.
Sorun, ham işaretçiler (*const T ve *mut T) ile ortaya çıkar. Referanslar veya akıllı işaretçilerden farklı olarak, ham işaretçiler üzerine derleyicinin doğrulama yapabileceği herhangi bir sahiplik veya takas anlamsal taşımaz. Eşzamanlı yerine getirilen saklama, tahsis edilmemiş bellek veya dış senkronizasyon yoluyla yönetilen paylaşılan değişken duruma işaret edebilirler. Öyleyse sadece T temelinde ham işaretçilere Send veya Sync uygulanması bellek güvenliğini ihlal eder, çünkü derleyici işaretçinin iş parçacığı sınırları boyunca doğru bir şekilde kullanıldığını garanti edemez.
Çözüm, türetim mantığını ikiye ayırır. Aggregatlar için, derleyici yapısal rekürsiyon yapar: her alanı kontrol eder. Ham işaretçiler için ise, derleyici bu uygulamaları açıkça saklar ve onları opak, potansiyel olarak tehlikeli tutacak yapılar olarak ele alır. Bu, geliştiricileri unsafe impl Send veya unsafe impl Sync kullanmaya zorlayarak, derleyicinin çıkaramayacağı görevlerini yerine getirmekle kişisel sorumluluk almaya iter.
use std::ptr::NonNull; // Bir aggrekat tür struct Container<T> { data: Vec<T>, // Vec<T> Send ise T Send'dir index: usize, } // Container<T>, T: Send ise otomatik olarak Send'dir // Ham işaretçi içeren bir tür struct Node<T> { value: T, next: *mut Node<T>, // Ham işaretçi otomatik türetimi bozar } // Açık opt-in gereklidir unsafe impl<T: Send> Send for Node<T> {} unsafe impl<T: Sync> Sync for Node<T> {}
Yüksek frekanslı ticaret uygulaması için sıfır tahsis, kilitsiz MPMC (çok üretici, çok tüketici) halka tamponu geliştirirken, düğümlerin önceden tahsis edilmiş bir dizide bulunması gerekiyordu, böylece jemalloc rekabetine girmemek için. Node yapısı yükü ve bir *mut Node<T> sonraki işaretçisi içeren bir müdahaleli bağlı liste oluşturuyordu. Tampon tutma nesnesini bir iş parçacığına göndermeye çalıştığımda, derleyici Send uygulaması olmadığından kodu reddetti, oysa düğümlerin yalnızca atomik karşılaştırma ve değiştirme işlemleriyle erişildiğini biliyordum.
Üç çözümü değerlendirdim. İlk olarak, ham işaretçiyi Box<Node<T>> ile değiştirmek. Bu, Box'ın yığın sahipliği anlamına geldiği ve bireysel tahsisler yaptığı için reddedildi; bu durum, önbellek dostu halka tamponunu parçalayarak HFT'de kabul edilemeyecek bir tahsis gecikmesi getirdi. İkincisi, AtomicPtr içinde sarılmış NonNull<Node<T>> kullanmaktı. AtomicPtr kendisi Send ise, T Send, içindeki Node yapısı hala otomatik türetimde başarısız oldu; çünkü NonNull (ham işaretçi etrafında bir sarıcı) içindeki ham işaretçi yapısal kontrolün engeliydi. Üçüncüsü, unsafe impl blokları kullanarak Send ve Sync'i elle uygulamaktı.
Tüm erişimlerin ayrı bir durum indeksinde SeqCst atomik işlemleriyle korunduğunu resmi olarak doğruladıktan sonra, üçüncü yaklaşımı seçtim; bu, veri yarışıyla sonuçlanmalarını önleyen happens-before ilişkilerini sağladı. Bu çözüm, kilitsiz, sıfır tahsis mimarisini korurken Rust'ın tür sistemini de karşıladı. Sonuç, kilit maliyetini azaltarak saniyede milyonlarca olayı işleyebilen üretim kalitesinde bir kuyruk oldu, ancak gelecekteki bakımcılar için kapsamlı SAFETY yorumları gerektirdi.
Neden bir Send türüne ham bir işaretçi otomatik olarak Send'i uygulamaz?
Adaylar sıklıkla Send'in tüm alanlar boyunca "geçişli" olduğunu varsayıyorlar, ham işaretçiler de dahil. Ham işaretçilerin, içsel sahiplik anlamları taşımayan temel türler olduğuna dikkat etmiyorlar. Derleyici, iş parçacığına özel depolama işaretçisini ve paylaşılan yığın bellekteki bir işaretçiyi ayırt edemez, ayrıca takas kurallarını doğrulayamaz. Sonuç olarak, *const T ve *mut T hiç zaman Send veya Sync'i otomatik olarak uygulamaz, T'den bağımsız olarak, programcıyı işaretçinin eşzamanlılık sözleşmesinden sorumlu olacak şekilde unsafe impl kullanmaya zorlar.
Güvenli olmayan iç alanlar içeren genel bir yapı için Send'i koşullu olarak nasıl uygularım?
Birçok geliştirici unsafe impl'nin koşulsuz olması gerektiğini varsayıyor. Gerçekte, unsafe impl<T> Send for MyType<T> where T: Send + 'static {} yazabilirsiniz. Bu, içerikleri Send olduğunda yalnızca Send olmasının gerektiği genel konteynerler (örneğin, özel bir UnsafeCell sarmalayıcısı) için esastır. Adaylar, unsafe impl içindeki where ifadesinin, genel kodun eşzamanlılık kısıtlamalarının düzgün bir şekilde yayılmasını sağladığını, uygulamayı aşırı kısıtlamadan koruyacak kadar ifadede benzerlik sunduğunu gözden kaçırıyorlar.
Ham işaretçilere sahip bir türde Send ile Sync'i uygularken güvenlik gereksinimlerini ne ayırır?
Send, değerin sahipliğinin iş parçacığı sınırları arasında güvenli bir biçimde aktarılmasını gerektirir. Bir ham işaretçi için, bu genellikle işaretçinin değeri güvenli bir şekilde aktarılabilir. Ancak Sync, iş parçacıkları arasında değişmez referansların (&Self) güvenli bir şekilde paylaşılmasını gerektirir. Eğer &Node, ham işaretçi değerini açığa çıkarıyorsa (ki bu, derefere edilebilir), ve başka bir iş parçacığı, işaretçiye mutable bir referans aracılığıyla değişiklik yapıyorsa, bu bir veri yarışı oluşturur. Dolayısıyla, ham işaretçi içeren türler için Sync uygulamaları neredeyse her zaman senkronize erişimin kanıtını gerektirir (örneğin, işaretçi yalnızca bir Mutex altında veya atomik işlemlerle erişilerek kullanıldığında), oysa Send yalnızca tekil sahiplik aktarımını kanıtlamak zorundadır.