Paylaşılan Durum Eşzamanlılığı
Mesaj geçişi, eşzamanlılığı ele almanın iyi bir yoludur, ancak tek yol değildir. Başka bir yöntem de birden fazla iş parçacığının aynı paylaşılan veriye erişmesidir. Go dil dokümantasyonundaki sloganın bu kısmını tekrar düşünün: “belleği paylaşarak iletişim kurmayın.”
Bellek paylaşarak iletişim kurmak neye benzer? Buna ek olarak, mesaj geçişi meraklıları neden bellek paylaşımını kullanmamaya dikkat ederler?
Bir bakıma, herhangi bir programlama dilindeki kanallar tekil sahipliğe benzer, çünkü bir değeri bir kanaldan aşağı aktardığınızda, artık o değeri kullanmamalısınız. Paylaşılan bellek eşzamanlılığı çoklu sahiplik gibidir: birden fazla iş parçacığı aynı bellek konumuna aynı anda erişebilir. Akıllı işaretçilerin çoklu sahipliği mümkün kıldığı Bölüm 15'te gördüğünüz gibi, çoklu sahiplik karmaşıklık yaratabilir çünkü bu farklı sahiplerin yönetilmesi gerekir. Rust'ın tür sistemi ve sahiplik kuralları bu yönetimin doğru yapılmasına büyük ölçüde yardımcı olur. Bir örnek olarak, paylaşılan bellek için en yaygın eşzamanlılık ilkellerinden biri olan mutekslere bakalım.
Aynı Anda Bir İş Parçacığından Veriye Erişime İzin Vermek için Muteksleri Kullanma
Muteks, karşılıklı dışlamanın kısaltmasıdır, yani bir muteks herhangi bir zamanda yalnızca bir iş parçacığının bazı verilere erişmesine izin verir. Bir muteks içindeki verilere erişmek için, bir iş parçacığı önce muteksin kilidini almak isteyerek erişim istediğini belirtmelidir. Kilit, muteksin bir parçası olan ve o anda verilere kimin özel erişime sahip olduğunu takip eden bir veri yapısıdır. Bu nedenle muteks, kilitleme sistemi aracılığıyla tuttuğu verileri koruyor olarak tanımlanır.
Mutekslerin kullanımı zor olmakla ünlüdür çünkü iki kuralı hatırlamanız gerekir:
- Veriyi kullanmadan önce kilidi elde etmeye çalışmalısınız.
- Muteksin koruduğu verilerle işiniz bittiğinde, diğer iş parçacıklarının kilidi alabilmesi için verilerin kilidini açmanız gerekir.
Muteks için gerçek dünyadan bir benzetme yapmak gerekirse, bir konferansta yalnızca bir mikrofonun olduğu bir panel tartışması hayal edin. Bir panelist konuşmadan önce mikrofonu kullanmak istediğini söylemeli ya da işaret etmelidir. Mikrofonu aldıklarında, istedikleri kadar konuşabilirler ve daha sonra mikrofonu konuşmak isteyen bir sonraki paneliste verirler. Eğer bir panelist işi bittiğinde mikrofonu vermeyi unutursa, başka kimse konuşamaz. Paylaşılan mikrofonun yönetimi yanlış giderse, panel planlandığı gibi çalışmaz!
Mutekslerin yönetimini doğru yapmak inanılmaz derecede zor olabilir, bu yüzden pek çok insan kanallar konusunda heveslidir. Ancak Rust'ın tür sistemi ve sahiplik kuralları sayesinde kilitleme ve kilit açma işlemlerini yanlış yapamazsınız.
Mutex<T>
API'si
Bir muteksin nasıl kullanılacağına örnek olarak, Liste 16-12'de gösterildiği gibi tek iş parçacıklı bir bağlamda bir muteks kullanarak başlayalım:
Dosya adı: src/main.rs
use std::sync::Mutex; fn main() { let m = Mutex::new(5); { let mut num = m.lock().unwrap(); *num = 6; } println!("m = {:?}", m); }
Birçok türde olduğu gibi, ilişkili new
fonksiyonunu kullanarak bir Mutex<T>
oluşturuyoruz. Mutex
içindeki verilere
erişmek için lock
metodunu kullanarak kilidi alırız. Bu çağrı mevcut iş parçacığını bloke eder, böylece kilide sahip
olma sırası bize gelene kadar herhangi bir iş yapamaz.
Kilidi elinde tutan başka bir iş parçacığı paniğe kapılırsa lock çağrısı başarısız olur. Bu durumda, hiç kimse kilidi alamaz, bu nedenle böyle bir durumla karşılaşırsak kilidi açmayı ve bu iş parçacığının paniklemesini sağlamayı seçtik.
Kilidi elde ettikten sonra, bu durumda num olarak adlandırılan geri dönüş değerini, içindeki verilere değiştirilebilir bir referans olarak
ele alabiliriz. Tür sistemi, m
içindeki değeri kullanmadan önce bir kilit elde etmemizi sağlar. m
'nin tipi i32
değil
Mutex<i32>
'dir, bu nedenle i32
değerini kullanabilmek için lock
'u çağırmalıyız. Unutmamalıyız; aksi takdirde
tür sistemi içteki i32
'ye erişmemize izin vermez.
Tahmin edebileceğiniz gibi, Mutex<T>
akıllı bir işaretçidir. Daha doğrusu, lock
çağrısı, unwrap
çağrısıyla işlediğimiz
bir LockResult
'a sarılmış MutexGuard
adlı bir akıllı işaretçi döndürür. MutexGuard
akıllı işaretçisi,
iç verilerimize işaret etmek için Deref
'i uygular; akıllı işaretçi ayrıca, bir MutexGuard
kapsam dışına
çıktığında kilidi otomatik olarak serbest bırakan bir Drop
'a sahiptir, bu da iç kapsamın sonunda gerçekleşir.
Sonuç olarak, kilidi serbest bırakmayı unutma. Muteksin diğer iş parçacıkları tarafından kullanılmasını engelleme riskimiz yoktur, çünkü kilit serbest bırakma işlemi otomatik olarak gerçekleşir.
Kilidi bıraktıktan sonra muteks değerini yazdırabilir ve i32
'yi 6
olarak değiştirebildiğimizi görebiliriz.
Birden Fazla İş Parçacığı Arasında Mutex<T>
Paylaşımı
Şimdi, Mutex<T>
kullanarak bir değeri birden fazla iş parçacığı arasında paylaştırmayı deneyelim.
10 iş parçacığı oluşturacağız ve her birinin bir sayaç değerini 1 artırmasını sağlayacağız, böylece sayaç
0'dan 10'a gidecek. Liste 16-13'teki bir sonraki örnekte bir derleyici hatası olacak ve bu hatayı
Mutex<T>
kullanımı ve Rust'ın bunu doğru kullanmamıza nasıl yardımcı olduğu hakkında daha fazla bilgi edinmek için
kullanacağız.
Dosya adı: src/main.rs
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Mutex::new(0);
let mut handles = vec![];
for _ in 0..10 {
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
Liste 16-12'de yaptığımız gibi, Mutex<T>
içinde i32
tutmak için bir counter
değişkeni oluşturuyoruz. ,
Ardından, bir dizi sayı üzerinde yineleme yaparak 10 iş parçacığı oluşturuyoruz.
Thread::spawn
kullanıyoruz ve tüm iş parçacıklarına aynı kapanışı veriyoruz: sayacı iş parçacığına taşıyan,
lock
metodunu çağırarak Mutex<T>
üzerinde bir kilit elde ediyor ve ardından muteksteki değere 1 eklemiş oluyoruz.
Bir iş parçacığı kapanışını çalıştırmayı bitirdiğinde, num
kapsam dışına çıkar ve kilidi serbest bırakır, böylece
başka bir iş parçacığı onu alabilir.
Ana iş parçacığında, tüm birleştirme tutamaçlarını toplarız. Ardından, Liste 16-2'de yaptığımız gibi, tüm iş parçacıklarının
bittiğinden emin olmak için her bir tanıtıcıda join
çağrısı yaparız. Bu noktada, ana iş parçacığı kilidi alacak ve bu
programın sonucunu yazdıracaktır.
Bu örneğin derlenmeyeceğini demiştik. Şimdi nedenini bulalım!
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0382]: use of moved value: `counter`
--> src/main.rs:9:36
|
5 | let counter = Mutex::new(0);
| ------- move occurs because `counter` has type `Mutex<i32>`, which does not implement the `Copy` trait
...
9 | let handle = thread::spawn(move || {
| ^^^^^^^ value moved into closure here, in previous iteration of loop
10 | let mut num = counter.lock().unwrap();
| ------- use occurs due to use in closure
For more information about this error, try `rustc --explain E0382`.
error: could not compile `shared-state` due to previous error
Hata mesajı, counter
değerinin döngünün önceki yinelemesinde taşındığını belirtir. Rust bize kilit sayacının sahipliğini
birden fazla iş parçacığına taşıyamayacağımızı söylüyor. Derleyici hatasını Bölüm 15'te tartıştığımız çoklu sahiplik yöntemi
ile düzeltelim.
Çoklu İş Parçacığı ile Çoklu Sahiplik
Bölüm 15'te, referans sayılan bir değer oluşturmak için Rc<T>
akıllı işaretçisini kullanarak bir değere
birden fazla sahip vermiştik. Burada da aynısını yapalım ve ne olacağını görelim. Liste 16-14'te Mutex<T>
'yi Rc<T>
'ye
saracağız ve sahipliği iş parçacığına taşımadan önce Rc<T>
'yi klonlayacağız.
Dosya adı: src/main.rs
use std::rc::Rc;
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Rc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Rc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
Bir kez daha derliyoruz ve... farklı farklı hatalar alıyoruz! Derleyici bize çok şey öğretiyor.
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely
--> src/main.rs:11:22
|
11 | let handle = thread::spawn(move || {
| ______________________^^^^^^^^^^^^^_-
| | |
| | `Rc<Mutex<i32>>` cannot be sent between threads safely
12 | | let mut num = counter.lock().unwrap();
13 | |
14 | | *num += 1;
15 | | });
| |_________- within this `[closure@src/main.rs:11:36: 15:10]`
|
= help: within `[closure@src/main.rs:11:36: 15:10]`, the trait `Send` is not implemented for `Rc<Mutex<i32>>`
= note: required because it appears within the type `[closure@src/main.rs:11:36: 15:10]`
note: required by a bound in `spawn`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `shared-state` due to previous error
İlgiçtir ki, bu hata mesajı çok karışık duruyor! İşte odaklanmanız gereken önemli kısım:
Rc<Mutex<i32>>
iş parçacıkları arasında güvenli bir şekilde gönderilemez (`Rc<Mutex<i32>>` cannot be sent between threads safely
) . Derleyici bize bunun nedenini de söylüyor:
Send
tanımı Rc<Mutex<i32>>
için uygulanmıyor. Send
hakkında bir sonraki bölümde konuşacağız: thread
'lerle
kullandığımız türlerin eşzamanlı durumlarda kullanılmasını sağlayan özelliklerden biridir.
Ne yazık ki, Rc<T>
'nin iş parçacıkları arasında paylaşılması güvenli değildir. Rc<T>
referans sayımını yönetirken,
her clone
çağrısı için sayıma ekleme yapar ve her klon bırakıldığında sayıdan çıkarma yapar. Ancak, sayıdaki
değişikliklerin başka bir iş parçacığı tarafından kesintiye uğratılamayacağından emin olmak için herhangi
bir eşzamanlılık ilkeli kullanmaz. Bu, yanlış sayımlara yol açabilir - bu da bellek sızıntılarına veya
bir değerin işimiz bitmeden önce bırakılmasına neden olabilecek ince hatalara yol açabilir.
İhtiyacımız olan şey tam olarak Rc<T>
gibi bir türdür, ancak referans sayımındaki değişiklikleri iş parçacığı
güvenli bir şekilde yapan bir türdür.
Arc<T>
ile Atomik Referans Sayma
Neyse ki Arc<T>
, Rc<T>
gibi eşzamanlı durumlarda kullanımı güvenli olan bir türdür. A
atomik anlamına gelir,
yani atomik olarak referans sayılan bir türdür. Atomikler, burada ayrıntılı olarak ele almayacağımız ek bir
eşzamanlılık ilkelidir: daha fazla ayrıntı için std::sync::atomic
için standart kütüphane
dokümantasyonlarına bakın. Bu noktada, atomiklerin ilkel tipler gibi çalıştığını ancak iş parçacıkları
arasında paylaşılmasının güvenli olduğunu bilmeniz yeterlidir.
O zaman neden tüm ilkel tiplerin atomik olmadığını ve neden standart kütüphane tiplerinin varsayılan
olarak Arc<T>
kullanacak şekilde uygulanmadığını merak edebilirsiniz. Bunun nedeni, iş parçacığı güvenliğinin yalnızca
gerçekten ihtiyaç duyduğunuzda ödemek isteyeceğiniz bir performans cezası ile birlikte gelmesidir.
Sadece tek bir iş parçacığı içinde değerler üzerinde işlem yapıyorsanız, atomiklerin sağladığı garantileri
uygulamak zorunda kalmazsanız kodunuz daha hızlı çalışabilir.
Örneğimize geri dönelim: Arc<T>
ve Rc<T>
aynı API'ye sahiptir, bu nedenle use
satırını, new
çağrısını ve clone
çağrısını değiştirerek programımızı düzeltiriz. Liste 16-15'teki kod nihayet derlenecek ve çalışacaktır:
Dosya adı: src/main.rs
use std::sync::{Arc, Mutex}; use std::thread; fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap()); }
Bu kod aşağıdakileri yazdıracaktır:
Result: 10
Başardık! 0'dan 10'a kadar saydık, bu çok etkileyici görünmeyebilir, ancak bize Mutex<T>
ve iş parçacığı güvenliği hakkında
çok şey öğretti. Bu programın yapısını bir sayacı artırmaktan daha karmaşık işlemler yapmak için de kullanabilirsiniz.
Bu stratejiyi kullanarak, bir hesaplamayı bağımsız parçalara bölebilir, bu parçaları iş parçacıkları arasında paylaştırabilir ve
ardından her iş parçacığının nihai sonucu kendi parçasıyla güncellemesini sağlamak için Mutex<T>
'i kullanabilirsiniz.
RefCell<T>
/Rc<T>
ve Mutex<T>
/Arc<T>
Arasındaki Benzerlikler
Sayacın değişmez olduğunu ancak içindeki değere değişebilir bir referans alabileceğimizi fark etmiş olabilirsiniz; bu,
Mutex<T>
'nin Cell
ailesinin yaptığı gibi iç değişebilirlik sağladığı anlamına gelir. Bölüm 15'te RefCell<T>
'yi bir Rc<T>
içindeki içeriği
değiştirmemize izin vermek için kullandığımız gibi, Mutex<T>
'yi bir Arc<T>
içindeki içeriği değiştirmek için kullanırız.
Unutulmaması gereken bir diğer ayrıntı da Mutex<T>
kullandığınızda Rust'ın sizi her türlü mantık hatasından koruyamayacağıdır.
Bölüm 15'te Rc<T>
kullanmanın, iki Rc<T>
değerinin birbirine atıfta bulunduğu ve bellek sızıntılarına neden olan referans döngüleri
oluşturma riskiyle birlikte geldiğini hatırlayın. Benzer şekilde, Mutex<T>
de kilitlenme yaratma riskini beraberinde getirir.
Bunlar, bir işlemin iki kaynağı kilitlemesi gerektiğinde ve iki iş parçacığının her biri kilitlerden birini aldığında ortaya çıkar ve
birbirlerini sonsuza kadar beklemelerine neden olur. Kilitlenmelerle ilgileniyorsanız, kilitlenmeye sahip bir Rust programı oluşturmayı deneyin;
daha sonra herhangi bir dilde muteksler için kilitlenme azaltma stratejilerini araştırın ve bunları Rust'ta uygulamayı deneyin. Mutex<T>
ve
MutexGuard
için standart kütüphane API belgeleri faydalı bilgiler sunar.
Bu bölümü Send
ve Sync
tanımlarından ve bunları özel türlerle nasıl kullanabileceğimizden bahsederek tamamlayacağız.