lip 26 2009

Kurs Qt – Część 4 – Wątki

Category: Kurs Qt,Programowanie,Qt,TechblogMatthew @ 21:51

Kolejna cześć kursu Qt. Tym razem będzie o wątkach. Niestety, praca i brak niebezpieczeństwa zawalenia egzaminów strasznie rozleniwiają, więc dopiero teraz udało mi się zebrać i coś napisać (nie lubię wakacji, masa czasu a nic nie udaje się zrobić ;) ). Stworzymy proste wątki, będzie to tradycyjny przykład producenta i konsumenta. Na początku z wyścigami, później postaramy się przed nimi zabezpieczyć. A więc do dzieła:

Aby stworzyć wątek należy utworzyć klasę, która będzie dziedziczyła po QThread oraz implementowała metodę run(). Najprościej wygląda to tak:

1
2
3
4
5
6
7
8
9
10
class Watek : public QThread
{
public:
   Watek();
   void run();
}

void Watek::run()
{
}

Taki wątek (po utworzeniu obiektu klasy) uruchamiamy metodą start():

1
2
3
4
Watek watek;

watek.start();
watek.wait();

Z kolei metoda wait() blokuje wątek do momentu w którym metoda run() nie zwróci wyniku lub czeka aż upłynie odpowiedni czas (podawany z milisekundach jako argument). A teraz przykład praktyczny:

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
103
104
105
106
107
108
109
110
111
112
113
114
115
#include <qtcore>
#include <qthread>
#include <qtime>
#include <iostream>

using namespace std;

const int rozmiar = 8192; // rozmiar bufora
int magazyn[rozmiar]; // bufor
int zajetosc = 0; // zajetosc bufora

class Producent : public QThread
{
public:
   Producent(int, int);
   void run();

private:
   int n;
   int i;
};

Producent::Producent(int n, int i)
{
   this->n = n;
   this->i = i;
}

void Producent::run()
{
   qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));

   cerr < < "Producent " << i << " start()" << endl;

   while(1)
   {
      this->msleep((int)qrand() % n);

      if (zajetosc < rozmiar)
      {
         ++zajetosc;
         this->msleep(50);
         magazyn[zajetosc] = (int)qrand() % 11;
         cerr < < "*" << i << "*" << " umieścił " <<magazyn[zajetosc] << " na polce " << zajetosc << endl;
      }
      else
      {
         cerr << "*" << i << "*" << " Brak wolnych pułek" << endl;
      }
   }
}

class Konsument : public QThread
{
public:
   Konsument(int, int);
   void run();

private:
   int n;
   int i;
};

Konsument::Konsument(int n, int i)
{
   this->n = n;
   this->i = i;
}

void Konsument::run()
{
   qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));

   cerr < < "Konsument " << i << " start()" << endl;

   while(1)
   {
      this->msleep((int)qrand() % n);

      if (zajetosc > 0)
      {
         magazyn[zajetosc] = (int)qrand() % 11;
         cerr < < "[" << i << "]" << " wzial " << magazyn[zajetosc] << " z polki " << zajetosc << endl;
         this->msleep(50);
         --zajetosc;
      }
      else
      {
         cerr < < "[" << i << "]" << " Brak towaru" << endl;
      }
   }
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    Producent producent1(123, 1);
    Producent producent2(95, 2);
    Producent producent3(52, 3);

    Konsument konsument1(94, 1);
    Konsument konsument2(326, 2);
    Konsument konsument3(123, 3);

    producent1.start();
    producent2.start();
    producent3.start();

    konsument1.start();
    konsument2.start();
    konsument3.start();

    return a.exec();
}

Kod producenta i konsumenta są bardzo proste. Mają magazyn, do którego dokładają lub pobierają dane. Oraz licznik, żeby wiedzieli która, ostatnia, półka jest zajęta. Wprowadziliśmy również pewne opóźnienia w pętlach wątków, aby całość wykonywała się trochę wolnej. Dodatkowe opóźnienie między zapisem/odczytem, a zmianą licznika, pozwala nam także zaobserwować możliwe wyścigi. Trochę to na siłę, ale w przeciwnym wypadku, dla tak prostego programu, bardzo trudno by było o taką sytuację (chociaż w przypadku bardziej złożonych aplikacji zdarza się to bardzo często). Teraz, aby pozbyć się wyścigów dodajemy jeden mutex. Blokujemy go również przed wykorzystaniem wrażliwych na wyścigi zmiennych:

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
// [...]
#include <qmutex>

// [...]

QMutex mutex;

// [...]

void Producent::run()
{
   qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));

   cerr < < "Producent " << i << " start()" << endl;

   while(1)
   {
      this->msleep((int)qrand() % n);

      mutex.lock();
      if (zajetosc < rozmiar)
      {
         ++zajetosc;
         this->msleep(50);
         magazyn[zajetosc] = (int)qrand() % 11;
         cerr < < "*" << i << "*" << " umieścił " <<magazyn[zajetosc] << " na polce " << zajetosc << endl;
      }
      else
      {
         cerr << "*" << i << "*" << " Brak wolnych pułek" << endl;
      }
      mutex.unlock();
   }
}

// dalsza część programu

Kod metodu run() konsumenta wygląda analogicznie. Dzięki temu prostemu zabiegowi pozbyliśmy się wyścigów. Możliwe jest również odmienne podejście do tematu z wykorzystaniem QWaitCondition. Jest to klasa, która pozwala synchronizować wiele wątków. Dzięki temu, jeden wątek może powiedzieć innym, że zaszło pewne zdarzenie, przez co pobudzić je do działania. A ponieważ nie warto od nowa wymyśleć koła, posłużymy się przykładem danym nam przez Qt Software i tylko omówię co ciekawsze framenty:

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
// [...]
#include <qwaitcondition>

// [...]

QWaitCondition bufferNotEmpty;
QWaitCondition bufferNotFull;

// [...]

void Producer::run()
{
   qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));

   for (int i = 0; i < DataSize; ++i)
   {
      mutex.lock();
      if (numUsedBytes == BufferSize)
         bufferNotFull.wait(&mutex);
      mutex.unlock();

      buffer[i % BufferSize] = "ACGT"[(int)qrand() % 4];

      mutex.lock();
      ++numUsedBytes;
      bufferNotEmpty.wakeAll();
      mutex.unlock();
   }
}

// dalsza część programu

Obiekt bufferNotFull informuje producenta o tym, że bufor przestał być pełen i znowu może zacząć się zapis. Z kolei bufferNotEmpty pilnuje konsumentów, żeby nie zaczęli czytać z pustego bufora. Jak widać, nie musimy również blokować mutexem samego zapisu, gdyż wystarczy, że będziemy tylko sprawdzać czy pojawiły się nowe dane do odczytu (producent zwiększa licznik gdy coś dołoży, konsument zmniejsza gdy zabierze).

To tyle, jeżeli chodzi o proste przedstawienie wątków. Gdyby ktoś zamiast mutexów i waitCondition chciał wykorzystać semafor, to tutaj znajduje się analogiczny przykład. Mam nadzieję, że na następną cześć nie będziecie musieli czekać aż tak długo. :)

Zostaw odpowiedź

Subskrybuj bez komentowania.