Bölüm 4: İş Parçacıkları

İşletim Sistemleri · Çalışma Soruları
Temel kavramlarEş zamanlılık ve paralellikAmdahl yasasıEşleme modelleriPthreads ve JavaÖrtük iş parçacığıİş parçacığı sorunlarıKoşul değişkenleri
Sercan KÜLCÜ

Çoktan Seçmeli · Temel Kavramlar

Soru 1: Hangisi Paylaşılmaz?

Aynı süreçteki iki iş parçacığı düşünün. Aşağıdakilerden hangisi bu iki iş parçacığı arasında paylaşılmaz, her birine özeldir?

C: Adres alanı (kod, veri, öbek) ve açık dosyalar, sinyal işleyicileri gibi kaynaklar sürece aittir; tüm iş parçacıklarınca paylaşılır. Her iş parçacığının ise kendi yürütme durumu vardır: program sayacı, yazmaçlar ve yığın. Yığın özeldir ama korunmaz: hatalı bir gösterici başka iş parçacığının yığınını bozabilir.

Hesaplama · Temel Kavramlar

Soru 2: Çok İş Parçacıklı Web Sunucusu

Bir web sunucusunda isteklerin 2/3’ü önbellekten karşılanıyor ve 15 ms CPU sürüyor. 1/3’ünde ayrıca diskten okumak gerekiyor: 15 ms CPU + 75 ms disk beklemesi. Tek CPU var; disk aynı anda birçok isteğe hizmet verebiliyor. Sunucu tek iş parçacıklıysa ve çok iş parçacıklıysa saniyede kaç isteğe yanıt verir?

Cevabı göster
  • Tek iş parçacığı: disk beklenirken süreç bloke olur, CPU boşta kalır. Ortalama istek süresi 2/3 × 15 + 1/3 × 90 = 10 + 30 = 40 ms → 25 istek/s.
  • Çok iş parçacığı: diski bekleyen işçi bloke olurken başka işçi CPU’yu kullanır; disk beklemesi örtüşür. Her istek yalnızca 15 ms CPU harcar → 1000 / 15 ≈ 66,7 istek/s.

Kazanç paralellikten değil (tek CPU), bloke eden çağrıların hesaplamayla örtüşmesinden gelir; kod yine sıralı ve okunaklı kalır. Aynı örtüşme olay güdümlü tek iş parçacığıyla da elde edilebilir ama kod karmaşıklaşır.

Adım Adım Çalıştırma · Çok Çekirdekli Programlama

Soru 3: Amdahl Yasası

Bir programın çalışma süresinin %20’si paralelleştirilemiyor. 2, 4 ve 8 çekirdekte hızlanma ve verim nedir? Çekirdek sayısı sonsuza giderse üst sınır nedir?

tek çekirdekseriparalel 0,8N = 1süre 1 → 1×

Tek çekirdekte süre %100: seri %20 + paralel %80.

tek çekirdekseriparalel 0,8N = 2süre 0,6 → 1,67×

0,2 + 0,8 / 2 = 0,6 → 1 / 0,6 ≈ 1,67×: çekirdek iki katına çıktı ama hızlanma 2 değil.

tek çekirdekseriparalel 0,8N = 4süre 0,4 → 2,5×

0,2 + 0,2 = 0,4 → 2,5×. Seri kısım artık sürenin yarısı.

tek çekirdekseriparalel 0,8N = 8süre 0,3 → 3,33×

0,2 + 0,1 = 0,3 → 3,33×; 8 çekirdeğin verimi %42. 4’ten 8’e geçmek yalnızca 1,33 kat kazandırdı.

tek çekirdekseriparalel 0,8N → ∞süre 0,2 → 5×

Paralel terim sıfıra gider, seri 0,2 kalır: en fazla 5×. Küçük görünen seri kısım ölçeklenmeyi sınırlar.

S = 0,2 (seri), 1 − S = 0,8 (paralel)
süre(N) = S + (1 − S) / N
hızlanma(N) = 1 / süre(N)
verim(N) = hızlanma(N) / N
üst sınır (N → ∞) = 1 / S

Çoktan Seçmeli · Çok Çekirdekli Programlama

Soru 4: Tek Çekirdekte Dört İş Parçacığı

Tek çekirdekli bir makinede dört iş parçacığı zaman paylaşımıyla sırayla çalışıyor. Bu durum için hangisi doğrudur?

B: Dördü de aynı zaman aralığında ilerler (eş zamanlılık), ama herhangi bir anda yalnızca biri çalışır. Paralellik en az iki çekirdek gerektirir. Paralellik eş zamanlılığı gerektirir; tersi doğru değildir, bu yüzden A olanaksızdır.

Hesaplama · Çok Çekirdekli Programlama

Soru 5: Hedef Hızlanma İçin Seri Kısım

16 çekirdekli bir sunucuda bir hesaplamanın en az 8 kat hızlanması isteniyor. Amdahl yasasına göre seri (paralelleştirilemeyen) kısım en fazla yüzde kaç olabilir? Seri kısım %10 olsaydı 16 çekirdekte hızlanma ne olurdu?

Cevabı göster

8 = 1 / (S + (1 − S) / 16) ⇒ S + (1 − S) / 16 = 1/8 ⇒ 16S + 1 − S = 2 ⇒ 15S = 1 ⇒ S = 1/15 ≈ %6,7.

Yani kodun en az %93,3’ü paralel olmalıdır. Kontrol: 1 / (0,0667 + 0,9333 / 16) = 1 / 0,125 = 8.

S = %10 iken: 1 / (0,1 + 0,9 / 16) = 1 / 0,15625 = 6,4× (verim %40). Hedefin yarısı bile olmayan bir seri kısım ölçeklenmeyi belirgin biçimde düşürür; gerçekte senkronizasyon ve iletişim yükü hızlanmayı daha da azaltır.

Çoktan Seçmeli · İş Parçacığı Modelleri

Soru 6: Çoktan Bire Modelde read()

Çoktan bire (N:1) modelde bir sürecin 8 kullanıcı düzeyi iş parçacığından biri bloke eden read() sistem çağrısı yaptı. Ne olur?

A: Çekirdek bu süreçte tek bir çizelgelenebilir varlık (çekirdek iş parçacığı) görür; o bloke olunca kütüphane başka iş parçacığına geçemez. Aynı nedenle N:1 modeli çok çekirdekten yararlanamaz. B, 1:1 modelinin davranışıdır.

Hesaplama · İş Parçacığı Modelleri

Soru 7: Geçiş Maliyeti ve Bloke Olma

Bir programda 1 000 000 iş parçacığı geçişi yapılıyor. Kullanıcı düzeyinde (N:1) bir geçiş 40 ns, çekirdek düzeyinde (1:1) 1,6 µs sürüyor. Geçişlere toplam ne kadar zaman gider? İş parçacıklarından biri 8 ms süren bir disk okuması yaptığında iki modelde ne olur?

Cevabı göster
  • N:1: 10⁶ × 40 ns = 0,04 s; 1:1: 10⁶ × 1,6 µs = 1,6 s → 40 kat. Kullanıcı düzeyi geçişte tuzak yok, çekirdeğe girilmez.
  • Disk okumasında N:1 modelde tüm süreç 8 ms bekler: çekirdek tek bir çekirdek iş parçacığı görür ve onu bloke eder.
  • 1:1 modelde yalnızca okuyan iş parçacığı bekler; diğerleri o 8 ms içinde (başka çekirdeklerde paralel olarak bile) çalışır.

M:N modeli (Go, Java sanal iş parçacıkları) ikisinin üstünlüklerini birleştirmeye çalışır: ucuz geçiş ve bloke olmayan çalışma zamanı.

Çoktan Seçmeli · İş Parçacığı Modelleri

Soru 8: Java Sanal İş Parçacıkları

Java 21’in sanal iş parçacıkları (Thread.ofVirtual()) hakkında hangisi doğrudur?

D: JVM çok sayıda sanal iş parçacığını az sayıda platform (taşıyıcı) iş parçacığına çoğullar. Bloke eden G/Ç’de sanal iş parçacığı taşıyıcısından indirilir (unmount), taşıyıcı başka sanal iş parçacığını çalıştırır. Çekirdek yalnızca taşıyıcıları görür; bu yüzden milyonlarca sanal iş parçacığı mümkündür.

Adım Adım Çalıştırma · İş Parçacığı Kütüphaneleri

Soru 9: Üç İş Parçacığıyla Toplam

Program ne yazdırır? Sonuç iş parçacıklarının çalışma sırasına bağlı mıdır? İkinci döngü (join) silinseydi ne olurdu?

mainçalışıyorT0yokT1yokT2yokkismi:[0]0[1]0[2]0

Süreç tek iş parçacığıyla (main) başlar; global kismi sıfırdır.

mainçalışıyorT0hazırT1hazırT2hazırkismi:[0]0[1]0[2]0

Üç iş parçacığı oluşturulur; her birine ayrı bir no[k] adresi verilir. Hangisinin önce çalışacağına çizelgeleyici karar verir.

mainbekliyorT0hazırT1çalışıyorT2hazırkismi:[0]0[1]0[2]0

main pthread_join(t[0])’da bloke olur. Bu çalıştırmada önce T1 seçildi.

mainbekliyorT0çalışıyorT1bittiT2hazırkismi:[0]0[1]26[2]0

T1: 5 + 6 + 7 + 8 = 26. Her iş parçacığı yalnız kendi hücresine yazar: yarış yok.

mainbekliyorT0bittiT1bittiT2çalışıyorkismi:[0]10[1]26[2]0

T0: 1 + 2 + 3 + 4 = 10.

mainbekliyorT0bittiT1bittiT2bittikismi:[0]10[1]26[2]42

T2: 9 + 10 + 11 + 12 = 42.

mainçalışıyorT0bittiT1bittiT2bittikismi:[0]10[1]26[2]42top = 78

Üç join de döner; top = 10 + 26 + 42 = 78. Çalışma sırası ne olursa olsun join’ler hepsinin bittiğini garanti eder.

mainçalışıyorT0bittiT1bittiT2bittikismi:[0]10[1]26[2]42top = 78

Çıktı her çalıştırmada aynıdır. join silinseydi main yarım toplamları okuyabilir, main’den dönüş ise tüm süreci iş parçacıklarıyla birlikte sonlandırırdı.

int kismi[3];
void *topla(void *arg) {
  int k = *(int *)arg;
  for (int i = 4*k + 1; i <= 4*k + 4; i++)
    kismi[k] += i;
  return NULL;
}
int main(void) {
  pthread_t t[3]; int no[3] = {0, 1, 2};
  for (int k = 0; k < 3; k++)
    pthread_create(&t[k], NULL, topla, &no[k]);
  int top = 0;
  for (int k = 0; k < 3; k++) {
    pthread_join(t[k], NULL);
    top += kismi[k];
  }
  printf("%d %d %d -> %d\n",
         kismi[0], kismi[1], kismi[2], top);
}

Çoktan Seçmeli · İş Parçacığı Kütüphaneleri

Soru 10: Döngü Değişkeninin Adresi

Programcı ekranda 0 1 2 görmeyi bekliyor. Hangisi doğrudur?

void *yaz(void *arg) {
  printf("%d ", *(int *)arg);
  return NULL;
}
int main(void) {
  pthread_t t[3];
  for (int i = 0; i < 3; i++)
    pthread_create(&t[i], NULL, yaz, &i);
  for (int j = 0; j < 3; j++)
    pthread_join(t[j], NULL);
  return 0;
}

C: Üç iş parçacığı da aynı i değişkeninin adresini alır; okudukları anda i değişmiş olabilir: 1 1 3, 2 3 3 gibi çıktılar görülebilir. Döngüden sonra i’nin ömrü de bittiği için okuma tanımsız davranıştır. Çözüm: her iş parçacığına ayrı bellek (int no[3], &no[i]).

Kod Çıktısı · İş Parçacığı Kütüphaneleri

Soru 11: run() mı, start() mı?

İş parçacığının adı "isci". Program ne yazdırır? t.run() neden yeni bir iş parçacığı başlatmaz?

public class Ad {
  public static void main(String[] a)
      throws Exception {
    Thread t = new Thread(() ->
      System.out.println("1: " +
        Thread.currentThread().getName()),
      "isci");
    t.run();
    t.start();
    t.join();
    System.out.println("2: " +
      Thread.currentThread().getName());
  }
}
Cevabı göster
1: main
1: isci
2: main
  • t.run() sıradan bir metot çağrısıdır: lambda çağıranın, yani main iş parçacığının içinde çalışır → “1: main”.
  • t.start() yeni bir işletim sistemi iş parçacığı başlatır; lambda orada çalışır → “1: isci”.
  • join onun bitmesini beklediği için sıra kesindir; son satırı main yazar.

Bir Thread nesnesi yalnızca bir kez start edilebilir; ikinci start IllegalThreadStateException fırlatır.

Adım Adım Çalıştırma · Örtük İş Parçacığı

Soru 12: Fork-Join ile Toplam

ESIK = 2 iken invoke hangi değeri döndürür? Görev ağacını çizin; her düğümün sonucunu yazın.

[0,8) ?

Kök görev 8 öğe: 8 − 0 > ESIK → bölünür.

[0,8) ?[0,4) ?[4,8) ?

sol.fork(): [0,4) havuzun kuyruğuna girer; boştaki bir işçi onu çalabilir. Aynı iş parçacığı [4,8)’i kendisi hesaplar.

[0,8) ?[0,4) ?[4,8) ?[4,6) ?[6,8) = 9

[4,8) de bölünür: [4,6) fork edilir, [6,8) doğrudan hesaplanır: 1 + 8 = 9.

[0,8) ?[0,4) ?[4,8) = 19[4,6) = 10[6,8) = 9

sol.join(): [4,6) = 2 + 8 = 10 → [4,8) = 10 + 9 = 19.

[0,8) ?[0,4) = 18[4,8) = 19[0,2) = 9[2,4) = 9[4,6) = 10[6,8) = 9

[0,4) aynı yolla: [0,2) = 2 + 7 = 9, [2,4) = 1 + 8 = 9 → 18. Bu dal büyük olasılıkla başka bir işçide (iş çalma) çalıştı.

[0,8) = 37[0,4) = 18[4,8) = 19[0,2) = 9[2,4) = 9[4,6) = 10[6,8) = 9

Kök: 18 + 19 = 37. 7 görev, 4 yaprak. Hangi işçinin hangi görevi çalıştırdığı değişebilir; sonuç değişmez.

class Toplam extends RecursiveTask<Integer> {
  static final int ESIK = 2; // + a, bas, son, kurucu
  protected Integer compute() {
    if (son - bas <= ESIK) {
      int s = 0;
      for (int i = bas; i < son; i++) s += a[i];
      return s;
    }
    int orta = (bas + son) / 2;
    Toplam sol = new Toplam(a, bas, orta);
    Toplam sag = new Toplam(a, orta, son);
    sol.fork();
    int sagSonuc = sag.compute();
    return sol.join() + sagSonuc;
  }
}
// a = {2, 7, 1, 8, 2, 8, 1, 8}
Toplam t = new Toplam(a, 0, 8);
ForkJoinPool.commonPool().invoke(t);

Çoktan Seçmeli · Örtük İş Parçacığı

Soru 13: OpenMP Paralel Bölge

Program -fopenmp ile derlenip çalıştırılıyor. Çıktı hakkında hangisi doğrudur?

#include <omp.h>
#include <stdio.h>
int main(void) {
  #pragma omp parallel num_threads(3)
  printf("merhaba\n");
  printf("bitti\n");
  return 0;
}

B: #pragma omp parallel yalnızca izleyen tek deyime uygulanır: 3 iş parçacıklı bir ekip oluşur, her biri “merhaba” yazar (sıra belirsiz). Bölgenin sonunda örtük bir bariyer vardır; ardından yalnızca ana iş parçacığı “bitti” yazar. num_threads(3) çekirdek sayısını geçersiz kılar.

Hesaplama · Örtük İş Parçacığı

Soru 14: Havuz Ne Kadar Büyük Olmalı?

8 çekirdekli bir makinede iki havuz kuruluyor: biri yalnızca hesap yapan görüntü sıkıştırma görevleri için, diğeri her isteğinde 5 ms hesap yapıp 45 ms veritabanı yanıtı bekleyen görevler için. Havuz boyutlarını nasıl seçersiniz?

Cevabı göster
  • CPU’ya bağlı: yaklaşık çekirdek sayısı kadar, 8 (ya da 9). Fazlası paralellik kazandırmaz; yalnızca bağlam değiştirme ve önbellek çekişmesi ekler.
  • G/Ç’ye bağlı: kaba kural Niş ≈ Nçekirdek × (1 + bekleme / hesap) = 8 × (1 + 45 / 5) = 80. Bir iş parçacığı beklerken diğer 9’u o çekirdeği kullanabilir.

Havuz eş zamanlı iş parçacığı sayısını sınırlar ve oluşturma maliyetini bir kez öder; boyut ölçümle ayarlanmalıdır: çok büyük havuz bellek tüketir, çok küçük havuz CPU’yu boş bırakır.

Çoktan Seçmeli · İş Parçacığı Sorunları

Soru 15: İş Parçacıklı Süreçte fork()

8 iş parçacıklı bir süreçteki bir iş parçacığı fork() çağırıyor (POSIX/Linux). Çocuk süreçle ilgili hangisi doğrudur?

D: Adres alanının tamamı kopyalanır, iş parçacıklarından yalnızca biri. Başka bir iş parçacığının tuttuğu bir mutex (ör. malloc içindeki) çocukta sonsuza dek kilitli kalabilir. Bu yüzden fork’tan hemen sonra exec önerilir; çoğu zaman posix_spawn daha iyi bir seçimdir.

Klasik Soru · İş Parçacığı Sorunları

Soru 16: Kaybolan Artırmalar

Paylaşılan sayac 0’dır. T1 ve T2 sayac++’ı ikişer kez çalıştırıyor; her artırma iki adımdır: oku (r = sayac) ve yaz (sayac = r + 1). Adımlar şu sırayla işlerse son değer nedir: T1.oku, T1.yaz, T2.oku, T1.oku, T2.yaz, T1.yaz, T2.oku, T2.yaz? Bütün sıralamalarda olası son değerler nelerdir?

Cevabı göster
adımT1.okuT1.yazT2.okuT1.okuT2.yazT1.yazT2.okuT2.yaz
rr1 = 0r2 = 1r1 = 1r2 = 2
sayac01112223

Son değer 3: T1 ve T2 aynı 1 değerini okuyup ikisi de 2 yazdığı için bir artırma kayboldu. 70 olası sıralamanın tamamında sonuç 2, 3 ya da 4’tür; en az 2’dir, çünkü her iş parçacığının son yazması kendi ilk yazmasından sonraki bir okumaya dayanır. 4 ancak artırma atomik yapılırsa (mutex, atomic_fetch_add) garanti edilir.

Çoktan Seçmeli · İş Parçacığı Sorunları

Soru 17: Her İş Parçacığına Ayrı Kopya

Her iş parçacığının kendi hata kodunu tutacağı ama tüm fonksiyonlardan global gibi erişilebilen bir değişken gerekiyor (C’deki errno gibi). Hangisi kullanılır?

A: TLS değişkeni her iş parçacığında ayrı bir kopya olarak bulunur ve iş parçacığının ömrü boyunca yaşar. Global ve öbek değişkeni paylaşılır; yerel değişken fonksiyon dönünce kaybolur ve diğer fonksiyonlar ona erişemez. Bugünkü C kütüphanelerinde errno zaten iş parçacığına yereldir.

Çoktan Seçmeli · Senkronizasyona Giriş

Soru 18: Neden while, Neden if Değil?

Birden çok tüketicinin olduğu tek hücreli tamponda tüketici while (buffer == 0) pthread_cond_wait(&condc, &m); yerine if ile bekleseydi ne olabilirdi?

C: Sinyalle uyanan tüketici kilidi yeniden alana kadar başka bir tüketici ürünü almış olabilir; ayrıca sahte (spurious) uyanmalar mümkündür. while koşulu yeniden sınar. A yanlıştır: pthread_cond_wait beklerken kilidi atomik olarak bırakır, uyanınca yeniden alır.

Son

Bölüm 4: İş Parçacıkları: Çalışma Soruları
Konu anlatımı: İş parçacıkları sunumu · Terim sözlüğü
1 / 1 Sercan KÜLCÜ, Tüm hakları saklıdır.