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

İşletim Sistemleri
Süreç ve iş parçacığıPaylaşılan ve özel durumÇok iş parçacıklı sunucu Eşzamanlılık ve paralellikAmdahl yasasıKullanıcı ve çekirdek iş parçacıkları 1:1, N:1, M:N modelleriPthreads ve JavaHavuzlar ve fork-join OpenMP ve GCDYarış koşuluTLS, iptal, sinyaller
Sercan KÜLCÜ · Giresun Üniversitesi · Bilgisayar Mühendisliği

Giriş

Öğrenme Çıktıları

Kavram

İş parçacığını süreçten ayırmak; hangi durumun paylaşıldığını, hangisinin iş parçacığına özel olduğunu söylemek.

Performans

Eşzamanlılık ile paralelliği ayırt etmek ve Amdahl yasasıyla hızlanma sınırını hesaplamak.

Modeller

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.

Programlama

Pthreads ve Java ile iş parçacığı oluşturmak, beklemek; havuz, fork-join, OpenMP ve GCD gibi örtük yaklaşımları tanımak.

Sorunlar

Yarış koşulu, yeniden girilemeyen kod, TLS, iptal, sinyaller ve fork() davranışını açıklamak.

Gerçek sistemler

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.
TTCB · iş parçacığıtiddurumPCyazmaçlarSP → yığınöncelikTLS işaretçisiPCB · süreçpidadres alanıaçık dosyalarsinyal işleyicilerkimlik (uid/gid)iş parçacığı listesiçok sayıda TCB → bir PCB(Linux'ta ikisi de task_struct)
Eski adı hafif süreç (lightweight process, LWP): oluşturmak ve aralarında geçiş yapmak sürece göre ucuzdur, çünkü adres alanı kopyalanmaz.

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).
uygulama süreciekranı güncelleveri getirağ isteğine yanıtçekirdek (çizelgeleyici)

Temel Kavramlar

Tek ve Çok İş Parçacıklı Süreç

tek iş parçacıklı süreçkodveridosyalaryazmaçlaryığın

Tek iş parçacıklı süreç: bir yürütme akışı, bir PC, bir yazmaç kümesi ve bir yığın.

tek iş parçacıklı süreçkodveridosyalaryazmaçlaryığınçok iş parçacıklı süreç (3 iş parçacığı)kodveridosyalaryazmaçlaryığınyazmaçlaryığınyazmaçlaryığı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.

tek iş parçacıklı süreçkodveridosyalaryazmaçlaryığınçok iş parçacıklı süreç (3 iş parçacığı)kodveridosyalaryazmaçlaryığınyazmaçlaryığınyazmaçlaryığınüst sıra: paylaşılan · alt sıra: iş parçacığına özelüç akış aynı kodu ve global veriyi görü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
Yığın özeldir ama korunmaz: hepsi aynı adres alanında olduğundan hatalı bir işaretçi başka bir iş parçacığının yığınını bozabilir. Paylaşılan veriye erişim senkronizasyon gerektirir (Bölüm 6).

Temel Kavramlar

Her İş Parçacığının Kendi Yığını

çekirdek alanı (erişilemez)ana iş parçacığı yığını ↓T1 yığını ↓koruma sayfasıT2 yığını ↓koruma sayfasıpaylaşılan kütüphaneler / mmapheap ↑ (malloc)global verikod (text)yüksek adresdüşük adresT0 SPT1 SPT2 SP
  • 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ı mmap ile ayrılır; varsayılan boyut genellikle ulimit -s değ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

belge (bellekte, paylaşılan)etkileşimbiçimlendirmeotomatik kaydetmeçekirdekklavye / faredisk
EtkileşimTuş ve fare olaylarını hemen işler; arayüz donmaz.
BiçimlendirmeBir satır silinince yüzlerce sayfayı arka planda yeniden düzenler.
Otomatik kaydetmeBelgeyi belirli aralıklarla diske yazar; G/Ç beklerken kullanıcıyı bekletmez.

Üç 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

ağ bağlantısıweb sunucusu sürecidağıtıcıişçi 1işçi 2işçi 3web sayfası önbelleğiçekirdekdiskistek

Bir istek gelir: dağıtıcı (dispatcher) iş parçacığı isteği ağdan okur.

ağ bağlantısıweb sunucusu sürecidağıtıcıişçi 1işçi 2işçi 3web sayfası önbelleğiçekirdekdiskistekdevret

Dağıtıcı boştaki bir işçi (worker) iş parçacığını seçer, isteği ona devreder ve uyandırır.

ağ bağlantısıweb sunucusu sürecidağıtıcıişçi 1işçi 2işçi 3web sayfası önbelleğiçekirdekdiskistekdevretbulundu ✓yanıt

İşçi 1 sayfayı önbellekte bulur ve yanıtı gönderir; disk erişimi gerekmez.

ağ bağlantısıweb sunucusu sürecidağıtıcıişçi 1işçi 2işçi 3web sayfası önbelleğiçekirdekdiskistekyok ✗read()işçi 2 bloke;dağıtıcı ve işçi 3çalışmaya devam

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

(a) dağıtıcı iş parçacığı
while (TRUE) {
  get_next_request(&buf);  /* ağdan oku */
  handoff_work(&buf);      /* işçiye ver */
}
(b) işçi iş parçacığı
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

ModelParalellik / örtüşmeSistem çağrılarıProgramlama
Çok iş parçacıklı süreçvarbloke edensıralı kod, kolay
Tek iş parçacıklı süreçyokbloke edenkolay ama yavaş
Sonlu durum makinesi (olay güdümlü)var (G/Ç örtüşür)bloke etmeyen + bildirim / kesmezor: her isteğin durumu elle saklanır
Bugün: olay güdümlü model nginx, Node.js, Redis gibi sistemlerde yaygındır. Linux'ta 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ı

Duyarlılık (responsiveness)

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.

Kaynak paylaşımı

İş parçacıkları sürecin belleğini ve dosyalarını kendiliğinden paylaşır; paylaşılan bellek ya da mesaj iletimi kurmaktan kolaydır.

Ekonomi

Oluşturmak ve aralarında geçiş yapmak süreçlere göre ucuzdur: adres alanı kopyalanmaz, sayfa tabloları değişmez.

Ölçeklenebilirlik

Tek bir süreç birden çok çekirdekte aynı anda çalışabilir; tek iş parçacıklı süreç yalnızca bir çekirdek kullanır.

Sık aktarılan “oluşturma 30 kat, bağlam değiştirme 5 kat hızlı” rakamları eski bir Solaris ölçümüdür. Linux'ta süreç de iş parçacığı da 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

İş parçacığı → iş parçacığı (aynı süreç)yazmaçları, PC, SP'yi kaydetdiğerinin yazmaçlarını yükleadres alanı aynı: TLB ve önbellek sıcakSüreç → süreçyazmaçları, PC, SP'yi kaydetdiğerinin yazmaçlarını yüklesayfa tablosu tabanını değiştir (x86: CR3)TLB girdileri geçersiz (PCID/ASID yoksa)önbellekler soğuk: ilk erişimler yavaşucuz
  • 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

Eşzamanlılık (concurrency)Birden çok görev aynı zaman aralığında ilerler; tek çekirdekte iç içe geçerek bile olur.
Paralellik (parallelism)Birden çok görev aynı anda yürütülür; en az iki çekirdek (ya da işlem birimi) gerekir.
tek çekirdekT1T2T3T4T1T2T3T4her an yalnızca BİR görev çalışır; hepsi zamanla ilerlerzaman →

Tek çekirdekte zaman paylaşımı: eşzamanlı ama paralel değil.

çekirdek 1T1T3T1T3T1T3T1T3çekirdek 2T2T4T2T4T2T4T2T4aynı anda İKİ görev çalışırzaman →

İki çekirdekte T1/T3 ve T2/T4 aynı anda çalışır: paralel yürütme.

tek çekirdekT1T2T3T4T1T2T3T4çekirdek 1T1T3T1T3T1T3T1T3çekirdek 2T2T4T2T4T2T4T2T4(eşzamanlı)(paralel)zaman →

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ı

Görevleri belirlemeUygulamada birbirinden bağımsız, paralel yürüyebilecek etkinlikleri bulmak.
Denge (balance)Görevler benzer büyüklükte olmalı; küçük bir görev için çekirdek ayırmak kazandırmaz.
Veri bölmeGörevlerin eriştiği veri de çekirdeklere bölünmeli.
Veri bağımlılığıBir görev diğerinin sonucunu bekliyorsa erişim senkronize edilmeli.
Test ve hata ayıklamaYürütme sırası her çalıştırmada değişebilir; hatalar zor tekrarlanır.
Bellek etkileriAynı önbellek satırına yazan çekirdekler birbirini yavaşlatır (yanlış paylaşım, false sharing).
Araçlar: ThreadSanitizer (-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ği12345678çekirdek 0toplaçekirdek 1toplaçekirdek 2toplaçekirdek 3toplaaynı işlem · verinin farklı parçalarıGörev paralelliği12345678çekirdek 0minçekirdek 1maksçekirdek 2ortalamaçekirdek 3std. sapmafarklı işlemler · aynı veri
  • Veri paralelliği: N öğeli diziyi toplamak için her çekirdek N/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

seri %25paralel %75N = 1100% süre1,00×hızlanma

Örnek: S = 0,25. Tek çekirdekte süre %100, hızlanma 1×.

seri %25paralel %75N = 1100% süre1,00×N = 262,5% süre1,60×hızlanma

N = 2: 0,25 + 0,75/2 = 0,625 → hızlanma 1 / 0,625 = 1,60× (2× değil).

seri %25paralel %75N = 1100% süre1,00×N = 262,5% süre1,60×N = 443,75% süre2,29×hızlanma

N = 4: 0,25 + 0,75/4 = 0,4375 → hızlanma 2,29×.

seri %25paralel %75N = 1100% süre1,00×N = 262,5% süre1,60×N = 443,75% süre2,29×N → ∞25% süre4,00×hızlanma

N → ∞: paralel kısım sıfıra yaklaşır, seri %25 kalır → en fazla 4×.

Çok Çekirdekli Programlama

Amdahl Eğrileri

12481632641282565121024çekirdek sayısı N (log ölçek)05101520hızlanmaP = %95 (sınır 20)P = %90 (sınır 10)P = %75 (sınır 4)P = %50 (sınır 2)
  • 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ı

(a) kullanıcı düzeyinde yönetimkullanıcı alanıçekirdek alanısüreç 1çalışma zamanı sistemiiş parçacığı tablosusüreç 2çalışma zamanı sistemiiş parçacığı tablosusüreç tablosuçekirdek(b) çekirdek tarafından yönetimkullanıcı alanıçekirdek alanısüreç 1süreç 2süreç tablosuiş parçacığı tablosuçekirdek
  • 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ı select ile ö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

kullanıcı alanıçekirdek alanıU1U2U3K1çalışma zamanı sistemi (N:1)U1 çalışıyor

Çoktan bire (N:1): üç kullanıcı iş parçacığı tek bir çekirdek iş parçacığı (K1) üzerinde; çizelgelemeyi kütüphane yapar.

kullanıcı alanıçekirdek alanıU1U2U3K1çalışma zamanı sistemi (N:1)U1 read() çağırdı → K1 bloke → U2 ve U3 de bekler

U1 bloke eden read() çağırır: çekirdek yalnızca K1'i görür ve onu bloke eder → tüm süreç durur.

kullanıcı alanıçekirdek alanıU1U2U3K1K2K3bloke (G/Ç)çekirdek 0çekirdek 1

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.
Bugün

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

kullanıcı alanıçekirdek alanıU1U2U3U4K1

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

kullanıcı alanıçekirdek alanıU1U2U3U4K1K2K3K4

Bire bir (1:1): gerçek paralellik ve bağımsız bloke olma; her iş parçacığı çekirdek kaynağı harcar. Linux, Windows, macOS.

kullanıcı alanıçekirdek alanıU1U2U3U4U5K1K2K3

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

kullanıcı alanıçekirdek alanıU1U2U3U4U5K1K2K3K4bağlı

İ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çütN:11:1M:N
Geçiş maliyetiçok düşükorta (çekirdek)düşük
Bloke eden çağrıtüm süreç dururyalnız o iş parçacığıyalnız o iş parçacığı*
Çok çekirdekkullanılamazkullanılırkullanılır
İş parçacığı sayısıçok fazlaçekirdek belleğiyle sınırlıçok fazla
Gerçeklemekolay (kütüphane)kolayzor (iki çizelgeleyici)

* Çalışma zamanı, bloke olan çağrıyı fark edip başka çekirdek iş parçacığına geçebiliyorsa.

İşletim sistemi düzeyinde M:N terk edildi: Solaris 9 ve FreeBSD 7 1:1'e geçti. Ancak M:N dil çalışma zamanlarında geri döndü: Go goroutine'leri, Erlang süreçleri, Java 21 sanal iş parçacıkları.

İş Parçacığı Modelleri

Güncel M:N: Sanal İş Parçacıkları

sanal iş parçacıkları (milyonlarca olabilir)V1V2V3V4V5V6V7V8V9park: G/Ç bekliyorJVM zamanlayıcı (M:N, ForkJoinPool)taşıyıcı 1taşıyıcı 2taşıyıcı 3taşıyıcı 4işletim sistemi (1:1)çekirdek 0çekirdek 1çekirdek 2çekirdek 3V3, V6, V8 bloke değil, park edildi: taşıyıcıları başka sanal iş parçacıklarını çalıştırır
Java 21+ (Loom)Thread.ofVirtual().start(r); bloke eden G/Ç çağrısında JVM sanal iş parçacığını taşıyıcıdan indirir (unmount).
GoG (goroutine) · M (OS iş parçacığı) · P (mantıksal işlemci, GOMAXPROCS); boştaki P iş çalar.

İş Parçacığı Modelleri

Açılır (Pop-up) İş Parçacıkları

sunucu sürecimevcut iş parçacıklarıağ✉ mesajmesaj geliyor…

(a) Mesaj gelmeden önce: süreçte bu mesajı bekleyen bir iş parçacığı yoktur.

sunucu sürecimevcut iş parçacıklarıağ✉ mesajaçılır iş parçacığıyeni, boş yığınlahemen başlar

(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

POSIX PthreadsLinux, macOS, BSD; C API'si. Oluşturma, bekleme, mutex, koşul değişkeni.
Windows APICreateThread, WaitForSingleObject; çekirdek düzeyi, 1:1.
JavaThread, Runnable, ExecutorService; JVM, OS iş parçacıklarını (ve Java 21'den beri sanal iş parçacıklarını) kullanır.
C ve C++ standardıC11 <threads.h>; C++11 std::thread, C++20 std::jthread (eski Boost.Thread'in yerini aldı).
Pythonthreading; GIL yüzünden CPU işinde paralellik yok. 3.13'te deneysel, 3.14'te desteklenen GIL'siz derleme.
Gogo f() ile goroutine; M:N çalışma zamanı, kanallarla iletişim.
Asenkron iş parçacığı: ebeveyn çocuğu başlatıp kendi işine devam eder (ör. sunucu). Senkron: ebeveyn çocuklarının bitmesini bekler (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_createYeni iş parçacığı oluşturur, verilen fonksiyonu çalıştırır.
pthread_exitÇağıran iş parçacığını sonlandırır.
pthread_joinBelirli bir iş parçacığının bitmesini bekler.
pthread_detachKaynakları bitince kendiliğinden geri verilir; join edilmez.
pthread_selfKendi kimliğini döndürür.
sched_yieldCPU'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;
}
mainmain çalışıyorrunnerhenüz yok

Süreç tek iş parçacığıyla (main) başlar; global toplam 0'dır.

mainpthread_createrunnerhazır (çalışmaya aday)

pthread_create yeni iş parçacığını oluşturur; runner("5") çalışacaktır.

mainpthread_join → bekliyorrunnerçalışıyor

main join'de bloke olur. runner'ın ne zaman başlayacağını çizelgeleyici belirler.

mainbekliyorrunnerust = atoi("5")

runner kendi yığınında yerel ust değişkenini oluşturur.

mainbekliyorrunnerdöngü: 1+2+3+4+5

Döngü paylaşılan toplam'ı günceller: 15.

mainuyanıyorrunnerpthread_exit(0) → bitti

runner sonlanır; join döner ve main uyanır.

mainprintfrunnerbitti

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 join ile bekler ve kısmi toplamları birleştirir: sonuç her zaman 500500.
  • Argüman olarak &k geç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);
  }
}
mainRUNNABLEtNEW (oluşturuldu, başlamadı)

new Thread(lambda) yalnızca nesneyi oluşturur; durum NEW.

mainRUNNABLEtRUNNABLE

start() yeni OS iş parçacığını başlatır (run() çağırmak başlatmazdı!).

mainprintlntRUNNABLE

main yazmaya devam eder; t ile aynı anda çalışabilir.

mainWAITING (join)tRUNNABLE

join(): main, t bitene kadar WAITING durumunda.

mainuyanıyortTERMINATED

t döngüyü bitirir: toplam = 15, durum TERMINATED.

mainprintlntTERMINATED

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şımNeredeFikir
İş parçacığı havuzuJava ExecutorService, Windows thread poolHazır iş parçacıkları kuyruktaki görevleri alır.
Fork-joinJava ForkJoinPool, paralel akışlarGörev böl, paralel çöz, sonuçları birleştir.
OpenMPC, C++, FortranDerleyici yönergeleri ile paralel bölgeler.
Grand Central DispatchmacOS, iOS (libdispatch)Bloklar dağıtım kuyruklarına konur.
oneTBB (eski adı Intel TBB)C++ şablon kütüphanesiparallel_for, görev çizelgeleme, iş çalma.

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

İş Parçacığı Havuzu

görev kuyruğuG1G2G3G4G5G6submit(g)uygulamahavuz (3 iş parçacığı)işçi 1boşta, bekliyorişçi 2boşta, bekliyorişçi 3boşta, bekliyor

Havuz başlangıçta sabit sayıda iş parçacığı oluşturur; uygulama görevleri kuyruğa koyar.

görev kuyruğuG4G5G6submit(g)uygulamahavuz (3 iş parçacığı)işçi 1G1çalışıyorişçi 2G2çalışıyorişçi 3G3çalışıyor

Boştaki işçiler kuyruktan görev alır: yeni iş parçacığı oluşturma maliyeti yok.

görev kuyruğuG5G6submit(g)uygulamahavuz (3 iş parçacığı)işçi 1G4çalışıyorişçi 2G2çalışıyorişçi 3G3çalışıyor

İşçi 1, G1'i bitirince yok olmaz, hemen sıradaki G4'ü alır.

görev kuyruğuboşsubmit(g)uygulamahavuz (3 iş parçacığı)işçi 1boşta, bekliyorişçi 2boşta, bekliyorişçi 3boşta, bekliyor

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();
Çıktı (JDK 21)
toplam = 30
  • Dört görev (1², 2², 3², 4²) iki iş parçacıklı havuza verilir.
  • submit bir Future dö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

[3,1,4,1,5,9,2,6]görev

Büyük görev: 8 sayının toplamı. Eşikten büyükse böl.

[3,1,4,1,5,9,2,6][3,1,4,1][5,9,2,6]fork

fork: iki alt görev oluşturulur; havuzdaki farklı iş parçacıklarında çalışabilirler.

[3,1,4,1,5,9,2,6][3,1,4,1][5,9,2,6][3,1]= 4[4,1]= 5[5,9]= 14[2,6]= 8fork + hesapla

Alt görevler yine bölünür; eşiğe (2 öğe) inen yapraklar doğrudan hesaplanır: 4, 5, 14, 8.

[3,1,4,1,5,9,2,6][3,1,4,1]= 9[5,9,2,6]= 22[3,1]= 4[4,1]= 5[5,9]= 14[2,6]= 8join

join: ebeveyn çocuklarını bekler ve birleştirir: 4+5 = 9, 14+8 = 22.

[3,1,4,1,5,9,2,6]= 31[3,1,4,1]= 9[5,9,2,6]= 22[3,1]= 4[4,1]= 5[5,9]= 14[2,6]= 8join

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));
işçi 1abcdişçi 2boşkuyruğun başından çalarsahip kendi ucundan (sondan) alır

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_THREADS ya da num_threads(n) ile ayarlanır).
  • parallel for: döngü yinelemelerini iş parçacıklarına böler; reduction kı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"); });
dağıtım kuyruklarıana (seri)^^global (eşzamanlı)^^^^sistem iş parçacığı havuzu
oneTBB (Intel Threading Building Blocks'un açık kaynak devamı): 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

ebeveyn süreçT1T2T3T2: fork()çocuk süreçT2'yalnızca fork'u çağıran kopyalanırT1'in tuttuğu bir kilit çocukta sonsuza dek kilitli kalabilir!
  • Bazı UNIX'ler iki fork sü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 sonra exec ç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_atfork kilitleri düzenlemeye yarar. Çoğu zaman posix_spawn daha 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ığınasenkron: SIGSEGV, SIGFPE
Süreçteki her iş parçacığınabazı sonlandırma durumları
Sinyali engellemeyen herhangi birinePOSIX süreç sinyali: SIGINT
Tüm sinyalleri alan özel bir iş parçacığınasigwait 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ığı sigwait ile 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

Asenkron iptalHedef hemen sonlandırılır. Kilit tutarken ya da veriyi yarım güncellemişken ölebilir: tehlikeli.
Ertelenmiş iptal (varsayılan)Hedef iptal isteğini belirli iptal noktalarında denetler ve düzenli biçimde sonlanır.
mainhedef Tzamanpthread_cancel(T)istek bekliyor

main pthread_cancel(T) çağırır; ertelenmiş modda bu yalnızca bir istektir, T çalışmaya devam eder.

mainhedef Tzamanpthread_cancel(T)istek bekliyorread()iptal noktası

T bir iptal noktasına gelir: read, sleep, pthread_cond_wait, pthread_testcancel()…

mainhedef Tzamanpthread_cancel(T)istek bekliyorread()iptal noktasıtemizlik: unlock, free

pthread_cleanup_push ile kaydedilen temizlik işleyicileri çalışır: kilitler açılır, bellek geri verilir.

mainhedef Tzamanpthread_cancel(T)istek bekliyorread()iptal noktasıtemizlik: unlock, free✕ bittijoin → CANCELED

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ı

iş parçacığı 1iş parçacığı 2zamanaccess("dosya") → errno = EACCESbağlam değişimi →open("x") → errno = ENOENT← bağlam değişimierrno'yu oku → ENOENT ✗errno (tek kopya)
  • 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 errno iş parçacığına yereldir.

İş Parçacığı Sorunları

İş Parçacığına Yerel Depolama (TLS)

T1T2int hata = ?tek kopya: son yazan kazanır

Düz global: int hata; iki iş parçacığı aynı değişkeni okur ve yazar.

T1T2hata = ENOENThata = EACCESmain: —aynı ad, iş parçacığı başına ayrı kopya

_Thread_local int hata; (C11) her iş parçacığına ayrı kopya verir; T1'in yazdığını T2 görmez.

T1T2hata = 1hata = 2main: 0aynı ad, iş parçacığı başına ayrı kopya

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.
  • malloc serbest 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üvensizGüvenli sürüm
strtokstrtok_r
localtimelocaltime_r
randrand_r / yerel üreteç
asctime, ctimestrftime
Java SimpleDateFormatDateTimeFormatter

İş 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 (çekirdek 0)T2 (çekirdek 1)sayac = 5paylaşılan bellekLOAD R1 ← sayac → R1 = 5zaman

T1 değeri yazmacına okur: R1 = 5.

T1 (çekirdek 0)T2 (çekirdek 1)sayac = 5paylaşılan bellekLOAD R1 ← sayac → R1 = 5LOAD R2 ← sayac → R2 = 5zaman

Araya T2 girer (ya da diğer çekirdekte aynı anda çalışır) ve o da 5 okur.

T1 (çekirdek 0)T2 (çekirdek 1)sayac = 6paylaşılan bellekLOAD R1 ← sayac → R1 = 5LOAD R2 ← sayac → R2 = 5ADD R2, 1 · STORE → sayac = 6zaman

T2 artırıp yazar: sayac = 6.

T1 (çekirdek 0)T2 (çekirdek 1)sayac = 6paylaşılan bellekLOAD R1 ← sayac → R1 = 5LOAD R2 ← sayac → R2 = 5ADD R2, 1 · STORE → sayac = 6ADD R1, 1 · STORE → sayac = 6 ✗zaman

T1 eski değerinden devam eder: 5 + 1 = 6 yazar. T2'nin artışı kayboldu (lost update).

T1 (çekirdek 0)T2 (çekirdek 1)sayac = 7paylaşılan belleklock · LOAD 5 · ADD · STORE 6 · unlocklock · LOAD 6 · ADD · STORE 7 · unlockkritik bölge sırayla yürür: sonuç 7 ✓C: pthread_mutex_lock / atomic_fetch_add · Java: synchronized / AtomicInteger

Çö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);
  }
}
Üç ayrı çalıştırma (JDK 21)
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: synchronized blok, 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()

fork(): kopyatask_struct Atask_struct Bmm (bellek)mm (bellek) ′filesfiles ′fsfs ′sighandsighand ′clone(CLONE_VM | CLONE_FILES | …): paylaşımtask_struct Atask_struct Bmm (bellek)filesfssighand
  • Linux “süreç” ve “iş parçacığı” ayırmaz: hepsi görev (task, task_struct).
  • Ne paylaşılacağını clone() bayrakları belirler; hiç bayrak yoksa fork gibidir.
  • 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

üretici
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);
}
tüketici
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_wait kilidi 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

üreticitüketiciçalışıyor: locklock için bekliyorbuffer = 0mutex: üretici

Üretici kilidi alır; tüketici lock'ta bekler. Tampon boş (0).

üreticitüketicibuffer = 1, signal(condc)bekliyorbuffer = 1mutex: üretici

Tampon boş olduğu için üretici 1'i koyar ve condc ile tüketiciye sinyal verir.

üreticitüketiciunlock; i = 2: buffer≠0 → wait(condp)uyandı: lock aldıbuffer = 1mutex: tüketici

Üretici kilidi bırakır; sonraki turda tampon dolu olduğundan condp'de bekler. Tüketici kilidi alır.

üreticitüketicibekliyor (condp)buffer = 0, signal(condp), unlockbuffer = 0mutex: tüketici

Tüketici ürünü alır (buffer = 0), üreticiye sinyal verip kilidi bırakır.

üreticitüketiciuyandı: buffer = 2lock için bekliyorbuffer = 2mutex: üretici

Ü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ı.
Yalıtım gerekiyorsa (tarayıcı sekmeleri, güvenlik sınırları) süreç, sıkı veri paylaşımı ve düşük maliyet gerekiyorsa iş parçacığı seçilir. Chrome her ikisini birlikte kullanır.

Özet

Kontrol Soruları

  1. Yazmaç değerleri, heap, global değişkenler ve yığından hangileri bir sürecin iş parçacıkları arasında paylaşılır?
  2. Tek çekirdekli bir sistemde eşzamanlılık var mıdır? Paralellik var mıdır?
  3. Programın %80'i paralelleştirilebiliyor. 8 çekirdekte hızlanma ve teorik üst sınır nedir?
  4. Çoktan bire modelde bir iş parçacığı read() ile bloke olursa ne olur? Neden çok çekirdekten yararlanamaz?
  5. İki iş parçacığı başlangıçta 0 olan sayac'ı birer kez artırıyor. Olası son değerler nelerdir?
  6. İş parçacığı havuzunun yeni iş parçacığı oluşturmaya göre iki avantajını yazın.
  7. Ertelenmiş ve asenkron iptal arasındaki fark nedir? Neden ertelenmiş tercih edilir?
  8. POSIX'te üç iş parçacıklı bir süreçte fork() sonrası çocukta kaç iş parçacığı vardır?
Cevaplar
  1. Heap ve global değişkenler paylaşılır; yazmaçlar ve yığın her iş parçacığına özeldir.
  2. Eşzamanlılık evet (zaman paylaşımı), paralellik hayır.
  3. 1 / (0,2 + 0,8/8) = 1 / 0,3 ≈ 3,33×; sınır 1 / 0,2 = 5×.
  4. Tek çekirdek iş parçacığı bloke olur, tüm süreç durur; çekirdekte aynı anda tek akış vardır.
  5. 2 ya da (kayıp güncellemeyle) 1.
  6. Hazır iş parçacığıyla daha hızlı hizmet; eşzamanlı iş parçacığı sayısını sınırlama.
  7. Asenkron hemen öldürür (kilit/veri tutarsız kalabilir); ertelenmiş iptal noktasında güvenle sonlanır.
  8. 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) ve clone(2) Linux kılavuz sayfaları.
  • JEP 444: Virtual Threads (Java 21); OpenMP 5.2 belirtimi.

Son

Bölüm 4: İş Parçacıkları
Sonraki bölüm: CPU Çizelgeleme
1 / 1 Sercan KÜLCÜ, Tüm hakları saklıdır.