Bölüm 4: İş Parçacıkları
Giriş
Öğrenme Çıktıları
İş parçacığını süreçten ayırmak; hangi durumun paylaşıldığını, hangisinin iş parçacığına özel olduğunu söylemek.
Eşzamanlılık ile paralelliği ayırt etmek ve Amdahl yasasıyla hızlanma sınırını hesaplamak.
Kullanıcı ve çekirdek düzeyi iş parçacıklarını, N:1, 1:1 ve M:N eşleme modellerini karşılaştırmak.
Pthreads ve Java ile iş parçacığı oluşturmak, beklemek; havuz, fork-join, OpenMP ve GCD gibi örtük yaklaşımları tanımak.
Yarış koşulu, yeniden girilemeyen kod, TLS, iptal, sinyaller ve fork() davranışını açıklamak.
Linux clone(), Java 21 sanal iş parçacıkları ve Go gibi güncel gerçeklemeleri yorumlamak.
Temel Kavramlar
İş Parçacığı Nedir?
- Süreç (process): yürütülmekte olan bir program; adres alanı, açık dosyalar gibi kaynakların sahibidir.
- İş parçacığı (thread): süreç içindeki bir yürütme akışı; CPU kullanımının ve çizelgelemenin temel birimidir.
- Bir süreçte bir ya da daha çok iş parçacığı bulunur; hepsi aynı adres alanını paylaşır.
- Her iş parçacığının kendine ait PC, yazmaç kümesi, yığını ve durumu vardır; bunlar TCB'de (thread control block) tutulur.
- Tanenbaum'un ayrımı: süreç kaynakları gruplar, iş parçacığı yürütülür.
Temel Kavramlar
Neden Çok İş Parçacığı?
- Modern uygulamaların çoğu çok iş parçacıklıdır: tarayıcı, IDE, oyun motoru, veritabanı, web sunucusu.
- Farklı görevler ayrı iş parçacıklarında yürür:
- ekranı güncelleme, veri getirme, ağ isteğine yanıt verme, yazım denetimi…
- Bir görev beklerken (disk, ağ) diğerleri ilerlemeye devam eder.
- Süreç oluşturmak pahalıdır; iş parçacığı oluşturmak çok daha ucuzdur.
- Sıralı kod gibi yazılan akışlar programı basitleştirir.
- İşletim sistemi çekirdeği de çok iş parçacıklıdır (Linux:
kthreadd,kworker,ksoftirqd).
Temel Kavramlar
Tek ve Çok İş Parçacıklı Süreç
Tek iş parçacıklı süreç: bir yürütme akışı, bir PC, bir yazmaç kümesi ve bir yığın.
Çok iş parçacıklı süreç: kod, veri ve dosyalar bir kez bulunur; her akışın kendi yazmaçları ve yığını vardır.
Turuncu satır paylaşılan kaynaklar: bir iş parçacığının global değişkene yazdığını diğerleri hemen görür.
Temel Kavramlar
Paylaşılan ve Özel Öğeler
| Süreç başına (tüm iş parçacıkları paylaşır) |
|---|
| Adres alanı: kod, global değişkenler, heap |
| Açık dosyalar, soketler |
| Çocuk süreçler |
| Bekleyen alarmlar (zamanlayıcılar) |
| Sinyal işleyiciler (signal handlers) |
| Kimlik ve hesap (accounting) bilgisi |
| İş parçacığı başına (özel) |
|---|
| Program sayacı (PC) |
| Yazmaçlar (register) |
| Yığın (stack) ve yığın işaretçisi |
| Durum: çalışıyor, hazır, bekliyor, sonlandı |
| İş parçacığı kimliği, sinyal maskesi |
İş parçacığına yerel depolama (TLS), errno |
Temel Kavramlar
Her İş Parçacığının Kendi Yığını
- Fonksiyon çağrıları ve yerel değişkenler her akış için ayrı tutulmalıdır: bu yüzden her iş parçacığının kendi yığını vardır.
- Linux/glibc'de yeni bir iş parçacığının yığını
mmapile ayrılır; varsayılan boyut genellikleulimit -sdeğeri (çoğunlukla 8 MiB) kadardır. - Yığının ucundaki koruma sayfası (guard page) erişilemez; taşan yığın
SIGSEGVüretir, komşu yığını sessizce bozmaz. - Boyut ayarı:
pthread_attr_setstacksize, Java'da-Xss. - Java sanal iş parçacıklarının yığın çerçeveleri heap'te tutulur; bu sayede milyonlarca sanal iş parçacığı mümkündür.
Temel Kavramlar
Örnek: Üç İş Parçacıklı Kelime İşlemci
Üç ayrı süreç bu işi yapamaz: üçü de aynı belge üzerinde çalışmalıdır. Ortak adres alanı burada temel avantajdır.
Temel Kavramlar
Çok İş Parçacıklı Web Sunucusu
Bir istek gelir: dağıtıcı (dispatcher) iş parçacığı isteği ağdan okur.
Dağıtıcı boştaki bir işçi (worker) iş parçacığını seçer, isteği ona devreder ve uyandırır.
İşçi 1 sayfayı önbellekte bulur ve yanıtı gönderir; disk erişimi gerekmez.
Yeni istek işçi 2'ye gider; sayfa önbellekte yoksa işçi 2 diskte bloke olur. Bu sırada dağıtıcı ve işçi 3 başka istekleri işler.
Temel Kavramlar
Dağıtıcı ve İşçi Kodu
while (TRUE) { get_next_request(&buf); /* ağdan oku */ handoff_work(&buf); /* işçiye ver */ }
while (TRUE) { wait_for_work(&buf); look_for_page_in_cache(&buf, &page); if (page_not_in_cache(&page)) read_page_from_disk(&buf, &page); return_page(&page); }
- Her iki kod da sıralı ve okunaklıdır: bloke eden
readçağrısı yalnızca o işçiyi durdurur. - Tek iş parçacıklı sunucuda önbellekte olmayan sayfa için tüm süreç bloke olur; disk beklenirken CPU boşta kalır.
- Çok iş parçacıklı sunucuda iş bir işçiye aktarılır, sunucu yeni istekleri kabul etmeye devam eder.
Temel Kavramlar
Sunucu Tasarımının Üç Yolu
| Model | Paralellik / örtüşme | Sistem çağrıları | Programlama |
|---|---|---|---|
| Çok iş parçacıklı süreç | var | bloke eden | sıralı kod, kolay |
| Tek iş parçacıklı süreç | yok | bloke eden | kolay ama yavaş |
| Sonlu durum makinesi (olay güdümlü) | var (G/Ç örtüşür) | bloke etmeyen + bildirim / kesme | zor: her isteğin durumu elle saklanır |
epoll ve io_uring, BSD/macOS'ta kqueue kullanılır. Çoğu sunucu iki yaklaşımı birleştirir: çekirdek başına bir olay döngüsü + bloke eden işler için iş parçacığı havuzu.Kaynak: Tanenbaum & Bos, Modern Operating Systems, Şekil 2-10'daki karşılaştırma.
Temel Kavramlar
Çok İş Parçacığının Faydaları
Bir kısım bloke olsa ya da uzun bir işlem yapsa bile program yanıt vermeye devam eder; özellikle kullanıcı arayüzleri için önemlidir.
İş parçacıkları sürecin belleğini ve dosyalarını kendiliğinden paylaşır; paylaşılan bellek ya da mesaj iletimi kurmaktan kolaydır.
Oluşturmak ve aralarında geçiş yapmak süreçlere göre ucuzdur: adres alanı kopyalanmaz, sayfa tabloları değişmez.
Tek bir süreç birden çok çekirdekte aynı anda çalışabilir; tek iş parçacıklı süreç yalnızca bir çekirdek kullanır.
clone() ile oluşturulduğundan fark daha küçüktür; asıl kazanç bellek eşlemesinin kopyalanmaması ve TLB'nin korunmasıdır.Temel Kavramlar
Bağlam Değiştirme Maliyeti
- Kullanıcı düzeyi iş parçacıklarında geçiş çekirdeğe girmeden (tuzak yok) bir kütüphane fonksiyonuyla yapılır: daha da ucuzdur.
- Doğrudan maliyet mikrosaniyeler mertebesindedir; dolaylı maliyet (soğuk önbellek, TLB kaçırmaları) çoğu zaman daha büyüktür.
Çok Çekirdekli Programlama
Eşzamanlılık ve Paralellik
Tek çekirdekte zaman paylaşımı: eşzamanlı ama paralel değil.
İki çekirdekte T1/T3 ve T2/T4 aynı anda çalışır: paralel yürütme.
Paralellik eşzamanlılığı gerektirir; tersi doğru değildir. Aynı program iki donanımda farklı davranır.
Çok Çekirdekli Programlama
Çok Çekirdekli Programlamanın Zorlukları
-fsanitize=thread), Helgrind, Java Flight Recorder; ama hiçbir araç tüm yarışları yakalamaz. Tasarım aşamasında paylaşımı azaltmak en etkili yöntemdir.Çok Çekirdekli Programlama
Veri ve Görev Paralelliği
- Veri paralelliği:
Nöğeli diziyi toplamak için her çekirdekN/4öğe toplar; sonunda kısmi toplamlar birleştirilir (GPU'lar bu modelin uç örneğidir). - Görev paralelliği: iş, bağımsız görevlere bölünür; görevler aynı ya da farklı veriyi kullanabilir.
- Gerçek uygulamalar çoğunlukla ikisini birlikte kullanır.
Çok Çekirdekli Programlama
Amdahl Yasası
hızlanma ≤ 1 / ( S + (1 − S) / N )
S: seri (paralelleştirilemeyen) oran · N: çekirdek sayısı · N → ∞ iken sınır 1 / S
Örnek: S = 0,25. Tek çekirdekte süre %100, hızlanma 1×.
N = 2: 0,25 + 0,75/2 = 0,625 → hızlanma 1 / 0,625 = 1,60× (2× değil).
N = 4: 0,25 + 0,75/4 = 0,4375 → hızlanma 2,29×.
N → ∞: paralel kısım sıfıra yaklaşır, seri %25 kalır → en fazla 4×.
Çok Çekirdekli Programlama
Amdahl Eğrileri
- Programın %95'i paralelse teorik hızlanma sınırı 1 / 0,05 = 20×; 1024 çekirdekte bile yaklaşık 19,6×.
- Gerçekte senkronizasyon ve iletişim yükü nedeniyle hızlanma daha da düşüktür. Gustafson yasası: problem boyutu çekirdekle büyürse ölçeklenmiş hızlanma S + (1 − S)·N olur.
İş Parçacığı Modelleri
Kullanıcı ve Çekirdek Düzeyi İş Parçacıkları
- Kullanıcı düzeyi: iş parçacıkları kütüphanede (çalışma zamanı sistemi) yönetilir; çekirdek her süreci tek akışlı sanır.
- Çekirdek düzeyi: çekirdek her iş parçacığını tanır ve kendisi çizelgeler. Linux, Windows, macOS böyledir.
İş Parçacığı Modelleri
Kullanıcı Düzeyi İş Parçacıkları
Avantajlar
- İş parçacığı desteği olmayan bir çekirdekte bile kütüphane olarak gerçeklenebilir.
- Her süreç kendi iş parçacığı tablosunu tutar: PC, yazmaçlar, SP, durum.
- Geçiş çok hızlıdır: tuzak (trap) yok, çekirdeğe giriş yok, bellek eşlemesi değişmez, TLB boşaltılmaz.
- Her süreç kendine özgü bir çizelgeleme algoritması kullanabilir.
Dezavantajlar
- Bloke eden bir sistem çağrısı (ör.
read) tüm süreci, dolayısıyla tüm iş parçacıklarını durdurur. - Çözümler zarif değil: çağrıyı
selectile önceden sınayan sarmalayıcılar (jacket), bloke etmeyen çağrılar. - Sayfa hatası da tüm süreci bloke eder.
- Saat kesmesi kütüphaneye gelmez: iş parçacığı gönüllü bırakmazsa (
yield) diğerleri hiç çalışmaz. - Tek süreç birden çok çekirdeği kullanamaz.
İş Parçacığı Modelleri
Bloke Eden Çağrı: N:1 ve 1:1
Çoktan bire (N:1): üç kullanıcı iş parçacığı tek bir çekirdek iş parçacığı (K1) üzerinde; çizelgelemeyi kütüphane yapar.
U1 bloke eden read() çağırır: çekirdek yalnızca K1'i görür ve onu bloke eder → tüm süreç durur.
Bire bir (1:1): U1'in bloke olması yalnızca K1'i etkiler; U2 ve U3 iki çekirdekte paralel çalışır.
İş Parçacığı Modelleri
Çekirdek Düzeyi İş Parçacıkları
- Çekirdek, kullanıcı düzeyindekiyle aynı bilgileri (yazmaçlar, durum…) içeren bir iş parçacığı tablosu tutar.
- Oluşturma ve sonlandırma sistem çağrısıdır: kullanıcı düzeyine göre daha pahalı.
- Bir iş parçacığı bloke olursa çekirdek başka birini seçer; seçilenin aynı süreçten olması gerekmez.
- Sayfa hatası yalnızca hatayı yapan iş parçacığını bekletir.
- Bir sürecin iş parçacıkları farklı çekirdeklerde gerçekten paralel çalışır.
- Her iş parçacığı çekirdek belleğinde yer kaplar (TCB, çekirdek yığını): sayısı sınırsız değildir.
Linux (NPTL), Windows ve macOS 1:1 model kullanır. Oluşturma maliyeti azaldığından on binlerce çekirdek iş parçacığı sorun değildir.
Linux'ta sınırlar: /proc/sys/kernel/threads-max, ulimit -u, cgroup pids.max.
Gözlemlemek için: ps -eLf, top -H, /proc/<pid>/task/.
İş Parçacığı Modelleri
Eşleme Modelleri
Çoktan bire (N:1): hızlı geçiş, ama bir bloke tüm süreci durdurur ve paralellik yoktur. Örnek: Solaris green threads, GNU Portable Threads (tarihî).
Bire bir (1:1): gerçek paralellik ve bağımsız bloke olma; her iş parçacığı çekirdek kaynağı harcar. Linux, Windows, macOS.
Çoktan çoğa (M:N): çok sayıda kullanıcı iş parçacığı, daha az sayıda çekirdek iş parçacığına çoğullanır; gerçeklemesi zordur.
İki düzeyli / hibrit: M:N'e ek olarak bir kullanıcı iş parçacığı tek bir çekirdek iş parçacığına bağlanabilir (bound).
İş Parçacığı Modelleri
Modellerin Karşılaştırılması
| Ölçüt | N:1 | 1:1 | M:N |
|---|---|---|---|
| Geçiş maliyeti | çok düşük | orta (çekirdek) | düşük |
| Bloke eden çağrı | tüm süreç durur | yalnız o iş parçacığı | yalnız o iş parçacığı* |
| Çok çekirdek | kullanılamaz | kullanılır | kullanılır |
| İş parçacığı sayısı | çok fazla | çekirdek belleğiyle sınırlı | çok fazla |
| Gerçekleme | kolay (kütüphane) | kolay | zor (iki çizelgeleyici) |
* Çalışma zamanı, bloke olan çağrıyı fark edip başka çekirdek iş parçacığına geçebiliyorsa.
İş Parçacığı Modelleri
Güncel M:N: Sanal İş Parçacıkları
Thread.ofVirtual().start(r); bloke eden G/Ç çağrısında JVM sanal iş parçacığını taşıyıcıdan indirir (unmount).GOMAXPROCS); boştaki P iş çalar.İş Parçacığı Modelleri
Açılır (Pop-up) İş Parçacıkları
(a) Mesaj gelmeden önce: süreçte bu mesajı bekleyen bir iş parçacığı yoktur.
(b) Mesaj gelince sistem onu işleyecek yeni bir iş parçacığı oluşturur.
Alternatif: receive çağrısında bloke olmuş bir iş parçacığı; onun durumunun (yazmaçlar, yığın) her mesajda geri yüklenmesi gerekir.
Açılır iş parçacığı yenidir, geri yüklenecek geçmişi yoktur: gecikme düşüktür. Çekirdek içinde de kullanılabilir (kesme işleme).
/* mesaj gelince */ pthread_t t; pthread_create(&t, NULL, isle, msg); pthread_detach(t); /* join yok */
İş Parçacığı Kütüphaneleri
İş Parçacığı Kütüphaneleri
CreateThread, WaitForSingleObject; çekirdek düzeyi, 1:1.Thread, Runnable, ExecutorService; JVM, OS iş parçacıklarını (ve Java 21'den beri sanal iş parçacıklarını) kullanır.<threads.h>; C++11 std::thread, C++20 std::jthread (eski Boost.Thread'in yerini aldı).threading; GIL yüzünden CPU işinde paralellik yok. 3.13'te deneysel, 3.14'te desteklenen GIL'siz derleme.go f() ile goroutine; M:N çalışma zamanı, kanallarla iletişim.join) ve sonuçlarını birleştirir: fork-join.İş Parçacığı Kütüphaneleri
POSIX Pthreads
- IEEE 1003.1c (POSIX.1c) standardı: iş parçacığı oluşturma ve senkronizasyon API'si.
- Bir tanımlamadır, gerçekleme değil: kütüphanenin nasıl davranacağını söyler; kullanıcı ya da çekirdek düzeyinde gerçeklenebilir.
- Linux'ta glibc NPTL (1:1), macOS ve BSD'lerde de çekirdek iş parçacıkları üzerinde çalışır.
- Derleme:
gcc prog.c -pthread.
| Çağrı | Açıklama |
|---|---|
pthread_create | Yeni iş parçacığı oluşturur, verilen fonksiyonu çalıştırır. |
pthread_exit | Çağıran iş parçacığını sonlandırır. |
pthread_join | Belirli bir iş parçacığının bitmesini bekler. |
pthread_detach | Kaynakları bitince kendiliğinden geri verilir; join edilmez. |
pthread_self | Kendi kimliğini döndürür. |
sched_yield | CPU'yu bırakır. (pthread_yield standart dışıdır, glibc 2.34'te kullanımdan kaldırıldı.) |
pthread_attr_init / _destroy | Öznitelik yapısını (yığın boyutu, ayrık durum…) ilklendirir / kaldırır. |
İş Parçacığı Kütüphaneleri · Kodu adım adım çalıştır
Pthreads: Oluştur ve Bekle
int toplam; /* paylaşılan */ void *runner(void *param) { int ust = atoi(param); toplam = 0; for (int i = 1; i <= ust; i++) toplam += i; pthread_exit(0); } int main(void) { pthread_t tid; pthread_create(&tid, NULL, runner, "5"); pthread_join(tid, NULL); printf("toplam = %d\n", toplam); return 0; }
Süreç tek iş parçacığıyla (main) başlar; global toplam 0'dır.
pthread_create yeni iş parçacığını oluşturur; runner("5") çalışacaktır.
main join'de bloke olur. runner'ın ne zaman başlayacağını çizelgeleyici belirler.
runner kendi yığınında yerel ust değişkenini oluşturur.
Döngü paylaşılan toplam'ı günceller: 15.
runner sonlanır; join döner ve main uyanır.
join sayesinde çıktı her çalıştırmada aynıdır: toplam = 15.
Silberschatz Şekil 4.11'den uyarlanmıştır; #include <pthread.h>, <stdio.h>, <stdlib.h> satırları gösterilmedi.
İş Parçacığı Kütüphaneleri
Birden Çok İş Parçacığı
#define N 4 long kismi[N]; /* her biri kendi hücresine */ void *topla(void *arg) { long k = (long)arg; /* 0 .. N-1 */ for (long i = k*250 + 1; i <= (k+1)*250; i++) kismi[k] += i; return NULL; } int main(void) { pthread_t t[N]; for (long k = 0; k < N; k++) pthread_create(&t[k], NULL, topla, (void *)k); long toplam = 0; for (int k = 0; k < N; k++) { pthread_join(t[k], NULL); toplam += kismi[k]; } printf("%ld\n", toplam); /* 500500 */ }
- 1..1000 toplamı dört iş parçacığına bölünür: her biri 250 sayı toplar (veri paralelliği).
- Her iş parçacığı yalnızca kendi
kismi[k]hücresine yazar: yarış yoktur. - main hepsini
joinile bekler ve kısmi toplamları birleştirir: sonuç her zaman 500500. - Argüman olarak
&kgeçmek hatalı olurdu: döngü değişkeni değişmeye devam eder. - Komşu hücreler aynı önbellek satırındadır: yoğun yazmada false sharing olur; yerel değişkende toplayıp sonda yazmak daha iyidir.
İş Parçacığı Kütüphaneleri · Kodu adım adım çalıştır
Java: Thread, start ve join
public class Topla { static int toplam = 0; public static void main(String[] a) throws InterruptedException { Thread t = new Thread(() -> { for (int i = 1; i <= 5; i++) toplam += i; }); t.start(); System.out.println("main: bekliyor"); t.join(); System.out.println("toplam = " + toplam); } }
new Thread(lambda) yalnızca nesneyi oluşturur; durum NEW.
start() yeni OS iş parçacığını başlatır (run() çağırmak başlatmazdı!).
main yazmaya devam eder; t ile aynı anda çalışabilir.
join(): main, t bitene kadar WAITING durumunda.
t döngüyü bitirir: toplam = 15, durum TERMINATED.
join, t'nin yazdıklarının main tarafından görülmesini de garanti eder (happens-before).
JDK 21 ile derlenip çalıştırıldı; çıktı her seferinde aynıdır.
İş Parçacığı Kütüphaneleri
Java 21: Sanal İş Parçacıkları
import java.time.Duration; import java.util.concurrent.*; import java.util.stream.IntStream; public class Sanal { public static void main(String[] args) { try (var ex = Executors.newVirtualThreadPerTaskExecutor()) { IntStream.range(0, 10_000).forEach(i -> ex.submit(() -> { Thread.sleep(Duration.ofSeconds(1)); return i; })); } // close(): tüm görevleri bekler System.out.println("bitti"); } }
- 10 000 görevin her biri kendi sanal iş parçacığında 1 sn uyur.
- Ölçümümüzde (JDK 21) tümü yaklaşık 2 saniyede bitti: birkaç taşıyıcı iş parçacığı yetti.
- 10 000 platform iş parçacığı ise 10 000 çekirdek iş parçacığı ve yığını demektir.
- Uygun alan: çok sayıda G/Ç bekleyen görev (sunucular). CPU yoğun işte kazanç yoktur.
- Klasik oluşturma da çalışır:
Thread.ofVirtual().start(r),Thread.ofPlatform().
Örtük İş Parçacığı
Örtük İş Parçacığı (Implicit Threading)
- İş parçacığı sayısı yüzlere çıkınca doğruluğu elle yönetmek zorlaşır.
- Çözüm: programcı görevleri (paralel çalışabilecek işleri) belirtir; iş parçacıklarını derleyici ve çalışma zamanı kütüphaneleri oluşturur ve yönetir.
| Yaklaşım | Nerede | Fikir |
|---|---|---|
| İş parçacığı havuzu | Java ExecutorService, Windows thread pool | Hazır iş parçacıkları kuyruktaki görevleri alır. |
| Fork-join | Java ForkJoinPool, paralel akışlar | Görev böl, paralel çöz, sonuçları birleştir. |
| OpenMP | C, C++, Fortran | Derleyici yönergeleri ile paralel bölgeler. |
| Grand Central Dispatch | macOS, iOS (libdispatch) | Bloklar dağıtım kuyruklarına konur. |
| oneTBB (eski adı Intel TBB) | C++ şablon kütüphanesi | parallel_for, görev çizelgeleme, iş çalma. |
Örtük İş Parçacığı
İş Parçacığı Havuzu
Havuz başlangıçta sabit sayıda iş parçacığı oluşturur; uygulama görevleri kuyruğa koyar.
Boştaki işçiler kuyruktan görev alır: yeni iş parçacığı oluşturma maliyeti yok.
İşçi 1, G1'i bitirince yok olmaz, hemen sıradaki G4'ü alır.
Kuyruk boşalınca işçiler bir sonraki göreve kadar bekler. Eşzamanlı iş parçacığı sayısı havuz boyutuyla sınırlıdır.
Faydalar: daha hızlı hizmet, sistemi aşırı iş parçacığından koruma, görevin nasıl çalıştırılacağını (gecikmeli, periyodik) görevden ayırma.
Örtük İş Parçacığı
Java ExecutorService
ExecutorService havuz = Executors.newFixedThreadPool(2); List<Future<Integer>> sonuclar = new ArrayList<>(); for (int g = 1; g <= 4; g++) { final int n = g; sonuclar.add(havuz.submit(() -> n * n)); } int toplam = 0; for (Future<Integer> f : sonuclar) toplam += f.get(); // sonucu bekler System.out.println("toplam = " + toplam); havuz.shutdown();
toplam = 30
- Dört görev (1², 2², 3², 4²) iki iş parçacıklı havuza verilir.
submitbirFuturedöndürür;get()sonuç hazır olana dek bekler.- Havuz boyutu: CPU yoğun işte ≈ çekirdek sayısı (
availableProcessors()); G/Ç yoğun işte daha fazla. - Diğer fabrikalar:
newCachedThreadPool,newScheduledThreadPool,newVirtualThreadPerTaskExecutor.
Örtük İş Parçacığı
Fork-Join
Büyük görev: 8 sayının toplamı. Eşikten büyükse böl.
fork: iki alt görev oluşturulur; havuzdaki farklı iş parçacıklarında çalışabilirler.
Alt görevler yine bölünür; eşiğe (2 öğe) inen yapraklar doğrudan hesaplanır: 4, 5, 14, 8.
join: ebeveyn çocuklarını bekler ve birleştirir: 4+5 = 9, 14+8 = 22.
Kök sonuç: 9 + 22 = 31. Senkron iş parçacığı kullanımının tipik örneği.
Örtük İş Parçacığı
Java ForkJoinPool ve İş Çalma
class Toplam extends RecursiveTask<Long> { protected Long compute() { if (son - bas <= ESIK) { // küçükse doğrudan long 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(); // asenkron başlat long sagSonuc = sag.compute(); return sol.join() + sagSonuc; } } long s = ForkJoinPool.commonPool() .invoke(new Toplam(a, 0, a.length));
1..100 000 dizisi için çıktı (JDK 21): toplam = 5000050000. Boşta kalan işçi başkasının kuyruğundan görev çalar (work stealing); yük kendiliğinden dengelenir.
Örtük İş Parçacığı
OpenMP
#include <omp.h> #include <stdio.h> int main(void) { #pragma omp parallel printf("merhaba, ben %d\n", omp_get_thread_num()); double a[1000], b[1000], c[1000], s = 0; /* ... a ve b doldurulur ... */ #pragma omp parallel for reduction(+:s) for (int i = 0; i < 1000; i++) { c[i] = a[i] + b[i]; s += c[i]; } return 0; }
- C, C++ ve Fortran için derleyici yönergeleri (compiler directives) ve kütüphane; paylaşılan bellekli sistemler için.
#pragma omp parallel: paralel bölge; varsayılan olarak çekirdek sayısı kadar iş parçacığı (OMP_NUM_THREADSya danum_threads(n)ile ayarlanır).parallel for: döngü yinelemelerini iş parçacıklarına böler;reductionkısmi toplamları güvenle birleştirir.- Derleme:
gcc -fopenmp. “merhaba” satırlarının sırası her çalıştırmada değişebilir.
Örtük İş Parçacığı
Grand Central Dispatch ve oneTBB
- Apple'ın macOS/iOS için çalışma zamanı kütüphanesi ve C/Objective-C dil eklentisi (bloklar):
^{ printf("Ben bir bloğum"); } - Bloklar dağıtım kuyruklarına konur; seri kuyruk sırayla, eşzamanlı kuyruk paralel çalıştırır. QoS sınıfları önceliği belirler.
- Kuyruktan çıkan blok havuzdaki boş bir iş parçacığına atanır; iş parçacığı yönetimi tamamen GCD'dedir.
dispatch_queue_t q = dispatch_get_global_queue(
QOS_CLASS_USER_INITIATED, 0);
dispatch_async(q, ^{ puts("Ben bir bloğum"); });tbb::parallel_for(0, n, [&](int i) { f(v[i]); }); Görevleri kendi çizelgeleyicisiyle iş çalma kullanarak iş parçacıklarına dağıtır. Swift'te GCD'nin yerini büyük ölçüde async/await almıştır.İş Parçacığı Sorunları
fork() ve exec() İş Parçacıklarıyla
- Bazı UNIX'ler iki
forksürümü sunmuştu (tüm iş parçacıklarını ya da yalnızca çağıranı kopyalayan). POSIX: yalnızca çağıran iş parçacığı kopyalanır. fork'tan hemen sonraexecçağrılacaksa bu doğrudur: yeni program zaten tüm adres alanını değiştirir.- Çocukta
exec'e kadar yalnızca async-signal-safe fonksiyonlar güvenlidir;pthread_atforkkilitleri düzenlemeye yarar. Çoğu zamanposix_spawndaha iyi seçimdir.
İş Parçacığı Sorunları
Sinyal İşleme
Sinyal: sürece bir olayın olduğunu bildirir. Çok iş parçacıklı süreçte soru: hangi iş parçacığına teslim edilmeli?
| Seçenek | Örnek |
|---|---|
| Sinyalin ilgili olduğu iş parçacığına | senkron: SIGSEGV, SIGFPE |
| Süreçteki her iş parçacığına | bazı sonlandırma durumları |
| Sinyali engellemeyen herhangi birine | POSIX süreç sinyali: SIGINT |
| Tüm sinyalleri alan özel bir iş parçacığına | sigwait döngüsü |
- Sinyal işleyicileri süreç geneli, sinyal maskesi iş parçacığına özeldir:
pthread_sigmask. - Belirli bir iş parçacığına:
pthread_kill(tid, sig). - İyi uygulama: tüm iş parçacıklarında engelle, bir iş parçacığı
sigwaitile beklesin. - Kullanıcı düzeyi iş parçacıklarında çekirdek doğru iş parçacığını adresleyemez; yalnızca süreci tanır.
- Windows'ta sinyal yok; benzeri: APC (asynchronous procedure call).
İş Parçacığı Sorunları
İş Parçacığı İptali
main pthread_cancel(T) çağırır; ertelenmiş modda bu yalnızca bir istektir, T çalışmaya devam eder.
T bir iptal noktasına gelir: read, sleep, pthread_cond_wait, pthread_testcancel()…
pthread_cleanup_push ile kaydedilen temizlik işleyicileri çalışır: kilitler açılır, bellek geri verilir.
T sonlanır; pthread_join PTHREAD_CANCELED döndürür. Java'da karşılığı işbirlikçi interrupt()'tır; Thread.stop() JDK 20'den beri çalışmaz.
İş Parçacığı Sorunları
Global Değişken Çakışması
- Tek iş parçacıklı programlar için tasarlanmış global değişkenler (UNIX
errno) iş parçacıkları arasında çakışır. - Çözüm: değişkeni yasaklamak, ya da her iş parçacığına kendi kopyasını vermek. Bugünkü C kütüphanelerinde
errnoiş parçacığına yereldir.
İş Parçacığı Sorunları
İş Parçacığına Yerel Depolama (TLS)
Düz global: int hata; iki iş parçacığı aynı değişkeni okur ve yazar.
_Thread_local int hata; (C11) her iş parçacığına ayrı kopya verir; T1'in yazdığını T2 görmez.
Java ThreadLocal çıktısı (JDK 21): A: 1, B: 2, main: 0; main başlangıç değerini görür.
- Yerel değişkenden farkı: fonksiyon çağrıları boyunca yaşar; global gibi ama iş parçacığına özel.
- C11
_Thread_local, C23/C++thread_local, GCC__thread; Pthreads:pthread_key_create,pthread_setspecific. - Örnek kullanım:
errno, işlem kimliği, rastgele sayı üreteci.
static ThreadLocal<Integer> no =
ThreadLocal.withInitial(() -> 0);İş Parçacığı Sorunları
Yeniden Girilir ve İş Parçacığı Güvenli Kod
- Yeniden girilemez (non-reentrant) kütüphane yordamları iç durumlarını statik değişkenlerde saklar.
- Örnek: bir iş parçacığı mesajı ortak ara belleğe koyar, bağlam değişir, başka iş parçacığı aynı belleğe yazar → ilk mesaj kaybolur.
mallocserbest bellek listesini güncellerken listeyi geçici olarak tutarsız bırakır; araya giren iş parçacığı geçersiz işaretçi alabilir. Bugünkümalloc'lar iç kilit ve iş parçacığı başına arenalar kullanır.- İş parçacığı güvenli: birden çok iş parçacığından aynı anda çağrılabilir (kilitle ya da durumsuz). Yeniden girilir: paylaşılan durum kullanmaz.
| Güvensiz | Güvenli sürüm |
|---|---|
strtok | strtok_r |
localtime | localtime_r |
rand | rand_r / yerel üreteç |
asctime, ctime | strftime |
Java SimpleDateFormat | DateTimeFormatter |
İş Parçacığı Sorunları
Yarış Koşulu: sayac++
LOAD R, sayac ADD R, 1 STORE sayac, R
sayac++ tek satırdır ama makinede üç adımdır (oku, artır, yaz). Başlangıçta sayac = 5; iki iş parçacığı birer kez artırıyor. Beklenen sonuç: 7.
T1 değeri yazmacına okur: R1 = 5.
Araya T2 girer (ya da diğer çekirdekte aynı anda çalışır) ve o da 5 okur.
T2 artırıp yazar: sayac = 6.
T1 eski değerinden devam eder: 5 + 1 = 6 yazar. T2'nin artışı kayboldu (lost update).
Çözüm: oku-artır-yaz atomik olmalı: kilit ya da atomik işlem. Ayrıntılar Bölüm 6'da.
İş Parçacığı Sorunları
Yarışı Gözlemlemek (Java)
public class Yaris { static int sayac = 0; public static void main(String[] a) throws Exception { Runnable is = () -> { for (int i = 0; i < 1_000_000; i++) sayac++; }; Thread t1 = new Thread(is), t2 = new Thread(is); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("sayac = " + sayac); } }
sayac = 1456465 sayac = 1544210 sayac = 1910165
- Beklenen 2 000 000; her çalıştırmada farklı ve daha küçük bir sonuç çıkar.
- Sonuç belirlenimci değildir: çizelgelemeye, çekirdek sayısına ve JIT'e bağlıdır. Bu yüzden çıktı adım adım izlenemez.
- Düzeltmeler:
synchronizedblok,AtomicInteger.incrementAndGet(),LongAdder. - En iyisi: her iş parçacığı yerel değişkende saysın, sonda birleştirilsin (fork-join fikri).
İş Parçacığı Sorunları
Linux'ta İş Parçacıkları: clone()
- Linux “süreç” ve “iş parçacığı” ayırmaz: hepsi görev (task,
task_struct). - Ne paylaşılacağını
clone()bayrakları belirler; hiç bayrak yoksaforkgibidir.
pthread_create≈clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD | …).CLONE_THREAD: aynı iş parçacığı grubu;getpid()herkes için aynı (TGID),gettid()farklı.
Senkronizasyona Giriş
Pthreads Mutex ve Koşul Değişkeni
pthread_mutex_t the_mutex; pthread_cond_t condc, condp; int buffer = 0; /* tek hücreli tampon */ void *producer(void *ptr) { for (int i = 1; i <= MAX; i++) { pthread_mutex_lock(&the_mutex); while (buffer != 0) /* dolu: bekle */ pthread_cond_wait(&condp, &the_mutex); buffer = i; /* ürünü koy */ pthread_cond_signal(&condc); pthread_mutex_unlock(&the_mutex); } pthread_exit(0); }
void *consumer(void *ptr) { for (int i = 1; i <= MAX; i++) { pthread_mutex_lock(&the_mutex); while (buffer == 0) /* boş: bekle */ pthread_cond_wait(&condc, &the_mutex); buffer = 0; /* ürünü al */ pthread_cond_signal(&condp); pthread_mutex_unlock(&the_mutex); } pthread_exit(0); }
- mutex: tampona aynı anda tek iş parçacığı.
cond_waitkilidi bırakıp uyur, uyanınca yeniden alır.while: sahte uyanmalara (spurious wakeup) karşı koşul yeniden sınanır.
Senkronizasyona Giriş
Üretici–Tüketici Adım Adım
Üretici kilidi alır; tüketici lock'ta bekler. Tampon boş (0).
Tampon boş olduğu için üretici 1'i koyar ve condc ile tüketiciye sinyal verir.
Üretici kilidi bırakır; sonraki turda tampon dolu olduğundan condp'de bekler. Tüketici kilidi alır.
Tüketici ürünü alır (buffer = 0), üreticiye sinyal verip kilidi bırakır.
Üretici uyanır, koşulu yeniden sınar, 2'yi koyar. Böylece ikisi sırayla ilerler; hiçbir ürün kaybolmaz.
Tanenbaum & Bos, Şekil 2-32'den uyarlanmıştır. Semafor, monitör ve kilitlenme ayrıntıları Bölüm 6 ve 7'de.
Özet
Avantajlar ve Dezavantajlar
Avantajlar
- Duyarlılık: biri bloke olunca diğeri kullanıcı G/Ç'sini işler.
- Kaynak paylaşımı: bellek, açık dosyalar, soketler.
- Hız: oluşturma ve geçiş süreçlerden ucuz.
- Donanım paralelliği: tek süreç çok çekirdeği kullanır.
- Bloke eden çağrılarla bile paralellik (web sunucusu), kolay programlama modeli.
Dezavantajlar
- Senkronizasyon: paylaşılan veriye erişim denetlenmeli; yarış ve kilitlenme riski.
- Bağımlılık: aynı süreçteki iş parçacıkları bağımsız değildir; biri çökerse süreç çöker.
- Bellek koruması yok: bir iş parçacığı başkasının yığınının üzerine yazabilir.
- Hata ayıklama ve test zor; hatalar zamanlamaya bağlı.
Özet
Kontrol Soruları
- Yazmaç değerleri, heap, global değişkenler ve yığından hangileri bir sürecin iş parçacıkları arasında paylaşılır?
- Tek çekirdekli bir sistemde eşzamanlılık var mıdır? Paralellik var mıdır?
- Programın %80'i paralelleştirilebiliyor. 8 çekirdekte hızlanma ve teorik üst sınır nedir?
- Çoktan bire modelde bir iş parçacığı
read()ile bloke olursa ne olur? Neden çok çekirdekten yararlanamaz? - İki iş parçacığı başlangıçta 0 olan
sayac'ı birer kez artırıyor. Olası son değerler nelerdir? - İş parçacığı havuzunun yeni iş parçacığı oluşturmaya göre iki avantajını yazın.
- Ertelenmiş ve asenkron iptal arasındaki fark nedir? Neden ertelenmiş tercih edilir?
- POSIX'te üç iş parçacıklı bir süreçte
fork()sonrası çocukta kaç iş parçacığı vardır?
Cevaplar
- Heap ve global değişkenler paylaşılır; yazmaçlar ve yığın her iş parçacığına özeldir.
- Eşzamanlılık evet (zaman paylaşımı), paralellik hayır.
- 1 / (0,2 + 0,8/8) = 1 / 0,3 ≈ 3,33×; sınır 1 / 0,2 = 5×.
- Tek çekirdek iş parçacığı bloke olur, tüm süreç durur; çekirdekte aynı anda tek akış vardır.
- 2 ya da (kayıp güncellemeyle) 1.
- Hazır iş parçacığıyla daha hızlı hizmet; eşzamanlı iş parçacığı sayısını sınırlama.
- Asenkron hemen öldürür (kilit/veri tutarsız kalabilir); ertelenmiş iptal noktasında güvenle sonlanır.
- Bir: yalnızca
fork'u çağıran iş parçacığının kopyası.
Özet
Kaynaklar
- A. Silberschatz, P. B. Galvin, G. Gagne, Operating System Concepts, 10. baskı, Wiley, 2018 — Bölüm 4: Threads & Concurrency.
- A. S. Tanenbaum, H. Bos, Modern Operating Systems, 4. baskı (2014) / 5. baskı (2022), Pearson — Bölüm 2.2: Threads.
- R. H. Arpaci-Dusseau, A. C. Arpaci-Dusseau, Operating Systems: Three Easy Pieces (OSTEP), ostep.org — “Concurrency: An Introduction”, “Thread API”.
- POSIX.1-2017 (IEEE Std 1003.1),
pthreads(7)veclone(2)Linux kılavuz sayfaları. - JEP 444: Virtual Threads (Java 21); OpenMP 5.2 belirtimi.