Bölüm 4: İş Parçacıkları
Ç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 çekirdekte süre %100: seri %20 + paralel %80.
0,2 + 0,8 / 2 = 0,6 → 1 / 0,6 ≈ 1,67×: çekirdek iki katına çıktı ama hızlanma 2 değil.
0,2 + 0,2 = 0,4 → 2,5×. Seri kısım artık sürenin yarısı.
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ı.
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?
Süreç tek iş parçacığıyla (main) başlar; global kismi sıfırdır.
Üç iş parçacığı oluşturulur; her birine ayrı bir no[k] adresi verilir. Hangisinin önce çalışacağına çizelgeleyici karar verir.
main pthread_join(t[0])’da bloke olur. Bu çalıştırmada önce T1 seçildi.
T1: 5 + 6 + 7 + 8 = 26. Her iş parçacığı yalnız kendi hücresine yazar: yarış yok.
T0: 1 + 2 + 3 + 4 = 10.
T2: 9 + 10 + 11 + 12 = 42.
Üç join de döner; top = 10 + 26 + 42 = 78. Çalışma sırası ne olursa olsun join’ler hepsinin bittiğini garanti eder.
Çı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”.joinonun 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.
Kök görev 8 öğe: 8 − 0 > ESIK → bölünür.
sol.fork(): [0,4) havuzun kuyruğuna girer; boştaki bir işçi onu çalabilir. Aynı iş parçacığı [4,8)’i kendisi hesaplar.
[4,8) de bölünür: [4,6) fork edilir, [6,8) doğrudan hesaplanır: 1 + 8 = 9.
sol.join(): [4,6) = 2 + 8 = 10 → [4,8) = 10 + 9 = 19.
[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ı.
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ım | T1.oku | T1.yaz | T2.oku | T1.oku | T2.yaz | T1.yaz | T2.oku | T2.yaz |
|---|---|---|---|---|---|---|---|---|
| r | r1 = 0 | r2 = 1 | r1 = 1 | r2 = 2 | ||||
| sayac | 0 | 1 | 1 | 1 | 2 | 2 | 2 | 3 |
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.