Monday, January 5, 2015

[C++] İş parçacıkları(Thread)


Evet, bugünkü konumuz iş parçacıkları, yani Thread'lar. Öncelikle bir iş parçacığının ne olduğu ile başlayalım.

İş parçacıkları, bir işlemin(process) sahip olduğu, belirli işler ve fonksiyonlar atayabildiği ayrı bir işlem gibi düşünülebilir. Varsayılan olarak her işlem "bir" iş parçacığına sahiptir, ve bu iş parçacığına "Ana iş parçacığı" denir(Main thread). Sadece bir iş parçacığına sahip uygulamalara ise tek-iş parçacıklı, yani single threaded uygulamalar adı verilir.

Normal olarak işletim sisteminin zamanlama algoritmasında, çalışmakta olan her işleme işlemciyi kullanması için bir zaman periyodu tanınır. Bu zaman periyodunda, işlem en son kaldığı yerden devam ederek bir sonraki fonksiyonu veya komutu işler, zamanı bittiğinde ise en son durumu işlem bloğuna kaydedilerek bir sonraki işleme geçilir. Örnek olarak;

[ explorer.exe | chrome.exe | csrss.exe | svchost.exe | cmd.exe | calc.exe | steam.exe | .... | winlogon.exe ]
0              5            10          17            24        30         32          45                    150

Çok-iş parçacıklı uygulamalarda ise, işlemin gerçekleştirdiği belirli fonksiyonlara bir iş parçacığı atanmıştır ve bu iş parçacıkları zamanlayıcı(scheduler) tarafından bağımsız olarak işlenirler. Örnek olarak;

[ explorer.exe |  chrome.exe  | ...]
[   t1|t2|t3   |  t1|t2|t3|t4 | ... ]
0     2  3     5    7  8  9   11


 Farzedelim ki, bir konsol uygulamamız var ve uygulamamızdaki bir hesaplama fonksiyonunun tamamlanma süresi 10 saniye olsun. Eğer uygulamanız tek iş parçacığına sahipse, uygulamanız 10 saniyelik fonksiyonunuzu çağırdığında donma yaşar ve bu süre zarfında ekrana yazacağınız herhangi bir komutu algılamaz. Örnek teşkil etmesi açısından aşağıdaki kodu deneyin ve sonucu görün. (bilindik bir örnek olarak pi sayısı hesaplama algoritması vereceğim)


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include "stdafx.h"
#include <cmath>
#include <iostream>

using namespace std;


int _tmain(int argc, _TCHAR* argv[])
{
 double sum = 0;
 double k;

 cout << "Pi sayisi hesaplaniyor.." << endl;
 for (k = 0; k <= 100000000; k++)
  sum += (sqrt(12)*(pow(-1, k) / ((2 * k + 1)*pow(3, k))));

 cout << "pi sayisi : " << sum << endl;
 cin.ignore();
 return 0;
}

Farkettiğiniz üzere, pi sayısı hesaplanırken konsola yazdığınız karakterler görünmezken, pi sayısı hesaplandıktan sonra yazdığınız tüm karakterler konsol ekranında belirdi. Bunun sebebi, çalışan tek iş parçacığınızın, yani ana iş parçacığının o an pi sayısını hesaplamakla uğraşıyor olmasıydı. Dolayısıyla klavyenizden gelen keystroke olaylarını işleyen fonksiyona vakit kalmadı.



Hepinizin kafasında beliren soru işaretini az çok tahmin edebiliyorum, "peki bu durumun üstesinden nasıl gelebiliriz?". Cevabı yeni iş parçacıkları yaratmakta yatıyor. İsterseniz ufaktan başlayalım.

Öncelikle, iş parçacıklarını Windows platrormunda kullanacaksanız, programınızda <Windows.h> kütüphanesini include etmelisiniz. Daha sonra yapmanız gereken şey ise, iş parçacığınız için bir fonksiyon yazmak olacak. Konunun devamını aşağıdaki kod üzerinde anlatacağım;




  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
// ThreadExample.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <Windows.h>
#include <iostream>

using namespace std;


// Global thread handle değişkeni
HANDLE thrHandle = NULL;

// İş parçacığının sonlanıp sonlanmaması gerektiğini tutan koşul değişkeni
bool m_bRunning = true;

DWORD WINAPI ThreadCall(LPVOID lpParam)
{
 // Durmalı mıyız?..
 while (m_bRunning)
 {
  cout << "is parcacigi says : i've got lots of work to do.." << endl;
  // İş parçacığımızı uyutalım..
  Sleep(1000);
 }
 cout << "is parcacigi durdu.." << endl;
 // İş parçacığımız başarı ile tamamlandı, geriye STATUS_SUCCESS döndürelim
 return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
 // Konsoldan okunan tuş verisini tutacak değişkenimiz
 char c;
 cout << "is parcacigi yaratiliyor.." << endl;
 /*
  CreateThread fonksiyonu, Windows platformunda iş parçacığı yaratmak için kullanılır.

  

  Aldığı 6 parametre bulunmaktadır, geriye yaratılan iş parçacığının unique handle değerini döndürür.
  Bu handle değeri iş parçacığını kontrol edebilmemiz için gerekli olduğundan, thrHandle değişkenine atadık.

  Parametrelerde şuan için ilgilenmeniz gereken sadece üçüncü ve beşinci parametre olduğu için,
  diğerlerini daha sonraki bir makalede anlatacağım.

  Üçüncü parametre, iş parçacığının fonksiyonudur. Yani iş parçacığı başladığında
  hangi fonksiyonu işletecek, onu belirtiyoruz.

  Beşinci parametre ise iş parçacığının nasıl yaratılacağını belirtir, 

  0 = iş parçacığı yaratılır yaratılmaz başlar
  CREATE_SUSPENDED(0x00000004) = iş parçacığı duraklatılmış durumda yaratılır (daha sonra ResumeThread ile başlatılması gerekir)

  Bu örneğimizde iş parçacığını duraklatılmış olarak yaratıp, sonra ResumeThread fonksiyonu ile başlatacağız.
 */
 thrHandle = CreateThread(NULL, 0, ThreadCall, NULL, CREATE_SUSPENDED, NULL);

 // thrHandle değeri sıfırdan büyükse, iş parçacığımız başarı ile yaratılmış demektir.
 if (thrHandle)
 {
  cout << "is parcacigi yaratildi, is parcacigi handle : " << thrHandle << endl;
 }
 cout << "is parcacigi baslatiliyor.." << endl;
 /*

  ResumeThread fonksiyonu, duraklatılmış herhangi bir iş parçacığını devam ettirmek 
  için kullanılır.
  Parametre olarak sadece duraklatmak istenilen iş parçacığının handle
  değerini alır.
 
 */
 ResumeThread(thrHandle);
 cout << "is parcacigi baslatildi.." << endl;
 cout << "is parcacigini duraklatmak icin 's',devam ettirmek için 'r', sonlandırmak için 'e' yazin.." << endl;
 while (true)
 {
  cin >> c;
  switch (c)
  {
  case 'e':
   m_bRunning = false;
   break;
  case 's':
   /*
    SuspendThread fonksiyonu çalışmakta olan herhangi bir iş parçacığını duraklatmak amacı
    ile kullanılır. Parametre olarak sadece duraklatmak istenilen iş parçacığının handle 
    değerini alır.
   
   */
   SuspendThread(thrHandle);
   cout << "is parcacigi duraklatildi. devam ettirmek icin 'r' yazin." << endl;
   break;
  case 'r':
   ResumeThread(thrHandle);
   cout << "is parcacigi devam ettirildi. duraklatmak için 's' yazin." << endl;
   break;
  }
 }
 
 cin.ignore();
 return 0;
}

Yukarıdaki uygulamayı çalıştırdığımızda, arka planda çalışan ThreadCall fonksiyonunun ana iş parçacığını bloke etmediğini farkedeceksiniz. Basit olarak multithreading mimarisi bunun üzerine kuruludur. Günümüzde neredeyse bütün uygulamalar multithreading kullanılarak yazılmaktadır.

Multithreading kullanmanın, özellikle çok-çekirdekli işlemcilerde gözle görünür performans artışı sağladığını göreceksiniz.

Bugün işlemin ana iş parçacığından bağımsız bir iş parçacığının nasıl yaratılacağını ve bu iş parçacığını nasıl kontrol edebileceğinizi öğrendiniz. İlerleyen zamanlarda fırsatını bulduğunuzda uygulamalarınızda bu methodu kullanarak gözle görünür düzeyde performans artışı ve stabilite sağlayabilirsiniz.

Umarım faydalı bir makale olmuştur.

İlk verdiğimiz pi sayısı örneğinin multithreading kullanılarak çözülmüş hali;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// ThreadExample.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <Windows.h>
#include <iostream>

using namespace std;


// Global thread handle değişkeni
HANDLE thrHandle = NULL;

// İş parçacığının sonlanıp sonlanmaması gerektiğini tutan koşul değişkeni
bool m_bRunning = true;

DWORD WINAPI ThreadCall(LPVOID lpParam)
{
 double sum = 0;
 // Durmalı mıyız?..
 while (m_bRunning)
 {
  sum = 0;
  double k;
  for (k = 0; k <= 1000000; k++)
   sum += (sqrt(12)*(pow(-1, k) / ((2 * k + 1)*pow(3, k)))); 
 }
 cout << "pi sayisi : " << sum << endl;
 cout << "is parcacigi durdu.." << endl;
 return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{

 char c;
 cout << "is parcacigi yaratiliyor.." << endl;

 thrHandle = CreateThread(NULL, 0, ThreadCall, NULL, CREATE_SUSPENDED, NULL);

 if (thrHandle)
 {
  cout << "is parcacigi yaratildi, is parcacigi handle : " << thrHandle << endl;
 }
 cout << "is parcacigi baslatiliyor.." << endl;

 ResumeThread(thrHandle);
 cout << "is parcacigi baslatildi.." << endl;
 cout << "is parcacigini duraklatmak icin 's',devam ettirmek için 'r', sonlandırmak için 'e' yazin.." << endl;
 while (true)
 {
  cin >> c;
  switch (c)
  {
  case 'e':
   m_bRunning = false;
   break;
  case 's':
  
   SuspendThread(thrHandle);
   cout << "is parcacigi duraklatildi. devam ettirmek icin 'r' yazin." << endl;
   break;
  case 'r':
   ResumeThread(thrHandle);
   cout << "is parcacigi devam ettirildi. duraklatmak için 's' yazin." << endl;
   break;
  }
 }
 
 cin.ignore();
 return 0;
}



Dip not : İlerleyen günlerde aynı konuyu C# ile de işlemeyi planlıyorum, .NET platformu kullananlar üzülmesinler :)

Gelecek konular : İş parçacıkları arası senkronizasyon, 'deadlock' nedir, senkronizasyon bileşenleri(mutex, critical section, semaphore) ve daha fazlası.

Sağlıcakla kalın.

Mustafa K. Bilgisayar Müh.

Kahve kokusu, visual studio, uykusuzluk ve huzur.

No comments:

Post a Comment