Thread Nedir? Detaylı Bir Thread İncelemesi
Bu yazıda thread yapısını detaylı bir şekilde anlatmaya çalışacağım. Çoğu yazının aksine bir programlama dili üzerinden örneklerle değil, işletim sistemindeki davranışı ve yaşam döngüsüyle alakalı bilgiler vermeyi umut ediyorum.
Unix ve Windows sistemlerde thread oluşturma, process ve thread kavramlarının farkları gibi konular ile alakalı inceleme yaparak bizlere ne gibi fayda sağladıklarını öğreneceğiz. Bunların yanı sıra paralel programlama, multi-threading vb. konular ile alakalı küçük açıklama notlarını da paylaşacağım.
Tanım
Bir process’in birden fazla işi aynı anda yapmasını sağlayan yapılara thread denir. Bir process bünyesinde bir ya da birden fazla thread barındırabilir. Thread’ler aynı anda sadece tek bir iş yapabilir. Kısaca N adet thread N adet iş yapabilir diyebiliriz. Thread’ler aynı zamanda ligth-weight process (hafif siklet proses) olarak da nitelendirilebilir. Thread kavramı Türkçe’deki en uygun karşılığı iş parçacıklarıdır.
Multi-threading ve parallel programming kavramlarına da yeri gelmişken birer cümle ile yer verelim.
Bir process içerisinde birden fazla thread çalıştırılmasına multi-threading diyoruz.
Thread’ler çok çekirdekli işlemcilerde farklı çekirdeklerde eş zamanlı olarak çalıştırılabilirler. Buna yaklaşıma da parallel programming diyoruz.
Thread konusunda karşımıza en sık çıkan iki farklı thread çeşidini de yazının ilerleyen kısımlarında inceleyeceğiz: User thread ve kernel thread.
Process (Süreç)
Öncelikle tanımsal bir ayrıştırma yapmak bu noktada daha iyi anlamamıza yardımcı olacaktır. Çalışan programlara process denir. Örnek olarak Word, Excel veya herhangi başka bir uygulama henüz çalışmıyorken bir programdır. Programlar çalıştırıldığında process olarak nitelendirilir. Process’ler hayatlarına tek bir thread ile başlar ve bu thread’e main thread adı verilmektedir. Diğer thread’ler ise programın çalışma esnasında sistem fonksiyonları tarafından yaratılmaktadır.
Wikipedia’daki tanıma göre process;
Bilgisayar bilimlerinde işlem (process) terimi, belleğe yüklenmiş ve işlemcide (CPU) yürütülmekte olan bir program olarak tanımlanmaktadır. Uygulamalar diskte çalışmaz halde bulunurken ise program olarak tanımlanır. Bir program (yürütülebilir dosya “.exe”) kendi başına pasif komut yığınıdır ve işlem ise bu komutların aktif olarak yürütülmesidir.
Buradaki tanımı çok daha basit bir örnek ile kavramaya çalışalım. Eminim herkes kariyerinin bir noktasında hesap makinesi yazmıştır. Kullanıcıdan sayıları alma, hangi işlem yapacağına ait bilgileri toplama, yapmak istediği işlemleri gerçekleştirip işlem sonucunu ekrana yazdırmak gibi çok basit bir süreçten bahsediyorum.
Cümlenin sonunda da söylediğim gibi tüm bu işlemlerin toplamı bir süreç yani process’tir. Yani kısaca process denilen yapı bir program çalıştığında yaptığı işlemlerdir.
Bir process kendi içerisinde başka bir process daha oluşturabilir, bu process’e ise child process adı verilir.
Process ve thread arasındaki farkları şöyle sıralayabiliriz:
- Thread process’lerin belirli bir segmentini işaret eder
- Bir thread’in oluşturulması ve sonlandırılması process’lere göre daha kısa sürer
- Process’ler birbirinden izole bir şekilde çalışırken, thread’ler aynı bellek kaynağını paylaşır
- Process’ler, thread’lere göre daha fazla kaynak tüketimi yaparlar
- Bir process bloklanırsa başka bir process işlemini gerçekleştiremez. Öncelikle ilk process’in işini bitirmesini bekler. Ancak bu durum thread’lerde geçerli değildir. Bir thread başka bir thread bloklansa bile çalışabilir.
- Ölçeklenebilirlik açısından thread’ler daha avantajlıdır. Process’lerin ölçeklendirilmesi için çok işlemcili yapı gerekmektedir. Bu da processler’de ölçeklendirmenin donanımsal olarak yapıldığını bizlere gösteriyor. thread’lerde ise çok çekirdekli işlemcilerde birden fazla thread farklı çekirdeklerde çalıştırılabilir.
Paralellism, birden fazla işlemci ya da çekirdekte aynı anda işlem yapılması anlamına gelmektedir. Concurrency ise tek bir process üzerinde eş zamanlı olarak birden fazla operasyonun gerçekleşmesidir. Konu ile ilgili 30 saniyede Concurrency ve Parallelism farkını anlatan Bora Kaşmer’in videosunu izlemeniz kafanızda daha da net bir fikir oluşturacaktır.
Thread Yaşam Döngüsü
İşletim sistemlerinde thread oluşturma işlemleri, Windows için CreateThread fonksiyonu ile yapılırken, Unix sistemlerde ise pthread_create() isimli POSIX fonksiyonu ile yapılmaktadır.
Thread’ler programlama dillerinin sunduğu çeşitli fonksiyon ve sınıflarla da yaratılabilir. .Net ve Java dillerinde yer alan Thread sınıfı ve bu sınıfa ait fonksiyonlar ile thread yaratılabilir, başlatılabilir ya da durdurulabilir.
Aksi belirtilmediği sürece, Windows için varsayılan thread boyutu 1 MB’dır. Linux sistemlerde ise varsayılan thread boyutu versiyondan versiyona değişiklik gösterebilir.
Her thread oluşturulmasından sonlandırılmasına kadar geçen süreçte farklı durumlara sahip olur. Aşağıdaki tabloda durumları ve ne anlama geldiklerini görebilir, diğer görselde ise örnek bir akış diyagramını inceleyebilirsiniz.
Yukarıdaki örnek diyagramı yazı ile açıklayalım.
Bir thread işletim sistemi veya programlama dilinin sunduğu fonksiyonlar aracılığıyla yaratıldığında ilk olarak NEW durumunda bekler. Eğer thread çalıştırılırsa Running durumuna geçer. Running/Runnable durumdaki bir thread, I/O isteğini gerçekleştirdiği esnada işlem bitene kadar Blocked durumunda kalır. Eğer thread geçici olarak durdurulmak istenirse belirtilen süre boyunca Waiting durumunda bekler ve süre bittiğinde tekrar Runnable durumuna geçer. Thread sonlandırıldığında ise Terminated durumuna geçer.
Thread’ler birçok farklı şekilde sonlanabilir. Eğer thread’in çalıştırdığı fonksiyon tamamlanırsa thread’de sonlanmış olur. Aynı zamanda main thread sonlanırsa process’e ait tüm thread’ler de sonlanmış olur.
Bir thread kendisini ya da başka bir thread’i sonlandırabilmektedir. Thread’lerin kendilerini sonlandırabilmeleri için Windows sistemlerde bu işlem ExitThread API fonksiyonu ile yapılırken UNIX işletim sistemlerinde ise pthread_exit() isimli POSIX fonksiyonu ile sonlandırma işlemi gerçekleştirilebilir.
Bir thread’in başka bir thread’i sonlandırması için ise Windows’ta TerminateThread fonksiyonu yer almakta, UNIX sistemlerde ise pthread_cancel() isimli POSIX fonksiyonu bulunmaktadır.
Kernel Thread
Kernel thread kavramı, process içinde yer alan ve kernel seviyesinde yönetilen bir thread türüdür. Kernel thread’ler işletim sistemi tarafından zamanlanmaktadır. Bu tip thread’ler sadece kernel modda çalışmaktadır. Yazılım geliştiricilerin bir kernel eklentisi ya da cihaz driver’ı yazmıyorlarsa bu thread türüne direkt erişimi yoktur. Yeni bir kernel thread yaratmak için kthread_create() ya da kthread_run() isimli fonksiyon kullanılır.
Process ve kernel thread arasındaki farklar ise;
- Kernel thread’ler kernel seviyesinde yönetilmektedirler
- Process’ler arası bir bilgi paylaşımı yoktur. Kernel thread’ler ise birbirleri arasında adres uzayı (address space) bilgisini paylaşırlar.
- Kernel thread’ler işletim sistemi tarafından zamanlanır
- Process’ler askıya alınabilirken, kernel thread’ler askıya alınamazlar
User Thread
User thread, kullanıcı tarafından yönetilmektedir ve kernel desteği bulunmamaktadır. Kernel, user thread’lerden habersizdir. Genelde bir uygulama geliştirildiğinde kullanılan thread’ler, user thread olarak geçmektedir. Örnek olarak Java ve .Net içerisinde yer alan Thread sınıfı user thread’lere örnek olarak verilebilir. CPU yalnızca Kernel thread çalıştırabildiği için user thread bir kernel thread’e eşlenerek çalıştırılır. Birazdan bu konuya da değineceğiz.
Process’leri anlatırken bir thread ile arasındaki farklardan zaten bahsetmiştik. Dilerseniz user thread ve kernel thread arasındaki farklara bir göz atalım.
- User thread’ler kullanıcılar tarafından Thread kütüphanelerinin sağladığı fonksiyonlar ile yönetilir. Kernel thread ise işletim sistemi tarafından yönetilmektedir.
- İşletim sistemleri user thread’ler ile alakalı herhangi bir düzenleme yapmaz.
- User thread’in implementasyonu kernel thread’e göre daha kolaydır.
- User thread’lerde bir thread sistem çağrılarını bloklayan bir operasyon gerçekleştirirse tüm process bloklanır. Kernel thread’lerde ise bir thread bloklasa bile farklı bir thread çalışmasına devam edebilir.
Kernel thread’lerde bir thread bloklansa bile başka bir thread’in çalışmaya devam etmesinin sebebi Kernel’ın bunu biliyor olması. Şöyle bir örnek üzerinden gidelim. Thread A ve thread B isminde iki farklı thread’imiz var. Thread A sistemi bloklasa bile kernel, thread B’nin varlığından haberdar olduğu için beklemeden thread B’yi çalıştırabilir.
Aynı durum user thread’ler için geçerli değil. Yukarıda da bahsettiğimiz gibi kernel, kullanıcı tarafından oluşturulan thread’lerden habersizdir. Bu thread’leri sadece uygulama bildiği için thread A sistemi blokladığında kernel thread B’yi çalıştıramaz.
Aklınıza şu soru gelebilir “neden birden fazla thread olduğu halde tek bir thread yüzünden tüm process duruyor?”. Bunu anlamak için multi-threading modellerine bir göz atalım.
1:1 (One to One) Model
Bu modelde her bir user thread, bir kernel thread ile eşleşmektedir ve thread yönetimi kernel seviyesinde yapılır. Buradaki user thread’lerden herhangi biri sistem çağrılarını blokladığında diğer thread’ler çalışmaya devam edebilir, böylece tüm process bloklanmaz. Concurrency açısından M:1 (Many to One) modele göre daha avantajlıdır. Ancak bu modelin dezavantajı ise daha çok efor gerektirmesi ve thread yönetiminin daha yavaş olmasıdır. Çoğu implementasyonunda yaratılabilecek thread sayısı limitlendirilmiştir.
M:1 (Many to One) Model
Bu modelde birden fazla user thread tek bir kernel thread’e maplenmiş durumdadır. Thread yönetimi thread kütüphanesi aracılığıyla user space’te yapılmaktadır. Eğer bir user thread sistem çağrılarını bloklayacak bir operasyon gerçekleştirirse tüm process bloklanır. Birim zamanda tek bir user thread Kernel’a erişebileceğinden dolayı diğer threadler çalışamazlar.
M:M (Many to Many) Model
Bu modelde ise user thread sayısı kadar kernel thread’e eşler. Bu modelde uygulamanın gerektiği kadar user thread yaratılabilir. Yaratılan user thread’lere karşılık gelen kernel thread’ler çok işlemcili makinelerde paralel olarak çalıştırılabilir. Herhangi bir thread bloklansa bile Kernel başka bir thread’i çalıştırabilir bu sayede tüm process bloklanmaz.
Hyper-threading (Simultaneous Multithreading-SMT) Nedir, Nasıl Çalışır?
Hyper-threading teknolojisi işlemci kapasitesinin daha iyi kullanılabilmesi için fiziksel bir işlemci çekirdeğini iki sanal işlemci çekirdeği gibi gösterebilmeye yarayan, Intel tarafından oluşturulmuş bir teknolojidir. İlk olarak Şubat 2002 tarihinde sunucularda kullanılan Xeon işlemciler ile hayatımıza giren bu teknoloji sonrasında Pentium 4 işlemciler ile beraber Kasım 2002 tarihinde masaüstü bilgisayarlarda da kullanılmaya başlandı. Her sanal işlemci diğer sanal işlemcilerden ve fiziksel işlemcilerden bağımsız olarak hareket eder. Kısaca hyper-threading tek bir işlemci çekirdeğinin, 2 farklı çekirdek gibi çalışabilmesini sağlar.
Hyper-threading çalışma mantığından önce CPU nasıl çalışıyor ona bakalım. CPU içerisinde 2 adet bölüm mevcut, biri kontrol birimi (control unit) diğeri ise aritmetik/mantık birimi (arithmetic/logic unit) kısaca ALU. Kontrol birimi elektrik sinyalleri ile direkt olarak bilgisayar sistemine çalıştırılması gerekenleri aktarır. Kontrol birimi herhangi bir mantıksal ya da aritmetik yönergeleri taşımaz onları çözerek bilgisayar sisteminin diğer parçalarına aktarır. Aritmetik/mantık birimi tüm bu aritmetik ve mantıksal operasyonları taşıma görevini üstlenir.
Bu anlattıklarımızı adımlandıralım.
1 – Kontrol birimi bilgisayar belleğinden talimatları alır.
2 – Bunları okuyup anlamlandırarak aritmetik/mantık birimine aktarır. 1. ve 2. adım birleşik olarak talimat/yönerge zamanı (instruction time) ya da I-time olarak isimlendirilir.
3 – Aritmetik/mantık birimi aritmetik ve mantıksal yönergeleri taşır. Bu kısım aynı zamanda aritmetik/mantık biriminin gerçekten operasyonu gerçekleştirdiği adımdır.
4 – Aritmetik/mantık birimi sonucu bellekte ya da register’da tutar. 3. ve 4. adım birleşik olarak çalışma zamanı (execution time) ya da E-time olarak isimlendirilir.
Bu adımlar kompleks görünse bile saniyeler içinde gerçekleşmektedir. Elbette bu süreç işlemci hızınızla ve farklı faktörlerle bağlantılı. Bu sebepten dolayı işlemciniz yeterince güçlü değilse sistemde yavaşlamalar ya da darboğazlar yaşanır. Tam bu noktada HT devreye giriyor. Hyper-threading her bir işlemci çekirdeğinin aynı anda iki işlem yapmasını sağlıyor. Bu da daha efektif işlemci kullanımı ve performansına kapı aralıyor. HT kernel içerisinde işlemciyi birden fazla işlemi aynı anda yapmak için kandırıyor diyebiliriz. HT işlemcinin farklı bölgelerini paralel bir biçimde kullanarak bir thread’in ihtiyaç duymadığı alanda başka bir thread’i çalıştırıyor. Ancak her iki thread’in aynı işlemci alanına ihtiyaç duyması halinde birinin diğerini beklemesi gerekiyor. Intel’in tanımına göre HT aktif olduğunda her fiziksel çekirdek için 2 execution context ortaya çıkarmaktadır. Bu da en temel anlamda CPU zamanlayıcısının birden fazla işi tek çekirdekte çalıştırabilmesini sağlıyor.
%100 CPU kullanımı, fiziksel alan kapasitesinin yetersizliğini değil işlemcinin başka bir thread’i zamanlayamadığının (scheduling) göstergesidir.
Son olarak thread başlığı altında bilinmesi gereken bazı tanımları kısa notlar halinde bu yazıyı tamamlıyorum.
Race Condition
Birden fazla thread ya da process’in aynı anda aynı kaynağa erişmeye ve durumunu değiştirmeye çalışması sonucunda ortaya çıkan durumdur.
Critical Section
Birden fazla thread ya da process tarafından paylaşılan kod alanlarına critical section adı verilir. Buradaki kontrolü sağlamak için Mutex, Semaphore, Monitor, Lock gibi yapılar kullanılmaktadır. Detaylara bu yazıdan ulaşabilirsiniz.
CPU Scheduling
İşlemcinin çalıştırılmaya hazır process’ler arasından seçim yaparak seçilen process’in çalıştırması işlemidir. İki farklı scheduler tipi mevcut birisi Short-term scheduler diğeri ise Long-term scheduler olarak isimlendirilir.
CPU Scheduling Algoritmaları
6 adet zamanlama (scheduling) algoritması vardır.
- First Come First Serve (FCFS)
- Shortest-Job-First (SJF) Scheduling
- Shortest Remaining Time
- Priority Scheduling
- Round Robin Scheduling
- Multilevel Queue Scheduling
Process Control Block (PCB)
İşletim sistemlerinin process’e ait tüm bilgileri saklamak için kullandığı veri yapısıdır. Her process’in bir PCB’si bulunur. PCB içerisindeki bilgiler process her durum değiştirdiğinde güncellenir.
Context Switch
Process’ler işlemcide sadece kendilerinin var olduğunu düşünen ve herhangi bir etkide bulunulmadığı takdirde tüm kaynağın ve adres uzaylarının kendisinin kullanımında olduğunu düşünen yapılardır. Bir process’in işlemci üzerinde çalışma zamanı dolduğunda o anki durumunu belleğe kaydedip işlemciden temizleyerek bir sonraki process’in çalıştırılması gerekli. Process durumunun kaydedilip, işlemciden temizlenmesi, bir sonraki process’in çalıştırılması ve zamanı geldiğinde process’in kaydedilen durumunun tekrar yüklenerek kaldığı yerden çalıştırılması işlemine context switch adı verilmektedir. Process bu süreçte işlemci üzerinden hiç ayrılmadığını düşünerek çalışmaya devam eder.
Symmetric Multiprocessing
Context switch sayısını azaltmanın yanı sıra işlemci üzerinde process’ler için daha fazla çalışma zamanı oluşturmanın bir diğer yolu ise Symmetric Multiprocessing’dir (SMP). SMP ile işletim sistemi aynı anda iki farklı process’i farklı işlemcilerde çalıştırabilir. Elbette bu process’ler iki işlemci olduğunda da sıra beklemek zorundadır. Ancak hizmet veren iki işlemci olduğundan ve çalışma süresi arttığından dolayı process’lerin çalışma süresi daha fazla ve bellekte beklediği süre daha az olacaktır. SMP iki ya da daha fazla işlemciye sahip bir işletim sisteminde main memory (MM) adı verilen, ortak bellek alanına sahiptir. Her işlemcinin veriye erişimi hızlandırmak ve veri trafiğini azaltmak için kendine ait önbelleği bulunmaktadır.
Kaynaklar :
http://locklessinc.com/articles/pthreads_on_windows/
https://github.com/torvalds/linux/blob/master/include/linux/kthread.h#L15
https://man.openbsd.org/kthread
https://www.kernel.org/doc/Documentation/x86/kernel-stack
pthread_create(3) – Linux manual page
Thread Class (System.Threading)
Win32 API – Create a new thread | winapi Tutorial
Default stack size for pthreads
https://en.wikipedia.org/wiki/Process_control_block