Sorunun cevabı.
Rust standart kütüphanesi thread::scope işlevini 1.63 sürümünde, thread::spawn işlevinin 'static kapalı alanlara ihtiyaç duymasının sınırlamasını aşmak için tanıttı. Tarihsel olarak, geliştiriciler crossbeam gibi kütüphanelere başvurarak kapsamalı eşzamanlılık elde etmeyi denediler ve bunun 'static sınırlamalar olmaksızın güvenli bir şekilde iş parçaları arasında ödünç almanın mümkün olduğunu gösterdi. Temel sorun, bir iş parçacığı, başvurduğu veriyi içeren yığın çerçevesini aşarsa, verinin geçersiz hale gelmesi ve bu durumun kullanımdan sonra serbest bırakma (use-after-free) zayıflıklarına yol açmasıdır.
Çözüm, başlatılan tüm iş parçacıklarının kapsamanın bitiminden önce tamamlandığından emin olmak için ömür alt türlemesi ve bırakma sırası garantileri kullanmaktadır. thread::scope işlevi, ödünç alınan çevre ile bağlı bir 'env ömrü olan bir Scope tutamacı alan bir kapalı alanı kabul eder; başlatılan iş parçacıkları, 'env'den daha kısa olan bir 'scope ömrüne sahiptir. Scope uygulaması, dahili olarak tüm ScopedJoinHandle örneklerini izler ve kapsama işlevi döndürülmeden önce otomatik olarak bunları birleştirir, böylece hiç bir iş parçacığı, veri serbest bırakıldıktan sonra ona erişemez.
use std::thread; fn parallel_sum(data: &[i32]) -> i32 { let mut sum = 0; thread::scope(|s| { let handle = s.spawn(|| { data.iter().sum::<i32>() }); sum = handle.join().unwrap(); }); sum }
Hayattan bir durum.
Bir veri işleme hattı, işçi iş parçacıkları için veriyi yığın üzerinden kopyalamadan gigabayt boyutundaki diziler üzerinde istatistiksel analiz yapması gerekti. Mühendislik ekibi başlangıçta paralel yineleme için rayon kullanmayı denedi ancak belirli özel toplama mantığı, iş parçacığı ayrımına ilişkin ince ayar gerektiren yönetim gerektirdi. Sorun, girdi dilimlerinin yığın üzerinde yerleştirilmiş geçici görünümler olmasıydı, bu da 'static sınırlamalarını sağlamayı imkansız hale getiriyordu.
Bir yaklaşım, veriyi sahiplikli Vec parçalarına ayırmak ve bunları başlatılan iş parçacıklarına taşımak, ancak bu 40% bellek yüküne neden oldu ve tahsis tıkanıklığından önemli gecikmelere yol açtı. Başka bir öneri, veriyi uzun ömürlü işçi iş parçacıklarına aktarmak için mpsc kanallarıyla mesaj geçişini kullandı, ancak bu senkronizasyon karmaşıklığı getirdi ve derleyicinin tüm iş parçacıklarının kaynak tampon haritalama kapatılmadan önce tamamlanmış olduğunun doğrulanmasını engelledi. Ekip en sonunda std::thread::scope'u benimsedi, çünkü bu, doğrudan iş parçacığı başlatma üzerinde sıfır maliyetli bir soyutlama sağlarken, hiçbir iş parçacığının kaynak verileri aşmasına yönelik derleme zamanı garantileri getirdi.
Uygulama, 'static olmayan dilimleri ödünç alan bir işleme kapalı alanı tanımladı ve dördü kapsamalı iş parçacıklarının her birinin toplama katılma öngörülmeden hesaplanmasını sağladı. Bu yaklaşım tahsis yükünü ortadan kaldırdı, gecikmeyi %60 oranında azalttı ve önceki C++ uygulamalarındaki erken kapsama çıkmalarının segmentasyon hatalarına yol açmasını önleyen bir hata türünü ortadan kaldırdı. Sonuç, Rust derleyicisinin bir iş parçacığı tutamacını kapsama sınırının ötesinde sızdırmaya yönelik herhangi bir girişimi reddettiği, güvenliği derleme zamanında zorunlu kıldığı sağlam bir sistemdi.
Adayların genellikle kaçırdığı şey.
Neden derleyici, ana iş parçacığı hemen bir birleştirme tutamacı için beklese bile, 'a ömrüne sahip bir referansı doğrudan std::thread::spawn'a iletmeyi reddeder?
std::thread::spawn, kapalı alanının 'static olmasını gerektirir çünkü derleyici, ana iş parçacığının başlatılan iş parçacığından daha uzun süre yaşayacağını kanıtlayamaz. Kod aniden birleştiriyormuş gibi görünse bile, tür sistemi, paniklerin veya erken dönüşlerin birleştirme çağrısını atlayabileceği dinamik çalışmayı hesaba katmak zorundadır. 'static sınırı, tüm yakalanan verilerin belleğini sahiplenmesini veya küresel tahsis kullanmasını sağlar ve kontrol akış yollarından bağımsız olarak kullanımdan sonra serbest bırakmanın (use-after-free) önüne geçer.
Scope<'env, '_> yapısı, başlatılan iş parçacıklarının kapsamanın yığın çerçevesini aşmasına nasıl engel olur? Çalışma zamanı referans sayımına dayanmadan.
Scope tipi, güvenliği sağlamak için invariant yaşam süreleri ve bırakma sırası mantığı kullanır; 'env ömrü, kapsayıcı yığın çerçevesini temsil ederken, 'scope (kısa olan 'env) her bir ScopedJoinHandle'a eklenir. thread::scope işlevi sağlanan kapalı alan tamamlanmadan dönüş yapmaz ve Scope uygulaması, kapalı alan döndürülmeden önce tüm başlatılan iş parçacıklarının bitmesini bekler. Bu tasarım, Rust'ın affine tür sistemi üzerinde çalışır: çünkü tutacakları kapalı alanı aşamaz (çünkü 'scope ömrü nedeniyle) ve kapalı alan scope döndürülmeden önce tamamlanmalıdır, derleyici, tüm iş parçacıklarının yığın çerçevesi çıkmadan önce sona ermesini statik olarak garanti eder.
Kapsamalı iş parçacıklarında panik yüklerinin 'static uygulaması neden gereklidir ve bu, kapsama sınırında panikleri yayarken neden sağlamlığa engel olur?
Bir kapsamalı iş parçacığı panik olduğunda, panik yükü Box<dyn Any + Send + 'static> içinde std::panic makineleri tarafından yakalanır. Bu 'static gereksinimi, bir panik içinde bulunan herhangi bir verinin kapsamalı yığın çerçevesine başvurmamasını garanti eder, çünkü eğer yapsaydı, kapsama çıkıldıktan sonra panik sonucunun çıkartılması, serbest bırakılmış belleğe erişirdi. ScopedJoinHandle::join yöntemi bu kutulu yükü döndürür ve 'static sınırı, panik kapsama dışına yayıldığında bile, ödünç alınan çevreye herhangi bir sarkan işaretçi içermediğini garanti eder, bellek güvenliğini sağlamak için üst üste gelme sınırları arasında korur.