Ders 05 / 10
Atomik İşlemler ve Bellek Sıralaması
Kilitsiz bir sayaç beklemeden, kimseye ihtiyaç duymadan ilerler; aynı toplam artış hem dışlama kilidiyle hem atomik işlemlerle yapılır ve iki yöntem birebir aynı sonucu verir — fark sonuçta değil, bekleyenin olup olmamasındadır.
Yayın Güncelleme
Bu derste
- Kilitli ve atomik sayaçların aynı toplam artışta birebir aynı sonucu verdiğini koşturarak doğrular
- Atomik bir sayacın kendi başına ilerlediğini, kilitli bir sayacın bekleyen taraf içerdiğini karşılaştırır
- Atomik olmanın sıralı olmak anlamına gelmediğini ve hangi artışın önce olduğunun ölçülmediğini gösterir
İçindekiler
Önceki ders bir dışlama kilidini bekleyen tarafın nasıl ilerlediğini ölçtü: kilidi tutan kendi başına, bekleyen başkası bitince. Bu dersin sorusu şu: her paylaşım bekleyen bir taraf gerektiriyor mu? Yanıt hayır — belirli bir koşulu sağlayan basit paylaşımlar, hiç kimseyi beklemeden, donanımın kendi garantisiyle güvenle güncellenebiliyor. Bu, İş Parçacıkları konusunun son dersi ve kilitsiz ilerlemeyi doğrudan ölçüyor.
Bu koşul dar: yalnızca tek bir makine sözcüğüne sığan basit değerler (tam sayılar,
mantıksal değerler, işaretçi genişliğinde sayılar) atomik olarak güncellenebiliyor. Önceki
derslerde görülen PaylasilanKayit gibi birden fazla alan taşıyan bir yapı, ya da
Kanallarla Mesajlaşma dersindeki gibi öbekte büyüyen bir liste, tek bir atomik işlemle
güncellenemez — bu tip veriler hâlâ bir kilide ihtiyaç duyuyor. Bu dersin ölçtüğü kilitsiz
ilerleme, bu yüzden dar ama önemli bir özel durum: tek bir sayının, tek bir bayrağın, tek
bir işaretçinin paylaşılan güncellemesi.
Donanımın Tek Adımlık Garantisi
İşletim Sistemi Kavramları kursu ve JVM, Eşzamanlılık ve Başarım kursu karşılaştır-ve-değiştir gibi donanım ilkellerinden söz etti; bu kavram burada yeniden kurulmuyor. Atomik bir işlem, işlemcinin tek bir bölünmez adımda gerçekleştirdiği bir okuma-değiştirme-yazma dizisidir: bir sayacı bir arttırmak, iki iş parçacığı aynı anda dener denese de, hiçbir zaman yarım kalmış ya da üst üste binmiş bir sonuç üretmez. Bir dışlama kilidi de aynı sonucu sağlıyor, ama farklı bir yolla — kilidi tutan tek taraf hariç herkesi bekletiyor. Atomik işlem ise hiç kimseyi bekletmiyor; donanımın kendisi, aynı anda gelen iki isteği sıraya koyup ikisini de kayıpsız işliyor.
Bu güvencenin neden gerekli olduğunu anlamak için, korumasız bir artışın neden yanlış sonuç verdiğine bakmak yeterli. Bir tam sayıyı bir artırmak aslında üç ayrı adım: mevcut değeri oku, bir ekle, sonucu geri yaz. İki iş parçacığı bu üç adımı aynı anda, korumasız biçimde yapmaya çalışırsa, ikisi de aynı eski değeri okuyabilir, ikisi de kendi hesapladığı yeni değeri yazabilir — ve son yazan, öbürünün artışını örter. Sonuç, iki artış yapılmış olmasına rağmen sayacın yalnızca bir artmış olmasıdır; bu bir veri yarışının doğrudan sonucu. Atomik bir işlem bu üç adımı donanım düzeyinde tek bir bölünmez birim hâline getirerek bu örtmeyi imkânsız kılıyor — okuma, ekleme ve yazma arasına hiçbir başka iş parçacığı giremiyor.
Bu farkın koşumdaki karşılığı şu: bir dışlama kilidiyle korunan bir sayacı arttıran on iş parçacığından dokuzu, herhangi bir anda dokuzuncusu kilidi tutarken bekliyor olabilir — başkası bitince sınıfına giriyorlar. Atomik bir sayacı arttıran on iş parçacığının hiçbiri böyle bir bekleyişe girmiyor; hepsi kendi başına sınıfında kalıyor. Aşağıdaki ölçüm bu farkı, iki yöntemin ürettiği sonucu karşılaştırarak gösteriyor: ikisi de aynı toplamı üretiyor, çünkü ikisi de kayıp güncelleme olmadan çalışıyor — farklı olan yalnızca bunu nasıl başardıkları.
İş Parçacıkları Arası Güvenliğin Tipteki Karşılığı dersinde kurulan Send/Sync çerçevesi
burada da geçerli: AtomicUsize tipi hem Send hem Syncdir, bu yüzden Arc<AtomicUsize>
birden fazla iş parçacığı arasında paylaşılabiliyor. Bu, Arc<Mutex<usize>>in taşıdığı
aynı iki izinle aynı — aradaki fark, Mutexin kendi içinde bir kilit taşıması, AtomicUsize
tipinin ise hiçbir kilit taşımadan, doğrudan donanımın bölünmez işlemlerine dayanmasıdır.
İkisi de tip sisteminde aynı iznin bir örneği, ama garantiyi sağlama biçimleri farklı.
İki Yöntem, Aynı Sonuç
Aşağıdaki program iki işlev tanımlıyor ve main içinde ikisini de aynı sabitlerle
çalıştırıyor: on iş parçacığı, her biri beş yüz kez artış. kilitle_ve_say bunu bildiğimiz
Arc<Mutex<usize>> düzeneğiyle yapıyor — her artış bir tutamaç istiyor, tutamaç kapsam
dışına çıkınca kilit bırakılıyor. atomik_say aynı işi Arc<AtomicUsize> ile yapıyor;
burada tutamaç yok, yalnızca fetch_add adlı tek bir çağrı var ve bu çağrı hem okuma hem
yazma işini kendi içinde, bölünmez biçimde yapıyor. İki işlevin de dönüş tipi aynı
(usize), böylece sonuçları doğrudan karşılaştırılabiliyor.
// atomik.rs — kilitsiz bir sayac beklemeden ilerler; artis sirasi olculmez, yalniz toplam.
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
const IS_PARCACIGI: usize = 10;
const ARTIS: usize = 500;
fn kilitle_ve_say() -> usize {
let sayac = Arc::new(Mutex::new(0usize));
let mut tutamaclar = Vec::new();
for _ in 0..IS_PARCACIGI {
let s = Arc::clone(&sayac);
tutamaclar.push(thread::spawn(move || {
for _ in 0..ARTIS {
// her artis kilidi ister: bekleyen bir is parcacigi baskasi bitince ilerler
let mut deger = s.lock().unwrap();
*deger += 1;
}
}));
}
for t in tutamaclar {
t.join().unwrap();
}
let sonuc = *sayac.lock().unwrap();
sonuc
}
fn atomik_say() -> usize {
let sayac = Arc::new(AtomicUsize::new(0));
let mut tutamaclar = Vec::new();
for _ in 0..IS_PARCACIGI {
let s = Arc::clone(&sayac);
tutamaclar.push(thread::spawn(move || {
for _ in 0..ARTIS {
// hicbir is parcacigi kilit beklemez: her artis kendi basina ilerler
s.fetch_add(1, Ordering::SeqCst);
}
}));
}
for t in tutamaclar {
t.join().unwrap();
}
sayac.load(Ordering::SeqCst)
}
fn main() {
let kilitli_sonuc = kilitle_ve_say();
let atomik_sonuc = atomik_say();
let beklenen = IS_PARCACIGI * ARTIS;
println!("beklenen toplam: {}", beklenen);
println!("kilitle sayilan toplam: {}", kilitli_sonuc);
println!("atomik sayilan toplam: {}", atomik_sonuc);
println!(
"iki yontem ayni sonucu verdi: {}",
kilitli_sonuc == atomik_sonuc && atomik_sonuc == beklenen
);
// --- iki atomik sayac: hangi artisin once oldugu olculmuyor, yalniz nihai toplam ---
let a = Arc::new(AtomicUsize::new(0));
let b = Arc::new(AtomicUsize::new(0));
let mut tutamaclar = Vec::new();
for i in 0..IS_PARCACIGI {
let (a, b) = (Arc::clone(&a), Arc::clone(&b));
tutamaclar.push(thread::spawn(move || {
if i % 2 == 0 {
a.fetch_add(1, Ordering::SeqCst);
} else {
b.fetch_add(1, Ordering::SeqCst);
}
}));
}
for t in tutamaclar {
t.join().unwrap();
}
let toplam_ikili = a.load(Ordering::SeqCst) + b.load(Ordering::SeqCst);
println!();
println!("iki ayri atomik sayacin toplami: {}", toplam_ikili);
println!("(hangi is parcaciginin hangi sayaci once arttirdigi basilmiyor)");
}
beklenen toplam: 5000 kilitle sayilan toplam: 5000 atomik sayilan toplam: 5000 iki yontem ayni sonucu verdi: true iki ayri atomik sayacin toplami: 10 (hangi is parcaciginin hangi sayaci once arttirdigi basilmiyor)
IP8. On iş parçacığı, her biri beş yüz kez artırarak, aynı sayacı beş bin defa
arttırıyor. kilitle_ve_say bunu bir dışlama kilidiyle yapıyor: her artış lock çağrısı
istiyor ve on iş parçacığından dokuzu herhangi bir anda kilit için bekleyebiliyor —
başkası bitince sınıfına giren bir düzenek. atomik_say aynı beş bin artışı fetch_add
çağrısıyla yapıyor: hiçbir iş parçacığı bir kilide ihtiyaç duymuyor, hepsi kendi başına
sınıfında kalıyor. İki yöntem de beklenen toplamı (5000) üretiyor ve bu sonuç üç ayrı
koşumda birebir aynı çıkıyor — kilitli ve atomik sayaçlar arasında hiçbir kayıp güncelleme
farkı yok.
İki işlevin yapısı da bunu görünür kılıyor: kilitle_ve_say içindeki döngü gövdesi bir
Mutex tutamacı alıyor, değeri değiştiriyor ve tutamacı bırakıyor — bu üç adım her artışta
tekrarlanıyor. atomik_say içindeki döngü gövdesi ise yalnızca fetch_add çağırıyor; ne
bir tutamaç alınıyor ne de açıkça bırakılıyor, çünkü AtomicUsize tipinin kendisi bir kilit
taşımıyor. İki işlevin de aynı sayıda iş parçacığı, aynı sayıda artış kullanması ve aynı
beklenen sabitle karşılaştırılması, tek değişkenin yöntem olmasını sağlıyor — sonuçtaki
her fark, kilitli olup olmamaktan kaynaklanıyor, başka hiçbir değişkenden değil.
Sıralama Ölçülmez, Yalnızca Toplam
Programın ikinci bölümü iki ayrı atomik sayaç kullanıyor: çift numaralı iş parçacıkları a
sayacını, tek numaralı iş parçacıkları b sayacını artırıyor. On iş parçacığının hangi
sırayla çalıştığı, hangisinin önce fetch_add çağırdığı bu programda hiçbir yerde
ölçülmüyor ve basılmıyor — yalnızca iki sayacın nihai toplamı (10) yazdırılıyor. Bu, İş
Parçacığı Oluşturma dersinde kurulan kuralın burada da geçerli olduğunu gösteriyor:
zamanlamaya bağlı hiçbir gözlem basılmıyor, yalnızca belirlenimci olan — bu örnekte, kaç
iş parçacığının çift ya da tek numaralı olduğu, dolayısıyla nihai toplamın ne olacağı —
basılıyor.
Bu ikinci ölçümün amacı, tek bir sayaç yerine iki ayrı sayaç kullanarak, “hangi iş
parçacığı önce çalıştı” sorusunun cevabının programın çıktısını değiştirmediğini
göstermek. On iş parçacığının zamanlayıcı tarafından hangi sırayla çalıştırıldığı her
koşumda farklı olabilir — bu programda IS_PARCACIGI sabiti on olduğu için beş tanesi a
sayacına, beş tanesi b sayacına yazıyor ve iki sayacın toplamı her zaman on. Zamanlama
değişse bile bu toplam değişmiyor, çünkü i % 2 hesabı iş parçacığının numarasına
bağlı, çalıştırılma sırasına değil. Bu, bir hesabın sonucu ile o hesabı üreten
işlemlerin sırası arasındaki farkı gösteren bir başka örnek.
Sınırlayıcı Ölçüm
Atomik olmak sıralı olmak anlamına gelmiyor. Bu derste kullanılan Ordering::SeqCst,
işlemin kendisinin bölünmez olduğunu garanti ediyor — hiçbir iş parçacığı yarım kalmış bir
artışı göremiyor — ama hangi artışın diğerinden önce gerçekleştiğini bu koşumda hiçbir
yerde belirlemiyoruz ve belirleyemeyiz de: on iş parçacığı aynı anda çalışıyor ve
zamanlayıcının onlara işlemci payını hangi sırayla vereceği ortama bağlı. Ölçülen tek şey,
beş bin artışın hiçbirinin kaybolmadığı — final toplamın tam olarak beklenen sayıya
eşit olduğu. Hangi iş parçacığının hangi anda kaçıncı artışı yaptığı sorusu, bu kursun
belirlenimcilik kuralı gereği hiçbir yerde ölçülmüyor.
Bellek sıralamasının kendisi — Ordering::SeqCst, Ordering::Relaxed ve aradaki
seçenekler — bu derste kavram olarak anlatılıyor, ölçülmüyor, çünkü ölçülemiyor: bir
sıralama seçeneğinin diğerinden farkı, çok işlemcili bir donanımın kendi önbellek
tutarlılık davranışında ortaya çıkıyor ve bu davranış koşumdan koşuma, hatta aynı koşum
içinde çağrıdan çağrıya değişebiliyor. SeqCst en güçlü ve en kolay akıl yürütülen
seçenektir: bütün iş parçacıkları, bütün atomik işlemleri aynı global sırada görüyormuş gibi
davranır. Daha gevşek seçenekler (Relaxed gibi) yalnızca tek bir değişkenin bölünmezliğini
garanti eder, işlemler arasındaki görünürlük sırasını garanti etmez — bu fark, iki farklı
atomik değişken arasında bir ilişki kurulmak istendiğinde önem kazanır, ama bu derste
kullanılan tek bir sayaç senaryosunda görünmez.
Neden SeqCst seçildiği de bu sınırın bir parçası. Bu ders tek bir atomik değişkeni (ve
ikinci ölçümde, birbirinden bağımsız iki atomik değişkeni) arttırıyor; iki farklı atomik
değişken arasında bir okumanın diğerinin yazmasından önce mi sonra mı göründüğünü
gözlemleyen bir senaryo burada kurulmuyor. Böyle bir senaryo kurulsaydı, seçilen sıralama
seçeneği sonucu etkileyebilirdi ve o fark yalnızca belirli işlemci mimarilerinde, belirli
önbellek durumlarında ortaya çıkardı — yani ortama bağlı bir gözlem olurdu. Bu kursun
kuralı gereği böyle bir gözlem hiç kurulmuyor; SeqCst en güvenli seçenek olduğu için
seçildi ve dersin ölçtüğü tek şey — toplamın doğruluğu — açısından hiçbir sıralama
seçeneği bir diğerinden farklı sonuç vermezdi.
Özet
- Atomik bir işlem, donanımın tek bir bölünmez adımda gerçekleştirdiği bir okuma-değiştirme- yazma dizisidir; kilitsiz bir sayaç bu sayede kimseye ihtiyaç duymadan, kendi başına ilerler.
- Aynı beş bin artış hem kilitli hem atomik yöntemle yapıldığında birebir aynı toplamı (5000) üretiyor; fark sonuçta değil, kilitli sayaçta bekleyen bir tarafın bulunması, atomik sayaçta ise hiç bulunmamasında.
- Kilitli bir sayaçta iş parçacıkları kilit için bekleyerek başkası bitince sınıfına girebilir; atomik bir sayaçta hiçbir iş parçacığı kimseyi beklemez, hepsi kendi başına ilerler.
- Atomik olmak sıralı olmak değildir: hangi artışın önce gerçekleştiği bu kursta hiç ölçülmez ve basılmaz; ölçülen yalnızca nihai toplamın doğruluğudur.
- Bellek sıralaması seçenekleri kavram olarak anlatılır, koşumla ölçülmez, çünkü aralarındaki fark donanımın önbellek davranışına bağlıdır ve bu davranış basılmaz.
- Kilitsiz ilerleme dar bir koşula bağlıdır: yalnızca tek bir makine sözcüğüne sığan basit değerler atomik olarak güncellenebilir; birden fazla alan taşıyan bir yapı hâlâ bir kilide ihtiyaç duyar.
Sonraki Adım
Bu konunun beş dersi boyunca ölçülen birimler kursun dört sınıfından üçüne düştü: kendi
başına ilerleyen bağımsız iş parçacıkları — bu derste görülen kilitsiz sayaçlar da bu sınıfta
—, başkasının bitmesini bekleyen taraflar ve kaynakta yazılı olduğu hâlde hiç ilerlemeyen
birimler. İlerleyenlerin hepsi ortak bir varsayıma dayanıyordu: bir iş parçacığı,
başlatıldığı andan itibaren işletim sisteminin zamanlayıcısı tarafından ileriye taşınıyor —
programcı yalnızca ne zaman bekleyeceğini belirliyor, ilerlemenin kendisini değil.
Dördüncü sınıf, yoklanınca, bu konuda hiç görülmedi.
Eşzamansız Programlama konusu bu varsayımı tersine çeviriyor. Kaynakta yazılan bir görev, hiçbir iş parçacığına bağlanmadan, kendiliğinden hiç ilerlemeyen tembel bir değer olarak duruyor — bir iş parçacığının aksine, bir görev başlatıldığı anda hiçbir şey yapmıyor. Sıradaki ders bu farkı doğrudan ölçüyor: aynı kaynaktan kurulan iki görevden biri bir yürütücüye verilir ve ilerler, öbürü verilmez ve hiçbir kayıt üretmez — kaynakları birebir aynı olsa da.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.