Bölüm 6: Senkronizasyon

İşletim Sistemleri
Yarış durumuKritik bölge gereksinimleriPeterson ve bellek modeli Bellek bariyerleritest_and_set, CASAtomik değişkenler Spinlock ve mutexSemaforlarKlasik problemler Monitör ve koşul değişkeniPOSIX ve Java APIÖncelik terslemesi, kilitsiz yapılar
Sercan KÜLCÜ · Giresun Üniversitesi · Bilgisayar Mühendisliği

Giriş

Öğrenme Çıktıları

Problem

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.

Yazılım ve bellek modeli

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.

Donanım desteği

test_and_set, compare_and_swap, XCHG ve atomik değişkenlerle kilit ve sayaç gerçeklemek.

Araçlar

Spinlock, mutex, semafor, monitör ve koşul değişkenini karşılaştırmak; Mesa anlamındaki while kuralını uygulamak.

Klasik problemler

Sınırlı tampon, okur-yazar ve yemek yiyen filozoflar problemlerini çözmek; açlık ve kilitlenme risklerini görmek.

Gerçek sistemler

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).
paylaşılan vericount = 5T1T2ikisi de: oku → değiştir → yazkesme / bağlam değişimigüncelleme yarıda kalır

Giriş

Yarış Durumu: fork() ve pid

P0P1pid = fork();pid = fork();next_available_pid2615Silberschatz Şekil 6.2'den uyarlanmıştır

Çekirdekte sıradaki boş pid next_available_pid değişkeninde tutulur: 2615. P0 ve P1 aynı anda fork() çağırır.

P0P1pid = fork();pid = fork();next_available_pid2615okur: 2615artırmadan önce kesildiSilberschatz Şekil 6.2'den uyarlanmıştır

P0'ın çekirdek kodu değeri okur (2615) ama artırıp yazmadan önce kesilir.

P0P1pid = fork();pid = fork();next_available_pid2616okur: 2615okur: 2615çocuk pid 2615Silberschatz Şekil 6.2'den uyarlanmıştır

P1 de 2615 okur, çocuğuna verir ve 2616 yazar.

P0P1pid = fork();pid = fork();next_available_pid2616okur: 2615okur: 2615çocuk pid 2615çocuk pid 2615aynı pid iki sürece verildi!ikisi de 2616 yazdı; bir artış kaybolduSilberschatz Şekil 6.2'den uyarlanmıştır

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) */
}
giriş bölümükritik bölgeçıkış bölümükalan bölümdöngüizin iste → kullan → bırak

Kritik Bölge

Bir Çözümün Üç Koşulu

1. Karşılıklı dışlamaPi kritik bölgesindeyken başka hiçbir süreç kendi kritik bölgesinde yürütülemez.
2. İlerlemeKritik bölge boşsa ve girmek isteyenler varsa, kimin gireceğine yalnızca kalan bölümde olmayanlar karar verir ve bu karar süresiz ertelenemez.
3. Sınırlı beklemeBir süreç istek yaptıktan sonra, isteği karşılanana kadar diğerlerinin kaç kez girebileceğinin bir üst sınırı vardır (açlık olmaz).
Tanenbaum'un dört koşuluKarşı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

T1T2T3T4ABA kritik bölgedezaman →

T1: A kritik bölgesine girer.

T1T2T3T4ABA kritik bölgedeB blokezaman →

T2: B girmek ister; bölge dolu olduğu için bloke olur (ya da meşgul bekler).

T1T2T3T4ABA kritik bölgedeB blokeB kritik bölgedezaman →

T3: A çıkar; bekleyen B hemen girer. Hiçbir anda iki süreç bölgede değildir.

T1T2T3T4ABA kritik bölgedeB blokeB kritik bölgedezaman →

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/sti ayrı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 veride spin_lock_irqsave().
Çekirdek 0Çekirdek 1cli · kesmeler kapalıkesmeler açık, çalışıyorpaylaşılan veri(ör. hazır kuyruğu)kesme kapatmak yalnız yerel çekirdeği durdurur

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(); */
P0kritik bölge dışındaP1kritik bölge dışında

Paylaşılan lock 0: bölge boş.

P0lock != 0 ? → hayırP1hazır

P0 lock'u okur: 0. Döngüden çıkar…

P0kesildi (satır 6'dan önce)P1lock != 0 ? → hayır

…ama lock = 1 yazmadan önce kesilir. P1 de 0 okur.

P0hazırP1lock = 1 → girdi

P1 kilidi 1 yapar ve kritik bölgeye girer.

P0lock = 1 → girdiP1kritik bölgede

P0 devam eder; zaten test ettiği için o da 1 yazar ve girer.

P0kritik bölgedeP1kritik bölgedeiki süreç birden kritik bölgede!

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();
}
P0KBP1bekleturn = 0

turn = 0: P0 girer, P1 meşgul bekler (KB kritik bölge, KDB kritik dışı bölüm).

P0KBKDBbekleP1bekleKBturn=1turn = 1

P0 çıkarken turn = 1 yapar; P1 girer. Karşılıklı dışlama sağlanır.

P0KBKDBbekleKBKDBP1bekleKBkritik dışı (uzun)turn=1turn=0turn=1turn = 1

P1 çıkar (turn = 0) ve uzun bir kritik dışı işe başlar. P0 bir kez daha girip çıkar: turn = 1.

P0KBKDBbekleKBKDBbekle (turn = 1)P1bekleKBkritik dışı (uzun)turn=1turn=0turn=1turn = 1bölge boş ama P0 giremiyor →

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.

Karşılıklı dışlamaİkisi de içerideyse flag[0] = flag[1] = true olmalı; ama turn aynı anda hem 0 hem 1 olamaz. turn'e son yazan bekler.
İlerlemePj ilgilenmiyorsa flag[j] = false: Pi hiç beklemeden girer.
Sınırlı beklemePj çıkıp yeniden girmek isterse turn = i yapar; Pi en fazla bir giriş bekler.
Bedeli: meşgul bekleme CPU yakar. Daha önemlisi, kanıt sıralı tutarlılık varsayar; modern donanımda bu varsayım yoktur →

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;
Çekirdek 0 · P0yazma tamponuÇekirdek 1 · P1yazma tamponuortak bellekflag[0]falseflag[1]falseturn0

Başlangıç: bellekte iki bayrak false. Her çekirdeğin yazmaları önce kendi yazma tamponuna gider.

Çekirdek 0 · P0yazma tamponuflag[0] = trueturn = 1Çekirdek 1 · P1yazma tamponuortak bellekflag[0]falseflag[1]falseturn0

P0 iki yazmasını yapar; ikisi de tamponda bekler, bellekte henüz görünmez.

Çekirdek 0 · P0yazma tamponuflag[0] = trueturn = 1Çekirdek 1 · P1yazma tamponuflag[1] = trueturn = 0ortak bellekflag[0]falseflag[1]falseturn0

P1 de aynısını yapar. x86 bile (TSO) yazmayı sonraki okumadan sonraya bırakabilir.

Çekirdek 0 · P0yazma tamponuflag[0] = trueturn = 1KB · girdiÇekirdek 1 · P1yazma tamponuflag[1] = trueturn = 0ortak bellekflag[0]falseflag[1]falseturn0

P0 flag[1]'i bellekten okur: false → döngüden çıkar.

Çekirdek 0 · P0yazma tamponuflag[0] = trueturn = 1KB · girdiÇekirdek 1 · P1yazma tamponuflag[1] = trueturn = 0KB · girdiortak bellekflag[0]falseflag[1]falseturn0

P1 de flag[0]'ı false okur ve girer: ikisi birden kritik bölgede!

Çekirdek 0 · P0yazma tamponuKBÇekirdek 1 · P1yazma tamponuKBortak bellekflag[0]trueflag[1]trueturn0

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.
DerleyiciTek iş parçacığı için eşdeğerse her erişimi taşıyabilir, kaydedebilir ya da silebilir.
C'deki 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

İş parçacığı 1 (okuyan)İş parçacığı 2 (yazan)while (!flag) ;print(x);x = 100;flag = true;belleğin diğer çekirdeğe göründüğü sıraflag = trueT1 okur x → 0x = 100yazmalar yer değiştirdi: çıktı 0

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.

İş parçacığı 1 (okuyan)İş parçacığı 2 (yazan)while (!flag) ;print(x);x = 100;flag = true;belleğin diğer çekirdeğe göründüğü sıraT1 okur x → 0x = 100flag = trueokumalar yer değiştirdi: x, flag'den önce okundu

Ya da T1'in kendisi x'i flag'den önce okuyabilir (okuma-okuma yeniden sıralaması).

İş parçacığı 1 (okuyan)İş parçacığı 2 (yazan)while (!flag) ;memory_barrier();print(x);x = 100;memory_barrier();flag = true;belleğin diğer çekirdeğe göründüğü sırax = 100flag = trueT1 okur x → 100bariyerler sırayı zorlar: çıktı 100

Bellek bariyeri (fence): öncesindeki tüm okuma/yazmalar tamamlanmadan sonrakiler başlamaz. T2: x önce; T1: flag önce.

İş parçacığı 1 (okuyan)İş parçacığı 2 (yazan)while (!flag) ;memory_barrier();print(x);x = 100;memory_barrier();flag = true;belleğin diğer çekirdeğe göründüğü sırax = 100store-release flagload-acquire flag → x = 100C11 release/acquire ya da Java volatile aynı garantiyi verir

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[] flag yalnız dizi referansını volatile yapar, elemanları değil.
    • getId() % N iki iş parçacığına aynı i'yi verebilir.
    • Kritik bölgedeki print ölçümü bozar.
  • Java'da volatile okuma/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 (x86 pause).
Çıktı (JDK 21, üç çalıştırma)
sayac = 200000
sayac = 200000
sayac = 200000

Bellek Modeli

Senkronizasyon Bariyeri

bariyerABCDher süreç kendi fazını hesaplıyor

Fazlara bölünmüş paralel hesap (ör. ızgara simülasyonu): her süreç bir dilimi hesaplar.

bariyerABCDA, B, D bariyerde bloke · C çalışıyor

Bitiren süreç barrier_wait() çağırır ve bloke olur; en yavaş süreç (C) beklenir.

bariyerABCDson süreç geldi: hepsi sonraki faza

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();
   } */
Çıktı (JDK 21)
faz 0 tamam
faz 1 tamam
Eski sunumda “bariyer” başlığı iki farklı kavramı karıştırıyordu: senkronizasyon bariyeri bir süreç grubunu buluşturur (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).
MimariAtomik okuma-değiştirme-yazma
x86-64XCHG, LOCK CMPXCHG, LOCK XADD, LOCK BTS
ARMv8LDXR/STXR (LL/SC); v8.1 LSE: CAS, LDADD, SWP
RISC-V “A”LR/SC, AMOSWAP, AMOADD
Tanenbaum'un sanal CPU'suTSL 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
CPU 0REGISTER = –kritik bölge dışındaCPU 1REGISTER = –kritik bölge dışındaLOCK (bellek)0

Kilit boş: LOCK = 0.

CPU 0REGISTER = 0TSL yürüttüCPU 1REGISTER = –hazırLOCK (bellek)1

CPU 0 TSL: eski değer (0) yazmaca, LOCK'a 1 — tek bölünmez adımda.

CPU 0REGISTER = 0TSL yürüttüCPU 1REGISTER = 1TSL yürüttüLOCK (bellek)1

CPU 1 de TSL yürütür: yazmaca 1 gelir; LOCK zaten 1'dir.

CPU 0REGISTER = 0CMP: 0 → RET, KB'deCPU 1REGISTER = 1JNE → döngü (spin)LOCK (bellek)1

CPU 0 sıfır gördü: kritik bölgede. CPU 1 sıfır olmayan değer gördü: meşgul bekler.

CPU 0REGISTER = 0MOVE LOCK,#0 → çıktıCPU 1REGISTER = 1hâlâ deniyorLOCK (bellek)0

CPU 0 çıkarken sıradan bir yazmayla LOCK'u 0 yapar.

CPU 0REGISTER = 0kalan bölümCPU 1REGISTER = 0TSL → 0 gördü, giriyorLOCK (bellek)1

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 olarak LOCK önekli, yani atomiktir.
  • C11 karşılığı: atomic_exchange(&lock, 1).
REGISTERönce 1LOCKönce 0 ya da 1atomik takassonra: REGISTER = eski LOCKLOCK = 1 (her durumda)eski LOCK 0 ise kilit alındı

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; }
T0dışarıdaT1dışarıda

Kilit boş.

T0CAS(0→1): eski 0 → kazandıT1hazır

T0'ın CAS'ı lock'u 0 bulur, 1 yazar ve 0 döndürür: döngüden çıkar, kritik bölgede.

T0kritik bölgedeT1CAS: eski 1 ≠ 0 → yazmaz

T1'in CAS'ı 1 bulur: beklenen değer değil, hiçbir şey yazmaz, 1 döndürür.

T0kritik bölgedeT1döngüde yeniden deniyor

T1 meşgul bekler; her deneme bellek sözcüğünü değiştirmeden döner.

T0cik(): lock = 0T1döngüde

T0 çıkarken kilidi 0 yapar (gerçek kodda release sıralamalı yazma).

T0kalan bölümT1CAS(0→1) kazandı → girdi

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); */
T1artir(v) çağrıldıT2artir(v) çağrıldıv = 5

İki iş parçacığı aynı sayacı artırmak istiyor; başlangıçta v = 5.

T1temp = 5T2hazırv = 5

T1 değeri okur: temp = 5. Burada kesilebilir; kilit yoktur.

T1temp = 5T2temp = 5v = 5

T2 de 5 okur. Düz v++ olsaydı bir artış kaybolacaktı.

T1temp = 5T2CAS(5→6) başarılıv = 6

T2'nin CAS'ı v'yi hâlâ 5 bulur, 6 yazar ve 5 döndürür: temp'e eşit → çıkar.

T1CAS(5→6) başarısız: v = 6T2bittiv = 6

T1'in CAS'ı v'yi 6 bulur: beklenen 5 değil, yazmaz ve 6 döndürür → tekrar dene.

T1temp = 6 (yeniden okudu)T2bittiv = 6

T1 güncel değeri okur.

T1CAS(6→7) başarılıT2bittiv = 7

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 */
}
P0–P1KBP2–P3waitingP4waitinglock = 1en fazla n − 1 = 4 sıra beklenir

P1 kritik bölgede; P3 ve P4 waiting bayrağıyla bekliyor.

P0–P1KBP2–P3waitingP4waitingj = 2: bekleyen yokj = 3: bekliyor ✓en fazla n − 1 = 4 sıra beklenir

P1 çıkarken j = 2, 3, … sırasıyla tarar; ilk bekleyen P3'tür.

P0–P1–P2–P3KBP4waitingwaiting[3] = falselock hâlâ 1en fazla n − 1 = 4 sıra beklenir

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 */
Çıktı (JDK 21, üç çalıştırma)
AtomicInteger = 200000
synchronized  = 200000
true 6
false 6
Atomik değişken tek bir değişkenin güncellemesini korur. Birden çok değişkeni birlikte tutarlı tutmak (ör. tampon dizisi + sayaç) için yine kilit ya da monitör gerekir.

Kilitler

Spinlock ve Mutex

Çekirdek 1, T1 kilidi tutarken T2'nin beklemesi:spinlockT2 döner: while(test_and_set)…T2 KBT1 bırakır

Spinlock: bekleyen iş parçacığı CPU'yu bırakmaz; kilit boşaldığı anda (bağlam değişimi olmadan) içeri girer.

Çekirdek 1, T1 kilidi tutarken T2'nin beklemesi:spinlockT2 döner: while(test_and_set)…T2 KBmutexT2uyuT3 çalışır (yararlı iş)uyanT2 KBT1 bırakır2 bağlam değişimi

Uyuyan (blocking) mutex: bekleyen bloke olur, CPU T3'e verilir; bedeli iki bağlam değişimidir.

SpinlockMutex (uyuyan)
UygunÇok çekirdek, KB iki bağlam değişiminden kısaUzun ya da bloke olabilen kritik bölge
Tek çekirdekAnlamsız: tutan çalışamaz, bekleyen dilimini yakarDoğru seçim
ÖrnekLinux spin_lock, pthread_spin_lockpthread_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ü yerine thread_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.
kullanıcı alanıçekirdekkilit sözcüğü1T1T2futex bekleme kuyruğuCAS 0 → 1: sistem çağrısı yokboş

Çekişme yok: T1 sözcüğü tek CAS ile 0 → 1 yapar; çekirdeğe hiç girilmez.

kullanıcı alanıçekirdekkilit sözcüğü2T1T2futex bekleme kuyruğuCAS başarısız → değer 2futex_waitT2

T2 kilidi dolu bulur, değeri 2 (“bekleyen var”) yapar ve futex_wait ile uyur.

kullanıcı alanıçekirdekkilit sözcüğü0 → 1T1T2futex bekleme kuyruğuT1 bırakır: 2 görür → uyandırfutex_wakeT2 uyandı, yeniden dener

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;
Eski sunumda 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);
  }
}
S.value1S.list (bekleyenler)boşkritik bölge

Kaynak bir tane: S 1 ile başlar (ikili semafor, mutex gibi).

S.value0S.list (bekleyenler)boşkritik bölgeT1

T1 wait: değer 0 olur, negatif değil → T1 devam eder.

S.value−1S.list (bekleyenler)T2kritik bölgeT1

T2 wait: değer −1 → T2 kuyruğa eklenir ve uyur.

S.value−2S.list (bekleyenler)T2T3kritik bölgeT1

T3 de uyur. −2: iki süreç bekliyor.

S.value−1S.list (bekleyenler)T3kritik bölgeT2

T1 signal: değer −1, hâlâ ≤ 0 → kuyruğun başındaki T2 uyandırılır ve girer.

S.value0S.list (bekleyenler)boşkritik bölgeT3

T2 signal: değer 0 → T3 uyandırılır.

S.value1S.list (bekleyenler)boşkritik bölge

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ı

İkili (mutex gibi)Başlangıç 1: wait … KB … signal.
SaymaBaşlangıç N: N özdeş kaynak (bağlantı havuzu, tampon yuvaları).
Sinyal / sıralamaBaşlangıç 0: bir olay gerçekleşene kadar bekle (sağdaki şekil).

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 unutulurDışlama bozulur ya da kilitlenme
İki semafor farklı sırayla alınırKilitlenme (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.

P1P2S1;signal(synch);wait(synch);S2;synch = 0 ile başlar

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);
  }
}
T:count == 0 okurkesilir(uyumadan)Ü:count = 1wakeup(T)T uyumuyor:sinyal kaybolduT:sleep()Ü: tampondolar, sleep()zaman →Sonuç: ikisi de sonsuza dek uyur. Sorun, count testi ile sleep() arasının atomik olmaması.Ü = üretici · T = tüketici

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);
  }
}
mutex = 1Karşılıklı dışlama. (Eski sunumda başlangıç 0 yazıyordu: hiç kimse giremezdi.)
empty = NÜretici tampon doluyken bloke olur. (Eski kodda N ile QUEUE_SIZE karışmıştı.)
full = 0Tüketici tampon boşken bloke olur: sıralama (senkronizasyon) görevi.

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);
  }
}
ÜbaşlıyorTbaşlıyortampon (N = 2)

Tampon boş: 2 boş yuva, 0 dolu yuva.

ÜhazırTwait(full) → bloketampon (N = 2)

Tüketici önce çalışır: full = 0 → uyur. Meşgul bekleme yok.

ÜA'yı koydu (KB)Tbloketampon (N = 2)A

Üretici: empty 2→1, mutex 1→0, A'yı koyar.

Üsignal(full)Tuyandı (hazır)tampon (N = 2)A

signal(full) bekleyen tüketiciyi uyandırır; uyanan süreç bu “1”i kullandığı için full 0 kalır.

ÜB'yi koyduThazırtampon (N = 2)AB

Tüketici henüz çalışmadı; üretici B'yi de koyar: empty = 0, full = 1.

Üwait(empty) → blokeThazırtampon (N = 2)AB

C için boş yuva yok: üretici uyur. Tampon taşmaz.

ÜuyandıTA'yı aldıtampon (N = 2)B

Tüketici A'yı alır; signal(empty) uyuyan üreticiyi uyandırır.

ÜC'yi koyduTtuket(A)tampon (N = 2)CB

Ü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);
  }
}
ÜreticiTüketicimutexempty = 0tampon dolu

Üreticide iki down'ın yeri değiştirildi. Tampon tamamen dolu: empty = 0.

ÜreticiTüketicimutexempty = 0tampon dolututuyorbekliyor

Üretici mutex'i alır, sonra down(&empty)'de uyur — mutex elindeyken.

ÜreticiTüketicimutexempty = 0tampon dolututuyorbekliyorbekliyorup(empty)yapacaktı

Tüketici down(&full)'ü geçer ama down(&mutex)'te uyur. up(&empty) yapacak olan oydu.

ÜreticiTüketicimutexempty = 0tampon dolututuyorbekliyorbekliyorup(empty)yapacaktıdöngüsel bekleme= kilitlenme

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();
  }
}
veritabanıR1rc = 1 · db alınmışilk okur db'yi kilitler

Okurlar birbirini dışlamaz; yalnız ilk okur db'yi alır.

veritabanıR1rc = 1 · db alınmışWdown(db)yazar bekler

Bir yazar gelir: down(&db)'de bekler.

veritabanıR1R2rc = 2 · db alınmışWdown(db)yeni okur hemen girer

Yeni okur R2 db'ye dokunmadan girer: rc = 2.

veritabanıR2R3rc = 2 · db alınmışWdown(db)R1 çıktı, R3 geldirc hiç 0 olmuyor

R1 çıkarken R3 gelir: rc 1'e inip yeniden 2 olur, hiç 0 olmaz.

veritabanıR3R4rc = 2 · db alınmışWdown(db)R2 çıktı, R4 geldi …yazar aç kalır

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

VaryantDavranış
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_tglibc varsayılanı okur öncelikli; yazar önceliği ayarlanabilir.
Java ReentrantReadWriteLocknew ReentrantReadWriteLock(true) adil (FIFO'ya yakın) sıralama.
Linux seqlock, RCUOkurları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.
hepsidüşünüyorF0F1F2F3F4

Çatallar boşta, herkes düşünüyor.

sol çatallaralınıyorF0F1F2F3F4

Hepsi aynı anda acıkır ve sol çatalını alır…

hepsi sağıbekliyorF0F1F2F3F4

…beşinci de alır. Şimdi herkes sağ çatalı bekliyor.

KİLİTLENMEdöngüsel beklemeF0F1F2F3F4

Kimse bırakmıyor: kilitlenme. Her çatal semaforla korunsa bile çözüm yanlıştır.

Klasik Problemler

Filozoflar: Çözüm Yolları

Masaya en fazla dört

Başlangıcı 4 olan bir oda semaforu: en az bir filozof iki çatala ulaşır. Döngüsel bekleme kırılır.

Kaynak sıralaması (asimetri)

Her filozof önce numarası küçük çatalı alır; F4 önce çatal 0'ı ister. Genel kural: kilitleri hep aynı global sırayla al.

İki çatalı birlikte al

Tanenbaum: state[] dizisi ve filozof başına semafor; yalnız iki komşu da yemiyorsa yemeye geç (sonraki slayt).

Monitör

Silberschatz: pickup(i)/putdown(i) ve her filozof için koşul değişkeni self[i]; aynı test() mantığı.

Kilitlenmesiz olmak açlıksız olmak demek değildir: iki komşusu sırayla yiyen bir filozof sonsuza dek bekleyebilir. Açlığı önlemek için ek kural (sıra numarası, yaşlandırma) gerekir.

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]);
  }
}
hepsi TF0F1F2F3F4

Başlangıç: herkes THINKING (T). s[i] semaforları 0.

F1 yiyorF0F1F2F3F4

F1 acıkır; test(1): komşular F0, F2 yemiyor → EATING, up(&s[1]); ardından down(&s[1]) hemen geçer.

F2 blokeF0F1F2F3F4

F2 acıkır; sol komşu F1 yiyor → test başarısız, down(&s[2])'de uyur.

F1 ve F3 yiyorF0F1F2F3F4

F3 acıkır; F2 aç ama yemiyor, F4 düşünüyor → F3 yer. Aynı anda iki filozof yiyebilir.

F2 hâlâ bekliyorF0F1F2F3F4

F1 bırakır, komşularını test eder: test(2) başarısız çünkü sağdaki F3 hâlâ yiyor.

F2 uyandı, yiyorF0F1F2F3F4

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 down sı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örpaylaşılan veriişlemler (prosedürler)başlangıç koduiçeride (en fazla 1)Pgiriş kuyruğuQRxykoşulkuyrukları

Monitörler

Koşul Değişkenleri

monitörpaylaşılan veriişlemler (prosedürler)başlangıç koduiçeride (en fazla 1)Pgiriş kuyruğuQxykoşulkuyrukları

P monitörde; tampon boş olduğu için devam edemiyor. Q giriş kuyruğunda.

monitörpaylaşılan veriişlemler (prosedürler)başlangıç koduiçeride (en fazla 1)Qgiriş kuyruğuxPykoşulkuyrukları

P x.wait(): monitörü bırakır ve x kuyruğunda uyur. Böylece Q girebilir.

monitörpaylaşılan veriişlemler (prosedürler)başlangıç koduiçeride (en fazla 1)Qgiriş kuyruğuPxykoşulkuyrukları

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).

monitörpaylaşılan veriişlemler (prosedürler)başlangıç koduiçeride (en fazla 1)Qgiriş kuyruğuxykoşulkuyrukları

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.

İşlemAnlamı
x.wait()Çağıranı askıya al, monitörü serbest bırak.
x.signal()x'te bekleyen bir süreci (varsa) devam ettir.
broadcast / notifyAllx'te bekleyen herkesi uyandır.
Koşul değişkeninin değeri yoktur; yalnızca bir bekleme kuyruğudur. Beklenen koşul (ör. count > 0) her zaman paylaşılan veride ayrıca tutulur.

Monitörler

Hoare ve Mesa Anlamı

Hoare: signal-and-waitQ (sinyal veren)P (bekleyen)Q bekler (acil kuyruğu)Q monitördeP hemen çalışır: koşul doğruQ devam edersignal(x)

Signal-and-wait (Hoare): uyanan P hemen çalışır, Q bekler. P, koşulun doğru olduğundan emin olabilir: if yeterlidir.

Mesa: signal-and-continueQ (sinyal veren)P (bekleyen)Q monitörde devam ederP hazır, monitörü bekliyorP girer: koşul hâlâ doğru mu?signal(x)

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.

AnlamKim hemen çalışır?Kullananlar
Signal-and-wait (Hoare)UyandırılanDers 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 verenPthreads, 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);
T1ÜT2count == 0 → waitzaman →

Tampon boş: tüketici T1 koşul değişkeninde bekliyor.

T1ÜT2count == 0 → waitkoy; count = 1; signalT1 yalnızca hazır olurzaman →

Üretici bir öğe koyar ve signal yapar. T1 uyanır ama önce kilidi geri alması gerekir.

T1ÜT2count == 0 → waitkoy; count = 1; signalönce girdi; al; count = 0zaman →

Bu arada yeni gelen T2 kilidi önce kapar (barging) ve öğeyi alır: count = 0.

T1ÜT2count == 0 → waitkoy; count = 1; signalönce girdi; al; count = 0if: al() → boş tampon!zaman →

T1 sonunda çalışır. if ile koşulu yeniden sınamadan boş tampondan okur: hata.

T1ÜT2count == 0 → waitkoy; count = 1; signalönce girdi; al; count = 0while: count == 0 → yine waitzaman →

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ınca signal(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ıran signal.
  • Burada if yeterlidir, çünkü Tanenbaum Brinch Hansen anlamını (signal son komut) varsayar. Pthreads/Java'da aynı kod while ile yazılmalıdır.
  • Kayıp uyandırma olmaz: test ile wait aynı 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ımKoşul çağrısıTanım
pthread_mutex_initMutex oluştururpthread_cond_initKoşul değişkeni oluşturur
pthread_mutex_destroyMutex'i yok ederpthread_cond_destroyKoşul değişkenini yok eder
pthread_mutex_lockKilidi alır ya da bloke olurpthread_cond_waitMutex'i atomik olarak bırakıp uyur; dönerken yeniden alır
pthread_mutex_trylockAlır ya da hemen EBUSY dönerpthread_cond_signalBekleyenlerden en az birini uyandırır
pthread_mutex_unlockKilidi bırakır (yalnız sahibi)pthread_cond_broadcastBekleyen tümünü uyandırır
Kuralcond_wait her zaman mutex tutulurken ve bir while döngüsü içinde çağrılır.
Mutex türleriNORMAL, ERRORCHECK, RECURSIVE; öncelik kalıtımı için PTHREAD_PRIO_INHERIT.
Ek API'lerpthread_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;
}
ÜbaşlamadıTlock(m)mutex m: T

Tüketici önce çalışır ve mutex'i alır.

ÜhazırTcond_wait(condc) → uyurmutex m: boş

Tampon boş: cond_wait mutex'i atomik olarak bırakır ve uyur. Kayıp uyandırma penceresi yoktur.

Ülock, buffer = 1Tuyuyormutex m: Ü

Üretici mutex'i alır; tampon boş olduğu için döngüyü atlar ve 1 yazar.

Üsignal(condc), unlockTuyandı, mutex bekliyormutex m: boş

signal tüketiciyi uyandırır ama tüketici mutex'i geri almadan dönemez (Mesa).

Üi = 2: cond_wait(condp)Tmutex bekliyormutex m: Ü

Üretici yarışı kazanıp mutex'i yeniden almış olabilir: tampon dolu → bekler ve mutex'i bırakır.

ÜuyuyorTwhile geçti, yazdı, buffer = 0mutex m: T

Tüketici mutex'i alır, while koşulunu yeniden sınar (1 ≠ 0) ve öğeyi tüketir.

Üuyandı, buffer = 2Tsignal(condp), unlockmutex m: Ü

Tüketici condp'ye sinyal verir; üretici uyanır, tamponu boş bulur ve 2 yazar.

ÜbittiT2'yi yazdımutex m: T

İ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_waitP / down: 0 ise bloke
sem_trywait, sem_timedwaitBeklemeden ya da süre sınırıyla dene
sem_postV / up; sinyal işleyicisinden çağrılabilir
sem_getvalueAnlı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_init kullanımdan kalktı); adlandırılmış semafor ya da dispatch_semaphore kullanı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).
  • synchronized metot/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 notifyAll daha güvenlidir.
Çıktı (JDK 21, her çalıştırmada aynı)
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; signal doğru grubu uyandırır.
  • tryLock(), süre sınırlı ve kesilebilir kilitleme, isteğe bağlı adil kip.
  • unlock() mutlaka finally içinde.
  • Java 21 sanal iş parçacıkları synchronized içinde bloke olunca taşıyıcı iş parçacığını “sabitliyordu”; JDK 24 (JEP 491) bunu giderdi.
Çıktı (JDK 21, üç çalıştırma)
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

H (yüksek)M (orta)L (düşük)L: lock(R)zaman →

Düşük öncelikli L, paylaşılan kaynak R'nin kilidini alır.

H (yüksek)M (orta)L (düşük)L: lock(R)HH: lock(R) → blokeL devam ederzaman →

Yüksek öncelikli H gelir, L'yi keser; R'yi ister ve bloke olur. Kısa bir bekleme beklenir: L çıkınca H girer.

H (yüksek)M (orta)L (düşük)L: lock(R)HH: lock(R) → blokeM çalışır (R gerekmiyor)Lzaman →H'nin bekleme süresi M'ye bağlı: sınırsız

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.

H (yüksek)M (orta)L (düşük)L: lock(R)HbeklerL, H önceliğindeH: R alındı, çalışırM çalışırzaman →L, R'yi bırakınca eski önceliğine döner

Ö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 ve rt_mutex; Linux 6.12'de ana dala giren PREEMPT_RT bu mekanizmaya dayanır.
bilgi veriyolu(mutex ile korunur)veriyolu yönetimiyüksek · blokeiletişim göreviorta · uzun çalışırmeteoroloji verisidüşük · mutex'i tutarbekçi (watchdog)→ sistemi sıfırla

Canlılık

Canlılık Sorunları

kilitlenmecanlı kilitlenmeaçlıkT1T2ikisi de bloke, CPU boştabirbirini bekliyortrylock → geri çekil → tekrarçalışıyor ama ilerleme yokT1 ✓T2 ✓T1 ✓T3 …T3'e sıra hiç gelmiyor(okur-yazar, öncelik)
Kilitlenme (deadlock)Bir küme sürecin her biri, kümedeki başka birinin tuttuğu kaynağı bekler. Dört koşul ve çözümler: Bölüm 7.
Canlı kilitlenme (livelock)Süreçler bloke değildir; birbirine tepki verip durmadan aynı adımları tekrarlar. Çare: rastgele geri çekilme (ör. Ethernet).
Açlık (starvation)Bir süreç sürekli başkalarına yol verir. Çare: FIFO kuyruk, adil kilit, yaşlandırma. Sınırlı bekleme koşulu bunu dışlar.

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 edenKilidi tutan durursa herkes durur
KilitsizSistem bütün olarak her zaman ilerler
BeklemesizHer iş parçacığı sınırlı adımda biter
topAB

Yığın: top → A → B. Kilit yerine tek bir atomik işaretçi.

topABXX.next = Aeski = AT1

T1, X'i eklemeye hazırlanır: eski = A, X.next = A.

topYABXX.next = Aeski = AT1T2: CAS(top, A, Y)başarılı

Bu arada T2, Y'yi ekler: CAS(top, A, Y) başarılı.

topYABXX.next = Yeski = YT1T1: CAS(top, A, X)başarısız: top = Yeski ← Y, yeniden dene

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.

topXYABT1: CAS(top, Y, X) başarılıkilit yok; bir iş parçacığı askıda kalsa dadiğerleri ilerlemeye devam eder

İ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ıraYalnız okuma/yazma ile dışlama (SC varsayımıyla)Kavramsal; gerçek kodda kullanılmaz
Bellek bariyeriOkuma/yazma sırası ve görünürlükKilit ve atomik gerçeklemelerinin içinde
TAS, XCHG, CASAtomik okuma-değiştirme-yazmaKilitlerin ve kilitsiz yapıların temeli
Atomik değişkenTek değişkenin bölünmez güncellemesiSayaç, bayrak, referans sayımı
SpinlockDışlama, uyumadanÇok çekirdek, çok kısa KB, çekirdek içi
MutexDışlama, bekleyen uyurGenel amaçlı kritik bölgeler
SemaforSayma, dışlama ve sıralamaN özdeş kaynak, olay bildirimi
Monitör + koşul değişkeniYapısal dışlama + koşul beklemeKarmaşık koşullar (Java, pthreads)
Okur-yazar kilidiÇoklu okur, tek yazarOkuma 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ı

  1. Kritik bölge çözümünün üç koşulunu yazın. Katı sırayla değişim hangisini ihlal eder?
  2. Peterson algoritması x86 üzerinde neden yanlış çalışabilir? Nasıl düzeltilir?
  3. test_and_set ile kurulan basit spinlock hangi koşulu garanti etmez?
  4. CAS ile artırmada iki iş parçacığı aynı değeri okursa ne olur? Son değer neden doğrudur?
  5. Başlangıç değeri 2 olan bir semaforda üç süreç art arda wait yaparsa değer ve kuyruk ne olur (Silberschatz gerçeklemesi)?
  6. Sınırlı tampon çözümünde üreticideki iki down'ın yeri değişirse ne olur?
  7. Mesa anlamında koşul değişkeni beklemesi neden while içinde yapılmalıdır?
  8. Öncelik terslemesini tanımlayın; öncelik kalıtımı onu nasıl çözer?
Cevaplar
  1. Karşılıklı dışlama, ilerleme, sınırlı bekleme; katı sıra ilerlemeyi ihlal eder.
  2. Yazma tamponu yazmayı sonraki okumadan sonraya bırakır, ikisi de false okur; tam bariyer ya da seq_cst atomikler.
  3. Sınırlı bekleme: aynı iş parçacığı hep kaybedebilir.
  4. Biri başarılı olur, diğerinin CAS'ı başarısız olup yeniden okur; artış kaybolmaz.
  5. Değer −1; ilk ikisi geçer, üçüncü kuyrukta bekler.
  6. Tampon doluyken üretici mutex'i tutarak uyur, tüketici mutex'te uyur: kilitlenme.
  7. Uyanan iş parçacığı hemen çalışmaz; koşul bu arada bozulabilir, sahte uyanma da olabilir.
  8. 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.

Son

Bölüm 6: Senkronizasyon
Sonraki bölüm: Kilitlenme
1 / 1 Sercan KÜLCÜ, Tüm hakları saklıdır.