Tarihsel olarak, Rust Rc'yi (referans sayımı) tek iş parçacıklı senaryolar için performansa duyarlı bir alternatif olarak Arc'ye (atomik referans sayımı) geliştirmiştir. Dilin erken sürümleri bu ayrımı içermediği için tüm paylaşımlı sahiplik atomik işlemlerinin maliyetini ödemek zorundaydı. Send ve Sync otomatik özellikleri, iş parçacığı güvenliğini parçalar halinde zorunlu kılmak için tasarlandı, böylece derleyici bu özellikleri bir türün bileşenlerine dayanarak otomatik olarak türetebiliyordu.
Temel sorun, Rc'nin iç uygulamasında yatıyor; bu, aktif referansları takip etmek için atomik olmayan bir sayaç (tipik olarak Cell<usize> veya UnsafeCell<usize> içinde sarılmış) kullanmaktadır. Bu tasarım, bellek bariyerlerinin maliyetini önlemek için tek iş parçacıklı erişimi varsayıyor. Eğer Rc<T> Send uygulamasına izin verilirse, bir program bir işaretçinin kopyasını farklı bir iş parçacığına taşıyabilir. Yeni iş parçacığında yıkım veya kopyalama sırasında, her iki iş parçacığı da referans sayısı üzerinde senkronize edilmemiş okuma-değiştirme-yazma işlemleri yapar. Bu bir veri yarışı oluşturur, sayının bozulmasına yol açabilir, erken bellek serbest bırakma (use-after-free) veya bellek sızıntılarına (double-free) neden olabilir.
Çözüm mimari bir nitelikte: Rc, iş parçacığı güvenli olmayan türler içerecek şekilde Send ve Sync'den açıkça vazgeçiyor (veya modern Rust'de negatif uygulamalar aracılığıyla). Bu, geliştiricilerin, sayaçları için AtomicUsize kullanan Arc<T> kullanmasını zorunlu kılarak, artış ve azaltma işlemlerinin atomik olmasını ve tüm CPU çekirdekleri arasında doğru bir şekilde sıralanmasını sağlar. Derleyici, tür düzeyinde bu ayrımı zorunlu kılarak, zaman kontrolü olmadan istem dışı paylaşımı önler.
Büyük bir belgeyi Soyut Sözdizim Ağaçına (AST) ayıran yüksek performanslı bir metin düzenleyicisini düşünün. Parsere, ağaç boyunca paylaşılan alt dizgileri temsil etmek için Rc<Node> kullanır (örneğin, aynı tanımlayıcılar), tek iş parçacıklı parse aşamasında bellek optimizasyonu yapar. Semantic doğrulamayı paralelleştirme gereksinimi ortaya çıkıyor; alt ağaçları bir iş parçacığı havuzuna dağıtarak.
Hemen ortaya çıkan sorun, Rc<Node>’yi işçi iş parçacıklarına göndermeye çalıştığınızda derlemenin başarısız olmasıdır. Çeşitli çözümler değerlendirildi:
Arc ile global değiştirme: Tüm Rc örneklerini Arc ile değiştirmek. Artıları: Minimum kod değişikliği ve anında iş parçacığı güvenliği. Eksileri: Profil oluşturma, parselamada gereksiz atomik işlemler nedeniyle %12-15 oranında bir performans düşüşü gösterdi, bu da performans bütçelerini ihlal etti.
İletim için derin kopyalama: Alt ağaçları Vec<u8> olarak seri hale getirip, baytları gönderip ve işçilerde yeniden seri hale getirme. Artıları: Güvensiz kod veya mimari değişiklikler yok. Eksileri: Karmaşık grafik yapılarının iç döngüleri ile marshalling yapmanın yüksek gecikmesi ve CPU maliyeti, gerçek zamanlı düzenleme için engelleyici hale getiriyor.
Güvenli işaretçi çıkarımı: Rc'yi ham bir işaretçiye dönüştürmek, işaretçiyi göndermek ve alıcıda Rc'yi yeniden inşa etmek. Artıları: Sıfır kopyalama maliyeti. Eksileri: Temel olarak sağlam değil; Rc’nin sahiplik değişmezini ihlal ediyor (alıcı iş parçacığı, gönderen iş parçacığın kopyalarını düşürüp düşürmediğini bilemez), kaçınılmaz olarak bellek bozulmasına veya boş işaretçilere neden olur.
Kanal tabanlı görev gönderimi: AST'yi ana iş parçacığında tutup, hafif doğrulama görevlerini (bayt aralıkları veya düğüm indeksleri) crossbeam kanallarıyla göndermek. İşçiler sonuç döner, Rc'yi yöneten belleğe dokunmazlar. Artıları: Rc'nin parselama için performansını korur, veri yarışlarını unsafe olmadan ortadan kaldırır ve bileşenleri ayrıştırır. Eksileri: Doğrulama algoritmasının veri-paralel yapıdan görev-paralel yapıya yeniden yapılandırılmasını gerektirir.
Ekip, kanal tabanlı yaklaşımı seçti. Parsere, tek iş parçacıklı ve hızlı kaldı, doğrulama çekirdek sayısıyla doğrusal bir şekilde ölçeklendi. Sonuç, unsafe blokları olmayan ve performans özelliklerini koruyan kararlı bir sistemdi.
Neden Rc<T> Sync değil, sarılı T türü Sync olsa bile ve bu, Send kısıtlamasından nasıl farklıdır?
Rc<T>, değişmez referanslar (&Rc<T>) .clone() çağrısına izin verdiği için Sync olamaz; bu, iç atomik olmayan referans sayısını değiştirmektedir. T kendisi paylaşmak için güvenli olsa bile (Sync), iş parçacıkları arasında Rc sarmalayıcısının paylaşılması, birden fazla iş parçacığından sayaçta eşzamanlı artışlara izin vererek bir veri yarışı yaratabilir. Send kısıtlaması, sahipliği tamamen başka bir iş parçacığına taşımayı engellerken, Sync kısıtlaması bu durumda referansların bile iş parçacıkları arasında paylaşılmasını engeller. Rc, "salt okunur" işlemleri (kopyalama) aslında içsel bir değişiklik gerçekleştirdiğinden her iki ilkeye de aykırıdır.
PhantomData<T>, ham bir işaretçiyi (const T) saran özel bir yapının Send ve Sync için otomatik türetilmesini nasıl etkiler ve dahil edilmesi neden kritik öneme sahiptir?*
PhantomData olmadan, *const T içeren bir yapı, otomatik özellik türetimi açısından T ile bağlantılı herhangi bir tür bilgisini taşımaz. Derleyici, işaretçinin sarktığını, rastgele olarak eşleştiğini veya iş parçacığı-yerel verilere işaret ettiğini varsayarak Send veya Sync’yi türetmeyi reddeder. PhantomData<T> ekleyerek, geliştirici derleyiciye yapının mantıksal olarak bir T'ye sahip olduğunu işaret eder. Sonuç olarak, yapı otomatik olarak Send uyguluyorsa T: Send ve Sync uyguluyorsa T: Sync, FFI sarmalayıcıları veya özel akıllı işaretçiler için gerekli olan bileşen iş parçacığı güvenliğini geri kazandırır.
Bir trait nesnesi Box<dyn Trait> ne zaman Send otomatik özelliğini kaybeder, oysa altında yatan somut tür Send uyguluyorsa?
Bir trait nesnesi dyn Trait yalnızca trait tanımı Send’i üst sınıf olarak açıkça gerektiriyorsa Send uygular (örneğin, trait Trait: Send). Somut türün bir trait nesnesine silinmesi sırasında, derleyici tüm spesifik tür bilgilerinin yanı sıra otomatik özellik uygulamalarını da atar. Trait kendisi Send olmasını garanti etmediği sürece, derleyici vtable'nin iş parçacığı güvenli yöntemlere işaret ettiğini doğrulayamaz. Bu, box'lanmış trait nesnelerinin iş parçacığı sınırlarında gönderilmesini engeller, yalnızca trait sınırı açıkça Send (ve Sync) içeriyorsa, nesne güvenliğini iş parçacığı güvenli uygulamalarla sınırlı hale getirir.