Bölüm 6: Senkronizasyon
Giriş
Öğrenme Çıktıları
Yarış durumunu, kritik bölgeyi ve bir çözümün sağlaması gereken üç koşulu (karşılıklı dışlama, ilerleme, sınırlı bekleme) açıklamak.
Kilit değişkeni, katı sıra ve Peterson çözümünü değerlendirmek; modern CPU'larda yeniden sıralamanın etkisini ve bellek bariyerlerini açıklamak.
test_and_set, compare_and_swap, XCHG ve atomik değişkenlerle kilit ve sayaç gerçeklemek.
Spinlock, mutex, semafor, monitör ve koşul değişkenini karşılaştırmak; Mesa anlamındaki while kuralını uygulamak.
Sınırlı tampon, okur-yazar ve yemek yiyen filozoflar problemlerini çözmek; açlık ve kilitlenme risklerini görmek.
POSIX ve Java API'lerini kullanmak; öncelik terslemesini (Mars Pathfinder) ve kilitsiz yapıların temelini yorumlamak.
Giriş
Neden Senkronizasyon?
- Senkronizasyon: eşzamanlı yürüyen süreç ya da iş parçacıklarının paylaşılan verilere erişimini düzenleyerek tutarlılığı korumak.
- Bir süreç komutlarının herhangi bir noktasında kesilebilir; yaptığı güncelleme yarım kalmış olabilir.
- Çok çekirdekli sistemlerde kesme bile gerekmez: iki çekirdek gerçekten aynı anda aynı belleğe yazabilir.
- Belirtiler:
- tutarsız veri, kayıp güncelleme, bozuk bağlı liste;
- yeniden girilemeyen (non-reentrant) fonksiyonun ortak tamponunun ezilmesi;
- birbirini sonsuza dek bekleyen iş parçacıkları (kilitlenme);
- hiç sıra gelmeyen iş parçacığı (açlık).
Giriş
Yarış Durumu: fork() ve pid
Çekirdekte sıradaki boş pid next_available_pid değişkeninde tutulur: 2615. P0 ve P1 aynı anda fork() çağırır.
P0'ın çekirdek kodu değeri okur (2615) ama artırıp yazmadan önce kesilir.
P1 de 2615 okur, çocuğuna verir ve 2616 yazar.
P0 kaldığı yerden devam eder: çocuğuna yine 2615 verir. Sonuç, işlemlerin hangi sırayla çalıştığına bağlıdır: yarış durumu.
Bölüm 3'te count++ ifadesinin yazmaç düzeyinde iç içe geçmesi ve yazıcı kuyruğu örneği adım adım izlenmişti; burada aynı sorun çekirdek içinden görülüyor.
Kritik Bölge
Kritik Bölge Problemi
- n süreç
{P0, P1, …, Pn-1}; her birinin paylaşılan veriye eriştiği bir kritik bölgesi vardır: global değişkene yazmak, tablo güncellemek, dosyaya yazmak… - Kural: bir süreç kritik bölgedeyken başka hiçbir süreç kendi kritik bölgesinde olmamalıdır.
- Her süreç girmek için giriş bölümünde izin ister, çıkarken çıkış bölümünde bunu duyurur.
- Yarış durumu: sonucun yürütme sırasına bağlı olması. Karşılıklı dışlama: paylaşılan veriye aynı anda tek erişim.
while (true) { /* giriş bölümü (entry) */ /* kritik bölge */ /* çıkış bölümü (exit) */ /* kalan bölüm (remainder) */ }
Kritik Bölge
Bir Çözümün Üç Koşulu
| Tanenbaum'un dört koşulu | Karşılığı |
|---|---|
| İki süreç aynı anda kritik bölgede olmamalı. | Karşılıklı dışlama |
| CPU hızı ve sayısı hakkında varsayım yapılmamalı. | Her süreç sıfırdan büyük hızla ilerler; göreli hız bilinmez |
| Kritik bölge dışındaki süreç diğerlerini engellememeli. | İlerleme |
| Hiçbir süreç sonsuza dek beklememeli. | Sınırlı bekleme |
Silberschatz 10e §6.2; Tanenbaum MOS §2.3.2. Dışlama bir güvenlik, diğer ikisi canlılık özelliğidir.
Kritik Bölge
Kritik Bölge Zaman Çizelgesi
T1: A kritik bölgesine girer.
T2: B girmek ister; bölge dolu olduğu için bloke olur (ya da meşgul bekler).
T3: A çıkar; bekleyen B hemen girer. Hiçbir anda iki süreç bölgede değildir.
T4: B çıkar. İyi bir çözüm bu davranışı hangi zamanlama olursa olsun garanti etmelidir.
Tanenbaum MOS Şekil 2-22'den uyarlanmıştır.
Yazılım Yaklaşımları
Kesmeleri Devre Dışı Bırakmak
- Süreç kritik bölgeye girmeden kesmeleri kapatır, çıkınca açar: zamanlayıcı kesmesi gelmediği için bağlam değişimi olmaz.
- Sorunlar:
- Kesmeleri açmayı unutan (ya da sonsuz döngüye giren) süreç sistemi kilitler.
- Bu sürede G/Ç ve zamanlayıcı kesmeleri bekler; uzun kritik bölgede sistem tepkisizleşir.
- Çok çekirdekte işe yaramaz: diğer çekirdekler çalışmaya devam eder.
cli/stiayrıcalıklı komutlardır; kullanıcı programı çalıştıramaz.
- Çekirdek içinde kısa süreli ve yerel kullanım yaygındır: Linux
local_irq_save(), kesme işleyicisiyle paylaşılan veridespin_lock_irqsave().
Yazılım Yaklaşımları · Kodu adım adım çalıştır
Kilit Değişkeni
int lock = 0; /* paylaşılan */ void gir(void) { while (lock != 0) ; /* meşgul bekle */ lock = 1; } void cik(void) { lock = 0; } /* her süreç: gir(); kritik_bolge(); cik(); */
Paylaşılan lock 0: bölge boş.
P0 lock'u okur: 0. Döngüden çıkar…
…ama lock = 1 yazmadan önce kesilir. P1 de 0 okur.
P1 kilidi 1 yapar ve kritik bölgeye girer.
P0 devam eder; zaten test ettiği için o da 1 yazar ve girer.
Karşılıklı dışlama bozuldu: test et ve ata ayrı adımlar. Çözüm: ikisini tek atomik komutta yapmak (TSL).
Kilidin kendisi paylaşılan bir değişkendir; onu korumak için yine kilide ihtiyaç duyulur. Bu yüzden yazılım kilidi tek başına yetmez.
Yazılım Yaklaşımları
Katı Sırayla Değişim
/* P0 */ while (true) { while (turn != 0) ; /* bekle */ kritik_bolge(); turn = 1; kritik_disi(); }
/* P1 */ while (true) { while (turn != 1) ; /* bekle */ kritik_bolge(); turn = 0; kritik_disi(); }
turn = 0: P0 girer, P1 meşgul bekler (KB kritik bölge, KDB kritik dışı bölüm).
P0 çıkarken turn = 1 yapar; P1 girer. Karşılıklı dışlama sağlanır.
P1 çıkar (turn = 0) ve uzun bir kritik dışı işe başlar. P0 bir kez daha girip çıkar: turn = 1.
P0 yeniden girmek ister ama sıra P1'de. Bölge boş olduğu hâlde kritik bölge dışındaki P1, P0'ı engelliyor: ilerleme koşulu bozulur.
Yazılım Yaklaşımları
Peterson Algoritması
/* paylaşılan */ bool flag[2] = { false, false }; int turn; /* P_i (j = 1 - i) */ while (true) { flag[i] = true; /* girmek istiyorum */ turn = j; /* önce sen buyur */ while (flag[j] && turn == j) ; /* meşgul bekle */ /* kritik bölge */ flag[i] = false; /* kalan bölüm */ }
G. L. Peterson, 1981. Yalnız iki süreç içindir; n süreç için fırıncı (bakery) algoritması vardır.
flag[0] = flag[1] = true olmalı; ama turn aynı anda hem 0 hem 1 olamaz. turn'e son yazan bekler.flag[j] = false: Pi hiç beklemeden girer.turn = i yapar; Pi en fazla bir giriş bekler.Bellek Modeli · Kodu adım adım çalıştır
Peterson Modern CPU'da Neden Bozulur?
/* P_i (i = 0 ya da 1, j = 1 - i) */ flag[i] = true; turn = j; while (flag[j] && turn == j) ; /* kritik bölge */ flag[i] = false;
Başlangıç: bellekte iki bayrak false. Her çekirdeğin yazmaları önce kendi yazma tamponuna gider.
P0 iki yazmasını yapar; ikisi de tamponda bekler, bellekte henüz görünmez.
P1 de aynısını yapar. x86 bile (TSO) yazmayı sonraki okumadan sonraya bırakabilir.
P0 flag[1]'i bellekten okur: false → döngüden çıkar.
P1 de flag[0]'ı false okur ve girer: ikisi birden kritik bölgede!
Tamponlar sonra boşalır ama iş işten geçmiştir. Çözüm: yazma ile okuma arasına tam bellek bariyeri ya da sıralı tutarlı atomikler.
Derleyici de bu yüklemeyi öne alabilir. C11'de _Atomic + varsayılan memory_order_seq_cst, Java'da volatile alanlar bu sıralamayı geri getirir.
Bellek Modeli
Bellek Modelleri ve Yeniden Sıralama
/* x = y = 0 */ /* T1 */ /* T2 */ x = 1; y = 1; r1 = y; r2 = x; /* r1 == 0 && r2 == 0 ? */
Sezgiye göre en az biri 1 görmeli. Ama x86, ARM ve RISC-V'de r1 = r2 = 0 gözlenebilir (store-buffer testi).
| Model | İzin verilen yeniden sıralama |
|---|---|
| Sıralı tutarlılık (SC) | Hiçbiri; tüm çekirdekler tek bir ortak sıra görür (Lamport 1979). |
| x86-64 (TSO) | Yalnız yazma → sonraki okuma; yazmalar kendi aralarında sıralı. |
| ARMv8, RISC-V (zayıf) | Okuma ve yazmalar bağımlılık yoksa birbirini geçebilir. |
| Derleyici | Tek iş parçacığı için eşdeğerse her erişimi taşıyabilir, kaydedebilir ya da silebilir. |
volatile iş parçacıkları için ne atomiklik ne de sıralama sağlar. Doğru araçlar: C11 <stdatomic.h>, Java volatile/java.util.concurrent, kilitler.Bellek Modeli
Bellek Bariyerleri
Bariyer yok: zayıf modelde T2'nin yazmaları başka sırayla görünebilir; T1 flag'i true görür ama x hâlâ 0'dır.
Ya da T1'in kendisi x'i flag'den önce okuyabilir (okuma-okuma yeniden sıralaması).
Bellek bariyeri (fence): öncesindeki tüm okuma/yazmalar tamamlanmadan sonrakiler başlamaz. T2: x önce; T1: flag önce.
Gerçek kod: x86 mfence, ARM dmb, Linux smp_mb(), C11 atomic_thread_fence; çoğu zaman release/acquire atomikleri yeterlidir.
Silberschatz 10e §6.4.1'deki örnek. Kilit ve semafor gerçeklemeleri bu bariyerleri zaten içerir: kilitle korunan kodda ayrıca bariyer yazmak gerekmez.
Bellek Modeli
Peterson Java'da (Düzeltilmiş)
public class Peterson { static volatile boolean flag0, flag1; static volatile int turn; static int sayac = 0; static void kilitle(int i) { if (i == 0) { flag0 = true; turn = 1; while (flag1 && turn == 1) Thread.onSpinWait(); } else { flag1 = true; turn = 0; while (flag0 && turn == 0) Thread.onSpinWait(); } } static void birak(int i) { if (i == 0) flag0 = false; else flag1 = false; } /* main: i = 0 ve i = 1 için iki iş parçacığı, her biri 100_000 kez: kilitle(i); sayac++; birak(i); ikisi join edilir, sayac yazdırılır */ }
- Sunumdaki eski örnekte üç hata vardı:
volatile boolean[] flagyalnız dizi referansını volatile yapar, elemanları değil.getId() % Niki iş parçacığına aynıi'yi verebilir.- Kritik bölgedeki
printölçümü bozar.
- Java'da
volatileokuma/yazmaları sıralı tutarlıdır; ayrı volatile alanlarla Peterson doğru çalışır. Thread.onSpinWait()(Java 9+) CPU'ya “meşgul bekliyorum” ipucu verir (x86pause).
sayac = 200000 sayac = 200000 sayac = 200000
Bellek Modeli
Senkronizasyon Bariyeri
Fazlara bölünmüş paralel hesap (ör. ızgara simülasyonu): her süreç bir dilimi hesaplar.
Bitiren süreç barrier_wait() çağırır ve bloke olur; en yavaş süreç (C) beklenir.
N'inci süreç gelince bariyer açılır; hepsi bir sonraki faza birlikte geçer.
var b = new CyclicBarrier(3, () -> System.out.println( "faz " + faz[0]++ + " tamam")); /* 3 iş parçacığı, her biri: for (f = 0; f < 2; f++) { hesapla(f); b.await(); } */
faz 0 tamam faz 1 tamam
pthread_barrier_wait, CyclicBarrier); bellek bariyeri tek bir çekirdeğin bellek erişim sırasını zorlar.Donanım Desteği
Atomik Donanım Komutları
/* tümü TEK atomik komuttur */ bool test_and_set(bool *hedef) { bool eski = *hedef; *hedef = true; return eski; } /* kullanım: basit spinlock */ bool lock = false; while (test_and_set(&lock)) ; /* meşgul bekle */ /* kritik bölge */ lock = false;
- Yazılım çözümlerinin sorunu: oku ve yaz ayrı adımlar. Donanım, bir bellek sözcüğünü kesintisiz test edip değiştiren komutlar sunar.
- Atomiklik çok çekirdekte de geçerlidir: önbellek tutarlılığı protokolü o satırı komut boyunca tek çekirdeğe ayırır (eskiden bellek veriyolu kilitlenirdi).
| Mimari | Atomik okuma-değiştirme-yazma |
|---|---|
| x86-64 | XCHG, LOCK CMPXCHG, LOCK XADD, LOCK BTS |
| ARMv8 | LDXR/STXR (LL/SC); v8.1 LSE: CAS, LDADD, SWP |
| RISC-V “A” | LR/SC, AMOSWAP, AMOADD |
| Tanenbaum'un sanal CPU'su | TSL REGISTER,LOCK |
Donanım Desteği · Kodu adım adım çalıştır
TSL ile Kritik Bölge
enter_region: TSL REGISTER,LOCK | R = LOCK; LOCK = 1 CMP REGISTER,#0 | LOCK 0 mıydı? JNE enter_region | değilse: tekrar dene RET | kritik bölgeye girildi leave_region: MOVE LOCK,#0 | LOCK = 0 RET
Kilit boş: LOCK = 0.
CPU 0 TSL: eski değer (0) yazmaca, LOCK'a 1 — tek bölünmez adımda.
CPU 1 de TSL yürütür: yazmaca 1 gelir; LOCK zaten 1'dir.
CPU 0 sıfır gördü: kritik bölgede. CPU 1 sıfır olmayan değer gördü: meşgul bekler.
CPU 0 çıkarken sıradan bir yazmayla LOCK'u 0 yapar.
CPU 1'in sonraki TSL'i 0 okur ve kilidi alır. İki CPU hiçbir an birlikte içeride değildir.
Tanenbaum MOS Şekil 2-25. TSL çok çekirdekte de çalışır; kesme kapatmanın aksine diğer CPU'ların o sözcüğe erişimini de engeller.
Donanım Desteği
XCHG Komutu
enter_region: MOVE REGISTER,#1 | yazmaca 1 koy XCHG REGISTER,LOCK | yazmaç ile LOCK'u takas et CMP REGISTER,#0 | LOCK 0 mıydı? JNE enter_region | değilse: döngü RET | kritik bölgeye girildi leave_region: MOVE LOCK,#0 | LOCK = 0 RET
- İki işlenenin içeriğini tek adımda değiştirir; etkisi TSL ile aynıdır.
- Intel x86'da kilitleme için kullanılan komuttur: bellek işlenenli
XCHGörtük olarakLOCKönekli, yani atomiktir. - C11 karşılığı:
atomic_exchange(&lock, 1).
Donanım Desteği · Kodu adım adım çalıştır
compare_and_swap ile Kilit
int compare_and_swap(int *v, int bekl, int yeni) { int eski = *v; /* üç satır birlikte */ if (eski == bekl) /* TEK atomik komut */ *v = yeni; return eski; } int lock = 0; void gir(void) { while (compare_and_swap(&lock, 0, 1) != 0) ; /* meşgul bekle */ } void cik(void) { lock = 0; }
Kilit boş.
T0'ın CAS'ı lock'u 0 bulur, 1 yazar ve 0 döndürür: döngüden çıkar, kritik bölgede.
T1'in CAS'ı 1 bulur: beklenen değer değil, hiçbir şey yazmaz, 1 döndürür.
T1 meşgul bekler; her deneme bellek sözcüğünü değiştirmeden döner.
T0 çıkarken kilidi 0 yapar (gerçek kodda release sıralamalı yazma).
T1'in sıradaki CAS'ı başarılı olur. Not: bu kilit sınırlı beklemeyi garanti etmez; şanssız bir iş parçacığı hep kaybedebilir.
Silberschatz 10e §6.4.2. x86: LOCK CMPXCHG; C11: atomic_compare_exchange_strong; Java: AtomicInteger.compareAndSet.
Donanım Desteği · Kodu adım adım çalıştır
CAS ile Atomik Artırma
void artir(atomic_int *v) { int temp; do { temp = *v; /* oku */ } while (temp != compare_and_swap(v, temp, temp + 1)); } /* değişmediyse yaz */ /* C11 tek satır: atomic_fetch_add(v, 1); */
İki iş parçacığı aynı sayacı artırmak istiyor; başlangıçta v = 5.
T1 değeri okur: temp = 5. Burada kesilebilir; kilit yoktur.
T2 de 5 okur. Düz v++ olsaydı bir artış kaybolacaktı.
T2'nin CAS'ı v'yi hâlâ 5 bulur, 6 yazar ve 5 döndürür: temp'e eşit → çıkar.
T1'in CAS'ı v'yi 6 bulur: beklenen 5 değil, yazmaz ve 6 döndürür → tekrar dene.
T1 güncel değeri okur.
Bu kez başarılı: v = 7. Hiçbir artış kaybolmadı ve kimse kilit tutmadı (kilitsiz güncelleme).
Bu “oku → hesapla → CAS → başarısızsa yeniden dene” döngüsü kilitsiz veri yapılarının temel kalıbıdır.
Donanım Desteği
Sınırlı Bekleme ile CAS
while (true) { waiting[i] = true; key = 1; while (waiting[i] && key == 1) key = compare_and_swap(&lock, 0, 1); waiting[i] = false; /* kritik bölge */ j = (i + 1) % n; while ((j != i) && !waiting[j]) j = (j + 1) % n; if (j == i) lock = 0; /* bekleyen yok */ else waiting[j] = false; /* sırayı devret */ /* kalan bölüm */ }
P1 kritik bölgede; P3 ve P4 waiting bayrağıyla bekliyor.
P1 çıkarken j = 2, 3, … sırasıyla tarar; ilk bekleyen P3'tür.
Kilidi bırakmaz, waiting[3] = false yapar: P3'ün döngüsü biter ve girer. Döngüsel tarama sınırlı beklemeyi sağlar.
Kilitler
Atomik Değişkenler
C11 <stdatomic.h>
#include <stdatomic.h> atomic_int sayac = 0; /* _Atomic int */ void *is(void *arg) { for (int n = 0; n < 100000; n++) atomic_fetch_add(&sayac, 1); return NULL; } /* 2 iş parçacığı + join → 200000 */
_Atomic türde sayac++ de atomiktir (varsayılan seq_cst). x86'da lock xadd, ARMv8.1'de ldadd olur.
Java AtomicInteger
static AtomicInteger atom = new AtomicInteger(); /* iki iş parçacığı, her biri 100_000 kez: */ atom.incrementAndGet(); var v = new AtomicInteger(5); v.compareAndSet(5, 6); /* true, v = 6 */ v.compareAndSet(5, 7); /* false, v = 6 */
AtomicInteger = 200000 synchronized = 200000 true 6 false 6
Kilitler
Spinlock ve Mutex
Spinlock: bekleyen iş parçacığı CPU'yu bırakmaz; kilit boşaldığı anda (bağlam değişimi olmadan) içeri girer.
Uyuyan (blocking) mutex: bekleyen bloke olur, CPU T3'e verilir; bedeli iki bağlam değişimidir.
| Spinlock | Mutex (uyuyan) | |
|---|---|---|
| Uygun | Çok çekirdek, KB iki bağlam değişiminden kısa | Uzun ya da bloke olabilen kritik bölge |
| Tek çekirdek | Anlamsız: tutan çalışamaz, bekleyen dilimini yakar | Doğru seçim |
| Örnek | Linux spin_lock, pthread_spin_lock | pthread_mutex_t, Java synchronized |
Eski sunum mutex'i “meşgul bekleyen kilit = spinlock” diye tanımlıyordu; bugün pthread_mutex bekleyeni uyutur. Çoğu gerçekleme önce kısa süre döner, sonra uyur.
Kilitler
Mutex Gerçeklemesi: yield ve futex
mutex_lock: TSL REGISTER,MUTEX | eskisini al, 1 yap CMP REGISTER,#0 | boş muydu? JZE ok | evet: alındı CALL thread_yield | hayır: CPU'yu ver JMP mutex_lock | sonra yine dene ok: RET mutex_unlock: MOVE MUTEX,#0 RET
- Tanenbaum Şekil 2-29:
enter_region'dan farkı döngü yerinethread_yield; kullanıcı düzeyi iş parçacıklarında meşgul beklemeyi önler. - Linux futex (fast userspace mutex): çekişme yoksa sistem çağrısı yapılmaz; glibc
pthread_mutex, Java kilitleri ve Go bunun üstüne kuruludur.
Çekişme yok: T1 sözcüğü tek CAS ile 0 → 1 yapar; çekirdeğe hiç girilmez.
T2 kilidi dolu bulur, değeri 2 (“bekleyen var”) yapar ve futex_wait ile uyur.
T1 bırakırken eski değer 2 olduğu için futex_wake çağırır; T2 uyanır ve kilidi alır.
Semaforlar
Semafor
- Dijkstra (1965): yalnızca iki atomik işlemle erişilen bir tamsayı.
wait(S)= P =down: değer 0 ise süreç bloke olur (uyur); değilse 1 azaltılır ve devam edilir.signal(S)= V =up: değer 1 artırılır; bekleyen varsa biri uyandırılır.
- Değer, kullanılabilir kaynak sayısı gibi düşünülebilir.
- Silberschatz gerçeklemesinde değer negatif olabilir:
−k, k sürecin beklediğini gösterir. wait/signal'ın kendisi kısa bir kritik bölgedir: tek çekirdekte kesme kapatılarak, çok çekirdekte spinlock ile korunur. Meşgul bekleme yok olmaz ama birkaç komuta iner.
typedef struct { int value; struct process *list; } semaphore;
down için “0 ise meşgul bekler” deniyordu. Dijkstra semaforunda ve gerçek sistemlerde bekleyen süreç uyur; meşgul bekleyen sürüm yalnızca kavramsal ilk tanımdır.Semaforlar · Kodu adım adım çalıştır
Semafor Bekleme Kuyruğu
void wait(semaphore *S) { /* P / down */ S->value--; if (S->value < 0) { ekle(S->list, bu_surec); sleep(); /* bloke ol */ } } void signal(semaphore *S) { /* V / up */ S->value++; if (S->value <= 0) { P = cikar(S->list); /* FIFO */ wakeup(P); } }
Kaynak bir tane: S 1 ile başlar (ikili semafor, mutex gibi).
T1 wait: değer 0 olur, negatif değil → T1 devam eder.
T2 wait: değer −1 → T2 kuyruğa eklenir ve uyur.
T3 de uyur. −2: iki süreç bekliyor.
T1 signal: değer −1, hâlâ ≤ 0 → kuyruğun başındaki T2 uyandırılır ve girer.
T2 signal: değer 0 → T3 uyandırılır.
T3 signal: değer 1, kimse beklemiyor. Kuyruk FIFO olduğu için bekleme sınırlıdır.
Silberschatz 10e §6.6.2. Kuyruk sırası tanımda belirtilmez; FIFO kullanılmazsa açlık mümkündür.
Semaforlar
Semafor Kullanım Kalıpları
wait … KB … signal.Yaygın hatalar
signal(m) … KB … wait(m) | Sıra ters: birden çok süreç KB'de |
wait(m) … KB … wait(m) | Kendini bekler: kalıcı bloke |
wait ya da signal unutulur | Dışlama bozulur ya da kilitlenme |
| İki semafor farklı sırayla alınır | Kilitlenme (sonraki slaytlar, Bölüm 7) |
Semafor güçlüdür ama yapısal değildir: doğruluk her wait/signal çiftine bağlıdır. Monitörler bu yüzden geliştirildi.
Klasik Problemler
Hatırlatma: Kayıp Uyandırma
void producer(void) { while (true) { item = produce_item(); if (count == N) sleep(); insert_item(item); count = count + 1; if (count == 1) wakeup(consumer); } }
void consumer(void) { while (true) { if (count == 0) sleep(); item = remove_item(); count = count - 1; if (count == N - 1) wakeup(producer); consume_item(item); } }
Tanenbaum Şekil 2-27; ayrıntılı iz Bölüm 3'te. Semafor “uyandırmayı” sayaç olarak sakladığı için sinyal kaybolmaz.
Klasik Problemler
Sınırlı Tampon: Semaforlarla
#define N 100 semaphore mutex = 1; /* tampona erişim */ semaphore empty = N; /* boş yuva sayısı */ semaphore full = 0; /* dolu yuva sayısı */ void producer(void) { while (true) { item = produce_item(); down(&empty); /* boş yuva bekle */ down(&mutex); /* KB'ye gir */ insert_item(item); up(&mutex); /* KB'den çık */ up(&full); /* dolu sayısı + 1 */ } }
void consumer(void) { while (true) { down(&full); /* dolu yuva bekle */ down(&mutex); item = remove_item(); up(&mutex); up(&empty); /* boş sayısı + 1 */ consume_item(item); } }
N ile QUEUE_SIZE karışmıştı.)Klasik Problemler · Kodu adım adım çalıştır
Sınırlı Tampon Adım Adım
semaphore mutex = 1, empty = 2, full = 0; void uretici(void) { while (true) { item = uret(); wait(&empty); wait(&mutex); tampona_koy(item); signal(&mutex); signal(&full); } } void tuketici(void) { while (true) { wait(&full); wait(&mutex); item = tampondan_al(); signal(&mutex); signal(&empty); tuket(item); } }
Tampon boş: 2 boş yuva, 0 dolu yuva.
Tüketici önce çalışır: full = 0 → uyur. Meşgul bekleme yok.
Üretici: empty 2→1, mutex 1→0, A'yı koyar.
signal(full) bekleyen tüketiciyi uyandırır; uyanan süreç bu “1”i kullandığı için full 0 kalır.
Tüketici henüz çalışmadı; üretici B'yi de koyar: empty = 0, full = 1.
C için boş yuva yok: üretici uyur. Tampon taşmaz.
Tüketici A'yı alır; signal(empty) uyuyan üreticiyi uyandırır.
Üretici C'yi boşalan yuvaya koyar. Her an: empty + full + yoldaki işlemler = N.
Klasik Problemler
down Sırası Değişirse
void producer(void) { while (true) { item = produce_item(); down(&mutex); /* ← önce */ down(&empty); /* ← sonra */ insert_item(item); up(&mutex); up(&full); } }
Üreticide iki down'ın yeri değiştirildi. Tampon tamamen dolu: empty = 0.
Üretici mutex'i alır, sonra down(&empty)'de uyur — mutex elindeyken.
Tüketici down(&full)'ü geçer ama down(&mutex)'te uyur. up(&empty) yapacak olan oydu.
Herkes ötekini bekliyor: kilitlenme. Kural: önce sayma semaforunu, sonra mutex'i bekle; mutex'i tutarken uyuma.
Klasik Problemler
Okur-Yazar Problemi
semaphore mutex = 1; /* rc'yi korur */ semaphore db = 1; /* veritabanını korur */ int rc = 0; /* okuyan sayısı */ void writer(void) { while (true) { think_up_data(); down(&db); write_data_base(); up(&db); } } void reader(void) { while (true) { down(&mutex); if (++rc == 1) down(&db); /* ilk okur */ up(&mutex); read_data_base(); down(&mutex); if (--rc == 0) up(&db); /* son okur */ up(&mutex); use_data_read(); } }
Okurlar birbirini dışlamaz; yalnız ilk okur db'yi alır.
Bir yazar gelir: down(&db)'de bekler.
Yeni okur R2 db'ye dokunmadan girer: rc = 2.
R1 çıkarken R3 gelir: rc 1'e inip yeniden 2 olur, hiç 0 olmaz.
Okur akışı sürdükçe yazar süresiz bekler: bu çözüm okur önceliklidir ve yazar açlığına açıktır.
Klasik Problemler
Okur-Yazar: Varyantlar ve API'ler
| Varyant | Davranış |
|---|---|
| 1. okur-yazar (okur öncelikli) | Yazar ancak hiç okur yokken girer; yazarlar aç kalabilir. |
| 2. okur-yazar (yazar öncelikli) | Bekleyen yazar varsa yeni okur giremez; okurlar aç kalabilir. |
| Adil (turnike) | Gelen herkes önce aynı turnikeden geçer; yazar beklerken turnikeyi tutar. |
pthread_rwlock_t | glibc varsayılanı okur öncelikli; yazar önceliği ayarlanabilir. |
Java ReentrantReadWriteLock | new ReentrantReadWriteLock(true) adil (FIFO'ya yakın) sıralama. |
| Linux seqlock, RCU | Okurların hiç kilit almadığı, okuma ağırlıklı çekirdek yapıları. |
/* Downey: açlıksız çözüm */ /* yazar */ wait(turnike); wait(odaBos); yaz(); signal(turnike); signal(odaBos); /* okur */ wait(turnike); signal(turnike); /* ilk okur odaBos'u alır, son okur bırakır (rc ile) */ oku();
Klasik Problemler
Yemek Yiyen Filozoflar: Naif Çözüm
semaphore catal[5]; /* hepsi 1 */ void filozof(int i) { while (true) { dusun(); wait(&catal[i]); /* sol */ wait(&catal[(i + 1) % 5]); /* sağ */ ye(); signal(&catal[(i + 1) % 5]); signal(&catal[i]); } }
- Dijkstra (1965): her filozof düşünür, acıkınca iki komşu çatalı alıp yer.
- Kısıtlı kaynakların süreçler arasında kilitlenmesiz ve açlıksız paylaştırılmasının modelidir.
Çatallar boşta, herkes düşünüyor.
Hepsi aynı anda acıkır ve sol çatalını alır…
…beşinci de alır. Şimdi herkes sağ çatalı bekliyor.
Kimse bırakmıyor: kilitlenme. Her çatal semaforla korunsa bile çözüm yanlıştır.
Klasik Problemler
Filozoflar: Çözüm Yolları
Başlangıcı 4 olan bir oda semaforu: en az bir filozof iki çatala ulaşır. Döngüsel bekleme kırılır.
Her filozof önce numarası küçük çatalı alır; F4 önce çatal 0'ı ister. Genel kural: kilitleri hep aynı global sırayla al.
Tanenbaum: state[] dizisi ve filozof başına semafor; yalnız iki komşu da yemiyorsa yemeye geç (sonraki slayt).
Silberschatz: pickup(i)/putdown(i) ve her filozof için koşul değişkeni self[i]; aynı test() mantığı.
Klasik Problemler · Kodu adım adım çalıştır
Tanenbaum Çözümü Adım Adım
void take_forks(int i) { down(&mutex); state[i] = HUNGRY; test(i); up(&mutex); down(&s[i]); /* çatal yoksa bloke */ } void put_forks(int i) { down(&mutex); state[i] = THINKING; test(LEFT); /* (i+4) % 5 */ test(RIGHT); /* (i+1) % 5 */ up(&mutex); } void test(int i) { if (state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING) { state[i] = EATING; up(&s[i]); } }
Başlangıç: herkes THINKING (T). s[i] semaforları 0.
F1 acıkır; test(1): komşular F0, F2 yemiyor → EATING, up(&s[1]); ardından down(&s[1]) hemen geçer.
F2 acıkır; sol komşu F1 yiyor → test başarısız, down(&s[2])'de uyur.
F3 acıkır; F2 aç ama yemiyor, F4 düşünüyor → F3 yer. Aynı anda iki filozof yiyebilir.
F1 bırakır, komşularını test eder: test(2) başarısız çünkü sağdaki F3 hâlâ yiyor.
F3 bırakınca test(2) başarılı: F2 EATING olur ve up(&s[2]) onu uyandırır.
Tanenbaum MOS Şekil 2-47. Kilitlenme yoktur; ancak F1 ve F3 sırayla yemeye devam ederse F2 teorik olarak aç kalabilir.
Monitörler
Monitör
- Hoare (1974) ve Brinch Hansen (1975): veri, prosedürler ve başlangıç kodunu bir arada tutan dil düzeyinde soyut veri türü.
- Süreçler monitörün prosedürlerini çağırabilir ama iç verisine doğrudan erişemez.
- Aynı anda monitör içinde yalnız bir süreç etkin olabilir; giriş kilidini derleyici ekler, programcı unutamaz.
- Monitör dolu ise çağıran giriş kuyruğunda bekler. Bir koşulu beklemek için koşul değişkenleri kullanılır.
- Semaforda
downsırasını karıştırmak kilitlenme yaratıyordu; monitör bu hata sınıfını azaltır.
monitor ornek integer i; condition c; procedure uretici(); … end; procedure tuketici(); … end; end monitor;
Monitörler
Koşul Değişkenleri
P monitörde; tampon boş olduğu için devam edemiyor. Q giriş kuyruğunda.
P x.wait(): monitörü bırakır ve x kuyruğunda uyur. Böylece Q girebilir.
Q koşulu sağlar ve x.signal() çağırır: x kuyruğundan bir süreç uyanır. Kimin çalışacağı anlamsal bir seçimdir (sonraki slayt).
x kuyruğunda kimse yokken x.signal() etkisizdir, hiçbir iz bırakmaz. Semaforun signal'ı ise değeri artırır ve hatırlanır.
| İşlem | Anlamı |
|---|---|
x.wait() | Çağıranı askıya al, monitörü serbest bırak. |
x.signal() | x'te bekleyen bir süreci (varsa) devam ettir. |
broadcast / notifyAll | x'te bekleyen herkesi uyandır. |
count > 0) her zaman paylaşılan veride ayrıca tutulur.Monitörler
Hoare ve Mesa Anlamı
Signal-and-wait (Hoare): uyanan P hemen çalışır, Q bekler. P, koşulun doğru olduğundan emin olabilir: if yeterlidir.
Signal-and-continue (Mesa, 1980): Q devam eder; P yalnızca “hazır” olur. P çalışana kadar başkası koşulu bozabilir: yeniden kontrol şart.
| Anlam | Kim hemen çalışır? | Kullananlar |
|---|---|---|
| Signal-and-wait (Hoare) | Uyandırılan | Ders kitabı monitörleri |
| Signal-and-exit (Brinch Hansen) | Uyandırılan; signal prosedürün son komutu olmalı | Concurrent Pascal |
| Signal-and-continue (Mesa) | Sinyal veren | Pthreads, Java, C#, Go sync.Cond, Linux çekirdeği |
Monitörler
Neden if Değil while?
/* YANLIŞ (Mesa'da) */ lock(&m); if (count == 0) cond_wait(&dolu, &m); x = al(); /* boş olabilir */ unlock(&m);
/* DOĞRU */ lock(&m); while (count == 0) cond_wait(&dolu, &m); x = al(); /* count > 0 kesin */ unlock(&m);
Tampon boş: tüketici T1 koşul değişkeninde bekliyor.
Üretici bir öğe koyar ve signal yapar. T1 uyanır ama önce kilidi geri alması gerekir.
Bu arada yeni gelen T2 kilidi önce kapar (barging) ve öğeyi alır: count = 0.
T1 sonunda çalışır. if ile koşulu yeniden sınamadan boş tampondan okur: hata.
while ile T1 koşulu yeniden sınar ve tekrar bekler. POSIX ayrıca sahte uyanmalara (spurious wakeup) izin verir: while her durumda zorunludur.
Monitörler
Monitörle Üretici-Tüketici
monitor ProducerConsumer
condition full, empty;
integer count;
procedure insert(item: integer);
begin
if count = N then wait(full);
insert_item(item);
count := count + 1;
if count = 1 then signal(empty)
end;
function remove: integer;
begin
if count = 0 then wait(empty);
remove := remove_item;
count := count - 1;
if count = N - 1 then signal(full)
end;
count := 0;
end monitor;- Tanenbaum Şekil 2-34 (Pidgin Pascal).
count'a erişim monitör tarafından zaten karşılıklı dışlanır. - Tampon doluysa üretici
wait(full)ile bloke olur ve monitörü bırakır; tüketici girip bir öğe alıncasignal(full)ile üreticiyi uyandırır. - Eski sunum bunu “dolu koşulunda sinyal yayınlayarak üreticinin bloke olması” diye anlatıyordu: bloke eden
wait'tir, uyandıransignal. - Burada
ifyeterlidir, çünkü Tanenbaum Brinch Hansen anlamını (signal son komut) varsayar. Pthreads/Java'da aynı kodwhileile yazılmalıdır. - Kayıp uyandırma olmaz: test ile
waitaynı monitör içinde, bölünmeden yapılır.
POSIX ve Java
Pthreads: Mutex ve Koşul Değişkeni
| Mutex çağrısı | Tanım | Koşul çağrısı | Tanım |
|---|---|---|---|
pthread_mutex_init | Mutex oluşturur | pthread_cond_init | Koşul değişkeni oluşturur |
pthread_mutex_destroy | Mutex'i yok eder | pthread_cond_destroy | Koşul değişkenini yok eder |
pthread_mutex_lock | Kilidi alır ya da bloke olur | pthread_cond_wait | Mutex'i atomik olarak bırakıp uyur; dönerken yeniden alır |
pthread_mutex_trylock | Alır ya da hemen EBUSY döner | pthread_cond_signal | Bekleyenlerden en az birini uyandırır |
pthread_mutex_unlock | Kilidi bırakır (yalnız sahibi) | pthread_cond_broadcast | Bekleyen tümünü uyandırır |
cond_wait her zaman mutex tutulurken ve bir while döngüsü içinde çağrılır.NORMAL, ERRORCHECK, RECURSIVE; öncelik kalıtımı için PTHREAD_PRIO_INHERIT.pthread_rwlock_*, pthread_spin_*, pthread_barrier_*, C11 mtx_t/cnd_t.Statik başlatma: PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER. cond_signal yalnız o koşulda bekleyenleri etkiler; bekleyen yoksa sinyal kaybolur.
POSIX ve Java · Kodu adım adım çalıştır
Pthreads Üretici-Tüketici Adım Adım
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t condc = PTHREAD_COND_INITIALIZER,
condp = PTHREAD_COND_INITIALIZER;
int buffer = 0; /* 0 = boş */
void *producer(void *p) {
for (int i = 1; i <= MAX; i++) { /* MAX = 2 */
pthread_mutex_lock(&m);
while (buffer != 0) pthread_cond_wait(&condp, &m);
buffer = i;
pthread_cond_signal(&condc);
pthread_mutex_unlock(&m);
} return NULL;
}
void *consumer(void *p) {
for (int i = 1; i <= MAX; i++) {
pthread_mutex_lock(&m);
while (buffer == 0) pthread_cond_wait(&condc, &m);
printf("%d\n", buffer); buffer = 0;
pthread_cond_signal(&condp);
pthread_mutex_unlock(&m);
} return NULL;
}Tüketici önce çalışır ve mutex'i alır.
Tampon boş: cond_wait mutex'i atomik olarak bırakır ve uyur. Kayıp uyandırma penceresi yoktur.
Üretici mutex'i alır; tampon boş olduğu için döngüyü atlar ve 1 yazar.
signal tüketiciyi uyandırır ama tüketici mutex'i geri almadan dönemez (Mesa).
Üretici yarışı kazanıp mutex'i yeniden almış olabilir: tampon dolu → bekler ve mutex'i bırakır.
Tüketici mutex'i alır, while koşulunu yeniden sınar (1 ≠ 0) ve öğeyi tüketir.
Tüketici condp'ye sinyal verir; üretici uyanır, tamponu boş bulur ve 2 yazar.
İkinci tur: tüketici 2'yi alır. Hangi sıra olursa olsun çıktı aynıdır.
POSIX ve Java
POSIX Semaforları
#include <semaphore.h> /* adsız: aynı sürecin iş parçacıkları */ sem_t bos, dolu; sem_init(&bos, 0, N); /* 0: süreç içi */ sem_init(&dolu, 0, 0); sem_wait(&bos); /* … koy … */ sem_post(&dolu); sem_destroy(&bos); /* adlandırılmış: ilgisiz süreçler arası */ sem_t *s = sem_open("/os06", O_CREAT, 0644, 1); sem_wait(s); /* KB */ sem_post(s); sem_close(s); sem_unlink("/os06");
sem_wait | P / down: 0 ise bloke |
sem_trywait, sem_timedwait | Beklemeden ya da süre sınırıyla dene |
sem_post | V / up; sinyal işleyicisinden çağrılabilir |
sem_getvalue | Anlık değer (yalnız bilgi amaçlı) |
sem_init'in ikinci argümanı 1 ise semafor süreçler arası paylaşılan bellekte olmalıdır.- macOS adsız semaforları desteklemez (
sem_initkullanımdan kalktı); adlandırılmış semafor ya dadispatch_semaphorekullanılır. - Java:
java.util.concurrent.Semaphore—acquire(),release(), isteğe bağlı adil kip.
POSIX ve Java
Java: synchronized, wait ve notify
public class TekYuva { private int yuva = 0; /* 0 = boş */ synchronized void koy(int x) throws InterruptedException { while (yuva != 0) wait(); /* kilidi bırakır */ yuva = x; notifyAll(); } synchronized int al() throws InterruptedException { while (yuva == 0) wait(); int x = yuva; yuva = 0; notifyAll(); return x; } /* main: üretici 10, 20, 30, 40 koyar; main dört kez al() ile okuyup yazdırır */ }
- Her Java nesnesi bir monitördür: içsel kilit + tek bir bekleme kümesi (wait set).
synchronizedmetot/blok kilidi alır, çıkışta (istisnada bile) bırakır; kilit yeniden girilebilir.wait()kilidi bırakıp uyur;notify()birini,notifyAll()hepsini uyandırır. Anlam Mesa'dır:whileşart.- Üretici ve tüketici aynı bekleme kümesini paylaştığı için
notifyAlldaha güvenlidir.
alındı 10 alındı 20 alındı 30 alındı 40
POSIX ve Java
Java: ReentrantLock ve Condition
class Tampon { private final int[] buf = new int[2]; private int bas = 0, son = 0, adet = 0; private final ReentrantLock kilit = new ReentrantLock(); private final Condition bosYer = kilit.newCondition(); private final Condition dolu = kilit.newCondition(); void koy(int x) throws InterruptedException { kilit.lock(); try { while (adet == buf.length) bosYer.await(); buf[son] = x; son = (son + 1) % buf.length; adet++; dolu.signal(); } finally { kilit.unlock(); } } /* al(): while (adet == 0) dolu.await(); … bosYer.signal(); */ } /* üretici 1..5 koyar; main 5 kez al() */
java.util.concurrent.locks: bir kilide birden çok koşul bağlanabilir;signaldoğru grubu uyandırır.tryLock(), süre sınırlı ve kesilebilir kilitleme, isteğe bağlı adil kip.unlock()mutlakafinallyiçinde.- Java 21 sanal iş parçacıkları
synchronizediçinde bloke olunca taşıyıcı iş parçacığını “sabitliyordu”; JDK 24 (JEP 491) bunu giderdi.
1 2 3 4 5 | toplam = 15 1 2 3 4 5 | toplam = 15 1 2 3 4 5 | toplam = 15
Canlılık
Öncelik Terslemesi
Düşük öncelikli L, paylaşılan kaynak R'nin kilidini alır.
Yüksek öncelikli H gelir, L'yi keser; R'yi ister ve bloke olur. Kısa bir bekleme beklenir: L çıkınca H girer.
Ama R ile ilgisi olmayan orta öncelikli M, L'yi keser ve uzun süre çalışır. H dolaylı olarak M'yi bekler: öncelik terslemesi.
Öncelik kalıtımı: H, R'yi beklerken L geçici olarak H'nin önceliğini alır; M araya giremez. L çıkar, H hemen çalışır.
Eski sunumda “öncelikleri değiştirme (priority inversion) çözüm olabilir” deniyordu: terslemenin kendisi sorundur; çözüm öncelik kalıtımı (inheritance) ya da öncelik tavanıdır (ceiling). Spinlock'la meşgul bekleyen H, tek çekirdekte L'yi hiç çalıştırmaz: sistem kilitlenir.
Canlılık
Mars Pathfinder (1997)
- Temmuz 1997: Mars yüzeyindeki Pathfinder, VxWorks gerçek zamanlı işletim sisteminde çalışırken tekrar tekrar kendini sıfırladı.
- Düşük öncelikli meteoroloji görevi veriyolu mutex'ini tutarken yüksek öncelikli veriyolu yönetim görevi bloke oldu; orta öncelikli iletişim görevi araya girdi.
- Yönetim görevi zamanında çalışamayınca bekçi zamanlayıcı “sistem asılı kaldı” deyip tam sıfırlama yaptı.
- JPL mühendisleri hatayı yerdeki kopyada üretti; mutex'in öncelik kalıtımı seçeneği uzaktan yüklenen bir yamayla açıldı.
- Bugün:
PTHREAD_PRIO_INHERIT, Linux PI-futex vert_mutex; Linux 6.12'de ana dala giren PREEMPT_RT bu mekanizmaya dayanır.
Canlılık
Canlılık Sorunları
Kilitsiz Programlama
Kilitsiz (Lock-free) Yapılara Giriş
void push(node_t *n) { node_t *eski = atomic_load(&top); do { n->next = eski; } while (!atomic_compare_exchange_weak( &top, &eski, n)); } /* başarısızsa eski = güncel top */
| Bloke eden | Kilidi tutan durursa herkes durur |
| Kilitsiz | Sistem bütün olarak her zaman ilerler |
| Beklemesiz | Her iş parçacığı sınırlı adımda biter |
Yığın: top → A → B. Kilit yerine tek bir atomik işaretçi.
T1, X'i eklemeye hazırlanır: eski = A, X.next = A.
Bu arada T2, Y'yi ekler: CAS(top, A, Y) başarılı.
T1'in CAS'ı başarısız olur (top artık Y). C11 compare_exchange güncel değeri eski'ye yazar; T1 X.next = Y ile yeniden dener.
İkinci CAS başarılı. ABA sorunu: pop'ta top A→B→A değişirse CAS bunu fark etmez; sürüm sayacı ya da güvenli bellek geri kazanımı (hazard pointer, epoch) gerekir.
Özet
Özet
| Araç | Ne sağlar? | Ne zaman? |
|---|---|---|
| Peterson, katı sıra | Yalnız okuma/yazma ile dışlama (SC varsayımıyla) | Kavramsal; gerçek kodda kullanılmaz |
| Bellek bariyeri | Okuma/yazma sırası ve görünürlük | Kilit ve atomik gerçeklemelerinin içinde |
| TAS, XCHG, CAS | Atomik okuma-değiştirme-yazma | Kilitlerin ve kilitsiz yapıların temeli |
| Atomik değişken | Tek değişkenin bölünmez güncellemesi | Sayaç, bayrak, referans sayımı |
| Spinlock | Dışlama, uyumadan | Çok çekirdek, çok kısa KB, çekirdek içi |
| Mutex | Dışlama, bekleyen uyur | Genel amaçlı kritik bölgeler |
| Semafor | Sayma, dışlama ve sıralama | N özdeş kaynak, olay bildirimi |
| Monitör + koşul değişkeni | Yapısal dışlama + koşul bekleme | Karmaşık koşullar (Java, pthreads) |
| Okur-yazar kilidi | Çoklu okur, tek yazar | Okuma ağırlıklı veriler |
Her araç karşılıklı dışlamayı sağlar; ilerleme ve sınırlı bekleme doğru kullanım ve adil kuyruklarla gelir. Öncelik terslemesi ve kilitlenme ayrıca düşünülmelidir.
Özet
Kontrol Soruları
- Kritik bölge çözümünün üç koşulunu yazın. Katı sırayla değişim hangisini ihlal eder?
- Peterson algoritması x86 üzerinde neden yanlış çalışabilir? Nasıl düzeltilir?
test_and_setile kurulan basit spinlock hangi koşulu garanti etmez?- CAS ile artırmada iki iş parçacığı aynı değeri okursa ne olur? Son değer neden doğrudur?
- Başlangıç değeri 2 olan bir semaforda üç süreç art arda
waityaparsa değer ve kuyruk ne olur (Silberschatz gerçeklemesi)? - Sınırlı tampon çözümünde üreticideki iki
down'ın yeri değişirse ne olur? - Mesa anlamında koşul değişkeni beklemesi neden
whileiçinde yapılmalıdır? - Öncelik terslemesini tanımlayın; öncelik kalıtımı onu nasıl çözer?
Cevaplar
- Karşılıklı dışlama, ilerleme, sınırlı bekleme; katı sıra ilerlemeyi ihlal eder.
- Yazma tamponu yazmayı sonraki okumadan sonraya bırakır, ikisi de
falseokur; tam bariyer ya daseq_cstatomikler. - Sınırlı bekleme: aynı iş parçacığı hep kaybedebilir.
- Biri başarılı olur, diğerinin CAS'ı başarısız olup yeniden okur; artış kaybolmaz.
- Değer −1; ilk ikisi geçer, üçüncü kuyrukta bekler.
- Tampon doluyken üretici mutex'i tutarak uyur, tüketici mutex'te uyur: kilitlenme.
- Uyanan iş parçacığı hemen çalışmaz; koşul bu arada bozulabilir, sahte uyanma da olabilir.
- Yüksek öncelikli görev, düşük öncelikli görevin tuttuğu kilidi beklerken orta öncelikli görevler araya girer; kalıtımda kilit sahibi bekleyenin önceliğini geçici olarak alır.
Özet
Kaynaklar
- A. Silberschatz, P. B. Galvin, G. Gagne, Operating System Concepts, 10. baskı, Wiley, 2018 — Bölüm 6: Synchronization Tools, Bölüm 7: Synchronization Examples.
- A. S. Tanenbaum, H. Bos, Modern Operating Systems, 4. baskı (2014) / 5. baskı (2022), Pearson — §2.3 Interprocess Communication, §2.5 Classical IPC Problems.
- R. H. Arpaci-Dusseau, A. C. Arpaci-Dusseau, Operating Systems: Three Easy Pieces (OSTEP), ostep.org — “Locks”, “Condition Variables”, “Semaphores”, “Common Concurrency Problems”.
- A. B. Downey, The Little Book of Semaphores, 2. baskı, Green Tea Press (ücretsiz) — okur-yazar ve filozof çözümleri.
- POSIX.1-2017 (IEEE Std 1003.1):
pthread_mutex_lock,pthread_cond_wait,sem_overview(7);futex(2)Linux kılavuz sayfası. - B. Goetz vd., Java Concurrency in Practice, 2006; Java SE 21 API:
java.util.concurrent; JEP 491. - G. L. Peterson, “Myths About the Mutual Exclusion Problem”, IPL, 1981; M. Jones, “What really happened on Mars?”, 1997.