23 Eylül 2015 Çarşamba

Çoklu Görev (Multitasking ve Threading) nedir ? (C++ Uygulamalı)

Herkese merhaba,

Uzun bir aradan sonra bayram tatilini fırsat bilerek tekrar bir şeyler yazayım dedim. Bloğumu takip edenler bilir, hem teorik açıklama tarzında hem de pratik uygulama tarzında yazılarım var. Yeri geldiğinde ise bu ikisini birleştirmekten keyif alıyorum, çünkü hem daha açıklayıcı hem de uygulanabilir olması açısından okuması daha keyifli olabiliyor. Bu yazı, üzerine son zamanlarda epey vakit harcadığım ve çalıştığım bir konu üzerine olacak, bilgilerim tazeyken yazalım da dursun bir kenarda.

Çoklu İşlem/Görev Nedir ?
Eğer işletim sistemleriyle ilgili özel bir merakınız varsa muhakkak "Çoklu Görev" kavramını duymuşsunuzdur. Kısaca birden fazla programın yada programı oluşturan alt programcıkların eş zamanlı olarak çalışmasına (yada öyle görünmesine) "Çoklu Görev" diyoruz. İşletim sistemi literatüründe "Process" diye geçen şey aslında bilgisayarınızda eş zamanlı çalıştırdığınız ayrık programlardan başkası değil. Örnek olarak bir web tarayıcının ve bir kelime işleme programının aynı anda çalışıyor olması ve hatta bir de arka planda müzik dinliyor olmanız çoklu göreve güzel bir örnektir. Aslında bilgisayarınız işletim sisteminizin yardımıyla sizden habersiz daha bir çok programı, servis yazılımlarını, donanım kontrolü gibi işlemleri eş zamanlı gerçekleştirir. İşletim sisteminiz, siz web'de gezinirken bir yandan sürekli farenizin ve klavyenizin hareketlerini tarar, eğer bir yazdırma işlemi varsa onu gerçekleştirir, arka planda bir dosya işlemi varsa onu icra eder, bir yandan e-posta istemcinize bir mail gelmiş mi ona bakar vs. Kısaca bu ve bunlar gibi bir çok üst ve alt seviyede yürütülen işleri eş zamanlı gerçekleştirmeye çalışır. 




C gibi bir dilde programlama tecrübeniz varsa tüm programın bir main() fonksiyonu içerisinde icra edildiğini ve özellikle kullanıcıdan bir girdi beklerken tüm programın girdi beklenen satırda durduğunu bilirsiniz. Kullanıcı klavyeden bir girdi girmeden herhangi bir şey yapamazsınız. Aşağıdan yukarıya bu tür tek yönlü bir akış hem işlemci zamanınızın boşa harcanmasına hem de birim zamanda tek bir işlemin yürütülmesine, dolayısıyla da verimsizliğe yol açar. Nitekim ilk zaman komut satırıyla çalışan işletim sistemlerinde (DOS, UNIX) aynen bu durum söz konusudur. Bilgisayar herhangi bir zamanda sadece tek bir işlem ile meşgul olabilir ve o işlem bitmeden kullanıcının başka bir işlemi yapması veya başlatması mümkün değildir. Yani bir dosyayı bir yerden başka bir yere taşırken aynı zamanda bir disketin içeriğine erişemezsiniz, bunları sırayla yapmak zorundasınızdır ve bu yüzden işletim sistemi her işlemden sonra ne yapılacağını size soran bir komut satırına sahiptir.


İşletim sistemlerinin günümüz şekline evrilmesinde bu problemin çözümü önemli bir yer tutar. İşlemciler (tek çekirdekli olanları) her saat darbesinde sadece tek bir komutu işler, dolayısıyla birden fazla görevin eş zamanlı yapılması donanımsal olarak da imkansız görünmektedir. Ancak işlemcinin çoğu zaman "idle" modda çalışması ve bu nedenle işleme gücünün verimli bir şekilde kullanılamaması mühendislerin canını sıkmış olacak ki oldukça akıllıca bir çözüm geliştirmişler - "İşlemci zamanını paylaştırmak". Bu yöntemde "schedule" denen ve işletim sistemi tarafından hazırlanan bir plan dahilinde işlemci, işleme gücünü sırasıyla kısa bir süreliğine bir programa(process) verir. Programın bir kısmı işlendikten sonra bu işlem sonucu oluşan veriler RAM üzerine kaydedilir ve başka bir program işlemciyi kullanmaya başlar. Bu şekilde her program belli bir süre işlemcide işlenir ve sırasını "plan" daki diğer programa bırakır. İşlemci, çalışan tüm programları işleyip bitirene kadar bu planı uygular ve tamamlanan programlar plandan çıkarılır. Böylece gerçekte işlemci zamanını bölerek sırayla işlenen işlemler, yüksek saat (clock) hızlarında kullanıcılara eş zamanlı icra ediliyormuş gibi görünür. Bu yöntem sayesinden küçük ve hızlı işlemler, büyük ve uzun işlemlerin bitmesini beklemeden işlemci gücü en verimli şekilde kullanılarak tamamlanır ve pratik anlamda bizlere günümüz işletim sistemlerinin sağladığı aynı andan birden fazla işlem yapma kabiliyeti kazandırır. Buna paralel işlem kabiliyeti de denebilir.

Thread Nedir ?
Bir bilgisayarda birden fazla programın(process) aynı anda çalışmasına (bir çok işi aynı anda yapmaya) "çoklu görev" dendiğini öğrendik. Peki ya tek başlarına programlarda birden fazla paralel bir şekilde icra edilmesi gerek işlemlere ihtiyaç duyarlarsa !! Mesele Microsoft Word kelime işleme programını düşünün; hem klavyeden gelen veriyi dinlemesi hemde yazdırma komutu uygulandığından yazdırma işlemlerini yönetmesi gerekir. Birini yaparken diğerini iptal etmez yada ertelemez. Bir yandan yazıcıya komut gönderir diğer yandan da hala size belge üzerinde değişiklik yapma imkanı verir. Fiziksel olarak elbette ikisini bir arada yapmaz; yine yukarıda anlattığım gibi bir "plan" dahilinde bu işlemleri de parçalayarak farklı işlemci zamanlarında seri bir şekilde tamamlar, bu kullanıcı gözünde işlemlerin paralel bir şekilde olması demektir. 




İşte her ayrık programın içerisindeki, alt işlemler için özelleşmiş (klavye verisini dinle, yazıcıyı kontrol et gibi) "program parçalarına" yada diğer bir tabirle "programcıklara" işletim sistemi literatüründe "thread" denir. Thread'ler programların alt parçalarıdır ve program döngüsü içerisinde herhangi bir zamanda başlar ve yine herhangi bir zamanda sonlanırlar. Program yazarken aynı anda birden fazla işlem yapma ihtiyacınız olduğunda başvuracağınız çözüm thread'leri kullanmaktır. Thread'ler yukarıda belirtildiği gibi program döngüsü içerisinde herhangi yerde başlatılır ve "thread fonksiyonu" denilen kod parçacıklarını icra ederler. Bu fonksiyon içeriği icra edildikten sonra thread sonlandırılır ve tekrar başlatılmaz.

Visual C++ ve Threading
Buraya kadar anlattıklarımın akılda kalması için bir örnek yapalım. Paralel işlemi anlatmak için Visual C++ da bir Windows Application Form uygulaması açalım ve aşağıdaki formu tasarlayalım. [1 textbox, 1 button, 1 listbox] (Ben 2010 Express Edition kullanıyorum). Aslında form uygulaması da başlı başına threading'e bir örnektir. Çünkü form üzerine koyduğunuz her bileşen (buton, metin alanı vs.) bir program thread'i tarafından dinlenir. Butona tıkladığınızda yada metin alanına (textbox) bir şeyler yazdığınızda bu thread ilgili "Event Handler" kod parçalarını icra eder ve bu olaylara tayin ettiğiniz işlemleri gerçekleştirir. Bunu yapmak için ayrı olarak kod yazıp thread oluşturmanız gerekmez, geliştirme ortamınız ve işletim sisteminiz sizin için bu işi varsayılan olarak yapacaktır. 




.NET Platformunda thread'leri kullanmak için System::Threading isim alanını kodumuza ekleyelim.

using namespace System::Threading;

Bu alan adının sağladığı 'Thread' sınıfından iki tane thread nesnesini Form1 sınıfı içerisine global olarak tanımlayalım.

Thread^ th1;
Thread^ th2;

Bu örnekte 2 tane thread oluşturup her thread içinde, form ekranındaki listeye programın o thread'de olduğunu gösteren bir yazı ekleyeceğiz. Şimdi bu thread'lerin icra edecekleri Thread fonksiyonlarını Form1 sınıfımızın içerisine aşağıdaki gibi yazalım.

private: void ThreadFunction1()
{
while(1)
{
listBox1->Items->Add("Thread - 1 Fonksiyonu");
th1->Sleep(1000);
}
}

private: void ThreadFunction2()
{
while(1)
{
listBox1->Items->Add("Thread - 2 Fonksiyonu");
th2->Sleep(2000);
}

}

th1->Sleep(ms) ifadesi ilgili thread'in aldığı ms değeri kadar durdurulması için kullanılır. Bu örnekte while içerisindeki sonsuz döngüde thread'lerden biri 1 sn diğeri ise 2 sn durarak (ara vererek) çalışıyor ve ekrana hangi thread içerisinde olduğu yazdırılıyor. Dolayısıyla 1. thread aslında birim zamanda daha çok çalışmış oluyor ki uygulama sonunda da bunu göreceğiz.

Thread fonksiyonlarımız hazırsa, Form1 varsayılan yapıcı metoduna (default constructor) yani Form1 nesnesi oluşturulduğunda icra edilecek fonksiyona aşağıdaki kodları ekleyelim. Dikkat ederseniz her bir thread için yapıcı method, ilgli thread fonksiyonunu parametre olarak alıyor. CheckForIllegalCrossThreadCalls = false; ifadesi ise thread çakışmalarını önlemek için kullanılır. th1->Start() ise tahmin ettiğiniz üzere thread'leri başlatmak için.



Form1(void)
{
  InitializeComponent();
  //
  //TODO: Add the constructor code here
  //
  CheckForIllegalCrossThreadCalls = false;

  th1=gcnew Thread(gcnew ThreadStart(this,&Form1::ThreadFunction1));
  th2=gcnew Thread(gcnew ThreadStart(this,&Form1::ThreadFunction2));

  th1->Start();
  th2->Start();

}

Son olarak butona basıldığında metin alanındaki değeri listeye ekleyecek kodu ve form kapanırken thread'leri de sonlandıran kodları da ekleyerek programı çalıştıralım. 

private: System::Void button1_Click(System::Object^  sender, System::EventArgs^  e) 
{
listBox1->Items->Add(textBox1->Text);
}

private: System::Void Form1_FormClosing(System::Object^  sender, System::Windows::Forms::FormClosingEventArgs^  e) 
{
th1->Abort();
th2->Abort();
}


Görüldüğü gibi threadler program başlatıldığı andan itibaren paralel bir şekilde çalışmaya başlayacak ve thread fonksyionlarınca belirlenen görevlerini icra edeceklerdir. Metin alanına bir şeyler yazıp ekle dediğinizde ise program başka bir thread'i icra edecek ve ekrana girdiğiniz değeri yazdıracaktır. Tüm bu işlemler çakışmadan ve eş zamanlı olarak yürütülecektir. Ayrıca görüldüğü üzere Thread1 fonksiyonu Thread2 fonksiyonuna göre daha fazla icra edilecektir.  

--------------------------------------------------------------------------------------------------------------------------
Bir yazının daha sonuna geldik, eğer kafanızı karıştıran noktalar ve kodlamada anlamadığınız kısımlar varsa lütfen yorum atarak belirtin.

Hepinize iyi çalışmalar
Emin

@Emin_Ucer