Threads #3

Synchronize thread access to shared data

W pracy z wątkami możemy spotkać się z sytuacją, kiedy wiele wątków będzie chciało wykorzystywać do pracy te same obiekty. Ze względu na jednoczesne, wielokrotne operacje zarówno zapisu jak i odczytu tej samej zmiennej istnieje duże ryzyko utracenia spójności danych. Może się okazać, że któryś wątek będzie pracował na nie aktualnej wartości. Z pomocą w rozwiązaniu tego problemu przychodzi nam tutaj synchronizacja wątków i szereg narzędzi, które kryją się pod tym hasłem.


Na skróty:

Thread [API]

Monitory [Opis]
Metody synchronizowane [Opis]
Bloki synchronizowane [Opis]
Zmienne volatile [Opis]
Obiekty immutable [Opis]

Repozytorium z przykładami [GitHub]


Zasoby współdzielone

Zdarzyło Ci się kiedyś korzystać z publicznej toalety, której nie dało się od środka zamknąć? Siedzisz w środku i zastanawiasz się, wejdzie ktoś czy nie wejdzie? Cóż, z praw statystyki wynika, że jeśli nie Tobie, to komuś na pewno przytrafił się wariant pesymistyczny 🙂

Kiedy projektujemy aplikację wielowątkową, bardzo ważnym krokiem jest zidentyfikowanie tych elementów, które mogą narazić nasz system na utratę spójności – elementów, które nie radzą sobie dobrze z obsługiwaniem wielu wątków jednocześnie.

Kandydatami do takich elementów są w Javie praktycznie wszystkie obiekty, ale na szczęście pewne ich części możemy z góry wykluczyć:

  • Zmienne lokalne
  • Parametry metod
  • Wartości zwracane za pomocą słowa kluczowego return

We wszystkich tych przypadkach mamy do czynienia z wartościami, które widoczne są tylko w obrębie wątku w którym zostały utworzone i nie mogą być zmodyfikowane przez inne wątki.

Inaczej ma się sprawa w przypadku obiektów, których referencje możemy przekazać do wielu wątków. Również, problematyczne są zmienne statyczne, które istnieją w „jednym egzemplarzu” dla całego systemu.

Przykładowo rozważmy sobie przypadek fabryki czekolady. Mamy klasę Chocolate, która zawiera statyczną zmienną informującą o tym ile sztuk czekolady wyprodukowaliśmy.

public class Chocolate {

  public static int producedChocolates = 0;
  
  public Chocolate(){
    producedChocolates = producedChocolates + 1;
  }
}

Mamy też linię produkcyjną, która po uruchomieniu, wytwarza dokładnie jedną tabliczkę czekolady.

public class ChocolateProductionLine extends Thread {

  public void run(){
    new Chocolate();
  }
}

Zgodnie z konstruktorem klasy Chocolate, utworzenie obiektu tej klasy, zwiększa jej statyczny licznik o jeden. W teorii licznik powinien mieć taką wartość, ile razy został utworzony obiekt klasy Chocolate.

Sprawdźmy. Uruchomimy dziesięć linii produkcyjnych, z których każda wytworzy dokładnie po jednej sztuce czekolady (ktoś coś mówił o optymalizacji? 🙂 )

public class Main {

  public static void main(String[] args) {
    
    for(int i = 0; i<10; i++ ){
      new ChocolateProductionLine().start();
    }
    
    System.out.println(Chocolate.producedChocolates);
  }

}

A oto wyniki z pięciu kolejnych uruchomień metody main:

10
10
8
9
10

Ale jak to? Ano tak, że nie zabezpieczyliśmy statycznej zmiennej przed „jednoczesnym” dostępem dla kilku wątków. Słowo jednoczesnym napisałem w cudzysłowie, ponieważ w rzeczywistości, na raz ze zmiennej może korzystać tylko jeden wątek, ale problem leży w tym, że to co dla nas jest jedną operacją zwiększania licznika, dla komputera w cale nie jest „na raz”.

Newralgicznym fragmentem naszego kodu jest oczywiście treść konstruktora klasy Chocolate:

public Chocolate(){
    producedChocolates = producedChocolates + 1;
  }

Choć jest to jedna linijka, to w rzeczywistości są to trzy operacje:

  1. Pobierz wartość zmiennej producedChocolates
  2. Dodaj 1 do pobranej wartości producedChocolates
  3. Przypisz wynik poprzedniej operacji do zmiennej producedChocolates

Zobaczmy teraz jak powyższy kod może zostać „rozbity”, kiedy dopuścimy do niego na raz 3 wątki. Jest to tylko jeden z możliwych scenariuszy:

Wątek 1: Pobieram producedChocolates, producedChocolates wynosi 0
Wątek 2: Pobieram producedChocolates, producedChocolates wynosi 0
Wątek 1: Dodaję 1 do 0
Wątek 3: Pobieram producedChocolates, producedChocolates wynosi 0
Wątek 1: Zapisuję 1 do producedChocolates, więc producedChocolates jest równy 1
Wątek 2: Dodaję 1 do 0
Wątek 3: Dodaję 1 do 0
Wątek 3: Zapisuję 1 do producedChocolates, więc producedChocolates jest równy 1
Wątek 2: Zapisuję 1 do producedChocolates, więc producedChocolates jest równy 1

Mimo tego, że wyprodukowaliśmy trzy czekolady, licznik wskazuje jeden.

Analogiczny problem możemy mieć, kiedy utworzymy jeden obiekt i przekażemy referencję do niego wielu wątkom. Spójrzmy na szybki przykład:

public class SingleObject {
  private int intValue = 0;
  public void incrementIntValue(){
    System.out.println("Wartość przed inkrementacją = " + intValue);
    intValue++;
    System.out.println("Wartość po inkrementacji = " + intValue);
  }
}

Klasa SimpleObject zawiera zmienną int oraz metodę, która zwiększa wartość tej zmiennej o jeden.

public class SingleObjectSample {
  public static void main(String[] args) { 
    SingleObject singleObject = new SingleObject();
    for(int i = 0; i<5; i++ ){ 
      new SampleThread(singleObject).start(); 
    }
  }

}

class SampleThread extends Thread{
  SingleObject singleObject;
  SampleThread(SingleObject singleObject){
    this.singleObject = singleObject;
  }
  public void run(){
    singleObject.incrementIntValue();
  }
}

Tutaj tworzymy JEDNĄ instancję klasy SingleObject i przekazujemy ją do pięciu wątków, z których każdy jeden raz woła metodę incrementIntValue(). Teoretycznie, powinniśmy zakończyć kod, z wartością naszej zmiennej równą 5. A w praktyce? Oto jeden z wyników wykonania kodu:

Wartość przed inkrementacją = 0
Wartość przed inkrementacją = 0
Wartość po inkrementacji = 2
Wartość przed inkrementacją = 0
Wartość po inkrementacji = 3
Wartość przed inkrementacją = 0
Wartość po inkrementacji = 4
Wartość przed inkrementacją = 0
Wartość po inkrementacji = 5
Wartość po inkrementacji = 1

Wiemy już z jakimi niebezpieczeństwami wiąże się współdzielenie zasobów. W takim razie jak się przed tymi niebezpieczeństwami zabezpieczać?

Monitory

Podstawową odpowiedzią Javy na problem synchronizacji dostępu do zasobów są tak zwane monitory. Monitor, to taki strażnik, który pilnuje, aby z danego zasobu w jednej chwili korzystał tylko jeden wątek. Dokładnie mówi się, że wątek ma „pozyskać” (ang. acquire) monitor. Możemy się też spotkać z określeniami „trzymać” (ang. hold) lub „blokować” (ang. lock) monitor. Jeżeli wątek zablokuje monitor danego obiektu, żaden inny wątek nie ma prawa wykonać synchronizowanych metod lub bloków tego obiektu. Jeśli kilka wątków czeka w kolejce na pozyskanie monitora tego samego obiektu, nie mamy wpływu na to, który wątek dostanie taką możliwość, gdy monitor się zwolni. Natomiast jeśli monitor zostanie już zablokowany, to tylko wątek, który posiada monitor, może go zwolnić.

Dobrze, więc czym są wspomniane synchronizowane metody i bloki?

Synchronizowane metody

Są to metody, których deklaracja poprzedzona jest słowem kluczowym „synchronized” . W ich przypadku mamy pewność, że całe ciało metody wykona się jako niepodzielna operacja w ramach jednego wątku, który akurat pozyskał monitor.

public class SynchronizedObject {
  static int counter = 0;
  synchronized public void  incrementCounter(){
    counter = counter +1;
    System.out.println(counter);
  }
}

Powyżej widzimy obiekt, który analogicznie jak w poprzednich przykładach posiada statyczny licznik, oraz metodę, która zwiększa wartość tego licznika. Jedyną różnicą jest to, że metoda posiada słowo kluczowe „synchronized” . Mamy więc pewność, że operacja

counter = counter +1;

którą wcześniej rozdzieliliśmy sobie na trzy oddzielne kroki, z których każdy mógł trafić do innego wątku, tym razem zostanie wykonana w całości w jednym wątku.

W związku z tym, kiedy utworzymy sobie jedną instancję obiektu klasy SynchronizedObject i przekażemy ją do pięciu wątków:

public class SynchronizedObjectSample {
  public static void main(String[] args) {
    SynchronizedObject singleObject = new SynchronizedObject();
    for(int i = 0; i<5; i++ ){ 
      new SampleThread(singleObject).start(); 
    }
  }
}

class SampleThread extends Thread{
  SynchronizedObject singleObject;
  SampleThread(SynchronizedObject singleObject){
    this.singleObject = singleObject;
  }
  public void run(){
    singleObject.incrementCounter();
  }
}

Zawsze wynik działania będzie ten sam:

1
2
3
4
5

Kiedy wątek chce wykonać synchronizowaną metodę, automatycznie ustawia się w kolejce do pozyskania monitora i może kontynuować pracę dopiero, kiedy ten monitor zablokuje. Jeśli mamy wiele wątków, a synchronizowana metoda ma wiele do zrobienia, oczywistym jest, że praca systemu w tym miejscu zwolni, ponieważ metoda działa jak wąskie gardło, przez które wszystkie wątki muszą przechodzić pojedynczo. Czasem jednak okazuje się, że w całej metodzie, tylko mały fragment dotyczy zasobów współdzielonych. Możemy przyśpieszyć przetwarzanie, czyniąc synchronizowanym tylko ten fragment, zamiast całej metody. Służą do tego synchronizowane bloki.

Synchronizowane bloki

Mechanizm bloków synchronizowanych daje nam możliwość objęcia monitorem tylko wybranego fragmentu metody, zamiast całego jej ciała. Sztandarowym przykładem wykorzystania tego mechanizmu jest jedna z możliwych implementacji wzorca projektowego singleton:

public class Singleton {
  private static Singleton instance;
  private Singleton() {
  }
  public static Singleton getInstance() {
    if (instance == null) {
      synchronized (Singleton.class) {      
        if (instance != null) {
          instance = new Singleton();
        }
      }
    }
    return instance;
  }
}

Zgodnie ze wzorcem, w całym systemie może istnieć tylko jeden obiekt danej klasy. Standardowo klasa taka ma prywatny konstruktor, a jej instancję pobiera się za pomocą statycznej, publicznej metody. Metoda ta najpierw sprawdza, czy już wcześniej utworzyła instancję swojej klasy, jeśli tak, zwraca do niej referencję, jeśli nie, tworzy instancję i dopiero zwraca do niej referencję. W prostszej wersji implementacji singletona moglibyśmy całą metodą getInstance() uczynić synchronizowaną. Wzorzec działałby prawidłowo. Natomiast warto zauważyć, że sprawdzenie, czy instancja klasy już istnieje i zwrócenie referencji do tej klasy nie stanowi żadnego zagrożenia dla spójności systemu i bez obaw możemy do tego fragmentu dopuścić wiele wątków na raz. Niebezpiecznie robi się dopiero, kiedy okaże się, że instancja nie została jeszcze utworzona i trzeba to zrobić. Gdybyśmy dopuścili do tego dwa wątki jednocześnie- powstały by dwa obiekty, co automatycznie jest zaprzeczeniem wzorca Singleton. Dlatego samo tworzenie obiektu umieszczamy w synchronizowanym bloku. Zwracam uwagę, że wewnątrz bloku musimy ponownie upewnić się, czy w międzyczasie instancja jednak nie powstawała, bo istnieje możliwość, że jakiś inny wątek zdążył już poprosić o dostęp do monitora i uzyskać go wcześniej. Kiedy utworzymy już obiekt, każdy następny wątek dostanie referencję do niego, bez konieczności czekania na zablokowanie monitora. Eliminujemy w ten sposób wąskie gardło.

Czym jest „Singleton.class” podane w nawiasie przed synchronizowanym blokiem? W przypadku synchronizowanych metod, monitorem jest obiekt, do którego należy wołana metoda. W przypadku synchronizowanych bloków możemy wskazać co ma być monitorem. Standardowo możemy użyć słowa kluczowego „this” , co oznacza, że monitorem jest obiekt klasy, w której umieściliśmy synchronizowany blok, ale w tej sytuacji nasz obiekt jeszcze nie istnieje, więc blok synchronizujemy wobec klasy- monitor blokuje dostęp do wszystkich operacji powiązanych z tą klasą (w tym tworzenie obiektu tej klasy, przez inne wątki).

Może się jednak okazać, że nasz blok powinien być synchronizowany w szerszym obrębie- na przykład z innymi blokami, z innych obiektów, które robią zupełnie coś innego, ale nie powinno to być robione jednocześnie z tym co robi nasz blok. Wtedy możemy utworzyć dowolny obiekt i przekazać go do tych wszystkich bloków jako wspólny monitor.

Rozważmy sobie bardziej obrazowy przykład. Mamy sklep, w którym przeprowadzany jest remont, na tyle mały, że sklep jest w dalszym ciągu otwarty dla klientów. Niestety w sklepie jest tylko jedna toaleta i mogą z niej korzystać zarówno panowie pracujący przy remoncie jak i klienci sklepu.

Mamy w takim razie dwie bliźniacze klasy reprezentujące pracownika i klienta:

public class Worker extends Thread{
  private WaterCloset wc;
  private String name;
  public Worker(String name, WaterCloset wc){ 
    this.name = name;
    this.wc = wc;
  }
  public void run(){
    System.out.println(String.format("Worker %s is working", name)); 
    try {
      Thread.sleep((long) (1000l*Math.random()));
    } catch (InterruptedException e) {
      e.printStackTrace();
    } 
    System.out.println(String.format("Worker %s is going to WC", name));
    synchronized(wc){
      System.out.println(String.format("Worker %s is inside WC", name));
      wc.flushWater();
      System.out.println(String.format("Worker %s is leaving WC", name));
    }
  }
}

 

public class Customer extends Thread{
  private WaterCloset wc;
  private String name;
  public Customer(String name, WaterCloset wc){ 
    this.name = name;
    this.wc = wc;
  }
  public void run(){
    System.out.println(String.format("Customer %s is shopping", name));
    try {
      Thread.sleep((long) (1000l*Math.random()));
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println(String.format("Customer %s is going to WC", name));
    synchronized(wc){
      System.out.println(String.format("Customer %s is inside WC", name));
      wc.flushWater();
      System.out.println(String.format("Customer %s is leaving WC", name));
    }
  }
}

Klasy te są wątkami, które w konstruktorze przyjmują referencję do obiektu WaterCloset, czyli do naszej sklepowej toalety. W metodzie run mamy blok synchronizowany względem tej toalety. W ten sposób zapewniamy sobie, że w jednej chwili tylko jedna osoba będzie mogła z niej korzystać.

Warto tutaj również zaznaczyć, że powyższe klasy łamią dobre praktyki programowania, ponieważ są praktycznie identyczne, ale celowo chcę pokazać, że synchronizować ze sobą można różne obiekty.

Toaleta w naszym sklepie nie jest zwykłą toaletą, dzięki zastosowaniu najnowszych technologii, toaleta zlicza ilość spłukań wody:

public class WaterCloset {
  private int flushCounter = 0;
  public void flushWater(){
    System.out.println("Water flushes today: " + ++flushCounter);
  }
}

Dzięki temu licznikowi, oraz wiedzy, że każda osoba korzystająca z WC miała na tyle skromną potrzebę, że spłukała wodę tylko raz, możemy się upewnić, że każdy korzystał z toalety oddzielnie i nikt mu w tym czasie nie przeszkadzał:

public class Shop {
  public static void main(String[] args) {
    WaterCloset wc = new WaterCloset();
    
    Worker w1 = new Worker("Johny", wc);
    Worker w2 = new Worker("George", wc);
    
    Customer c1 = new Customer("April", wc);
    Customer c2 = new Customer("Winter", wc);
    Customer c3 = new Customer("Tom", wc);
    
    w1.start();
    w2.start();
    c1.start();
    c2.start();
    c3.start();
  }

}
Worker Johny is working
Worker George is working
Customer April is shopping
Customer Winter is shopping
Customer Tom is shopping
Worker George is going to WC
Worker George is inside WC
Water flushes today: 1
Worker George is leaving WC
Worker Johny is going to WC
Worker Johny is inside WC
Water flushes today: 2
Worker Johny is leaving WC
Customer Tom is going to WC
Customer Tom is inside WC
Water flushes today: 3
Customer Tom is leaving WC
Customer April is going to WC
Customer April is inside WC
Water flushes today: 4
Customer April is leaving WC
Customer Winter is going to WC
Customer Winter is inside WC
Water flushes today: 5
Customer Winter is leaving WC

Po powyższym, jednym z możliwych wyników działania kodu, widać, że wszystkie trzy linie kodu zawarte w bloku synchronizowanym zawsze wykonywały się razem, w jednym wątku.

Zmienne volatile

Czasem może się okazać, że synchronizowane metody, czy nawet bloki to trochę za dużo na potrzeby naszej operacji. Być może potrzebujemy tylko odczytać wartość danej zmiennej, która jest często modyfikowana przez wiele wątków. Zależy nam aby mieć aktualną wartość zmiennej, ale nie chcemy spowalniać procesowania dodatkowymi monitorami. W takiej sytuacji z pomocą przychodzi słowo kluczowe „volatile” umieszczone przed deklaracją typu zmiennej:

private volatile int flushCounter = 0;

Dla maszyny Javy oznacza to, że wartość tej zmiennej ma być zawsze przechowywana w głównej pamięci programu, a nie, jak w przypadku normalnych zmiennych- w lokalnym cache wątku.

Żeby lepiej zrozumieć zmienne volatile, rozbudujmy sobie nasz poprzedni „toaletowy” przykład. Okazuje się, że właściciel sklepu to straszny dusigrosz i chce mieć na bieżąco podgląd na to ile razy w toalecie została spłukana woda. Na szczęście nasz Pan Oszczędnicki ma na tyle taktu, że nie zamierza wchodzić do toalety i blokować jej tylko po to, aby sprawdzić licznik spłukań. Zainstalował sobie system, który co dany czas, przysyła mu maila z wartością licznika:

public class WaterCloset {
  private volatile int flushCounter = 0;
  public void flushWater(){
    System.out.println("Water flushes today: " + ++flushCounter);
  }
  public int getFlushCounter(){
    return flushCounter;
  } 
}

Klasę WaterCloset zmodyfikowaliśmy poprzez dodanie słowa kluczowego volatile do zmiennej flushCounter, oraz utworzenie dla tej zmiennej gettera.

Klasa Owner to wątek, który co 100ms odpytuje się o stan licznika, bez blokowania monitora obiektu wc:

public class Owner extends Thread{
  private WaterCloset wc;
  public Owner(WaterCloset wc){ 
    this.wc = wc;
  }
  public void run(){ 
    for(int i = 0; i < 15; i++){
      System.out.println(String.format("--> FLUSH COUNTER: %d", wc.getFlushCounter()));
      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
}

Na koniec dodajemy uruchomienie wątku Owner w klasie Shop:

public class Shop {
  public static void main(String[] args) {
    WaterCloset wc = new WaterCloset();
    
    Owner owner = new Owner(wc);
    owner.start();
    
    Worker w1 = new Worker("Johny", wc);
    Worker w2 = new Worker("George", wc);
    
    Customer c1 = new Customer("April", wc);
    Customer c2 = new Customer("Winter", wc);
    Customer c3 = new Customer("Tom", wc);
    
    w1.start();
    w2.start();
    c1.start();
    c2.start();
    c3.start();
  }

}

Oto jeden z możliwych wyników działania kodu:

Worker George is working
--> FLUSH COUNTER: 0
Worker Johny is working
Customer Tom is shopping
Customer Winter is shopping
Customer April is shopping
Customer Winter is going to WC
Customer Winter is inside WC
Water flushes today: 1
Customer Winter is leaving WC
Customer Tom is going to WC
Customer Tom is inside WC
Water flushes today: 2
Customer Tom is leaving WC
--> FLUSH COUNTER: 2
Worker Johny is going to WC
Worker Johny is inside WC
Water flushes today: 3
Worker Johny is leaving WC
--> FLUSH COUNTER: 3
--> FLUSH COUNTER: 3
--> FLUSH COUNTER: 3
--> FLUSH COUNTER: 3
Worker George is going to WC
Worker George is inside WC
Water flushes today: 4
Worker George is leaving WC
--> FLUSH COUNTER: 4
--> FLUSH COUNTER: 4
--> FLUSH COUNTER: 4
--> FLUSH COUNTER: 4
Customer April is going to WC
Customer April is inside WC
Water flushes today: 5
Customer April is leaving WC
--> FLUSH COUNTER: 5
--> FLUSH COUNTER: 5
--> FLUSH COUNTER: 5
--> FLUSH COUNTER: 5
--> FLUSH COUNTER: 5

Zawsze widzimy aktualny stan licznika, ale nie spowalniamy procesowania oczekiwaniem na zablokowanie monitora.

W takiej sytuacji pojawia się pokusa, aby wszystkie zmienne deklarować jako volatile. W końcu to dodatkowo zabezpieczenie przed utratą spójności danych. Niestety nie ma nic za darmo. Zmienna volatile wymusza utworzenie tak zwanej relacji „happens-before”. Jest to temat oddzielnego artykułu, ale w skrócie chodzi o to, że Java, choć nie może zagwarantować dokładnej kolejności pewnych zdarzeń, to może zagwarantować, że jedno zdarzenie na pewno wydarzy się wcześniej niż drugie (np w tym przypadku, najpierw upewnij się, że zmienna ma aktualną wartość, a dopiero potem ją zwróć- a w między czasie mogą, ale nie muszą być inne operacje nie powiązane z tą zmienną). Utworzenie relacji „happens-before” wymusza na kompilatorze zrezygnowanie z niektórych optymalizacji, które standardowo wykonuje. Dlatego zbyt duża liczba zmiennych volatile spowolni nasz system bardziej niż stosowanie monitorów.

Obiekty immutable

Na koniec, mechanizm, którego głównym celem nie jest wspomaganie synchronizacji wątków, ale mimo to da się go świetnie wykorzystać do pomocy w zachowaniu spójności danych. Obiekty niezmienne (ang. immutable). Są to obiekty, których raz nadana wartość nie może zostać zmieniona. Przykładami klas takich obiektów są wszystkie tzw. wrappery: Long, Integer, Double, Float itd. Również, co nie dla wszystkich jest oczywiste, taką klasą jest String.

Tak, kiedy raz zainicjujemy obiekt klasy String, jego wartości nie da się już zmienić.

Moment, przecież mogę zrobić coś takiego:

String name = "Tadek";
name = "Wiesiek";

Owszem, jak najbardziej. Tylko, że w tym kodzie utworzyliśmy dwa obiekty klasy String. Jeden zawiera ciąg „Tadek”, a drugi „Wiesiek”, po prostu przypisaliśmy je do tej samej referencji. A ponieważ, w tym przypadku, do obiektu „Tadek” nie wskazuje już żadna referencja, to zostanie on usunięty przez Garbage Collector.

Wracając do tematu. Skoro nie da się zmienić wartości danego obiektu, to automatycznie mamy gwarancję, że ta dana zawsze będzie taka sama dla wszystkich wątków, które będą chciały ją odczytać. Nie potrzebujemy chronić dostępu do niej ani monitorami, ani słowem kluczowym volatile.

Jak samemu utworzyć obiekt immutable? Jest to nic innego jak zastosowanie słowa kluczowego final, którym poprzedzamy zmienną zawierającą nasze dane:

private final int IMMUTABLE_INT = 101;

Słowo kluczowe final będzie jeszcze opisywane w oddzielnym artykule.


Na koniec małe sprostowanie. Obiecałem, że w tym artykule napiszę więcej o metodach wait(), notify(), notifyAll() oraz podam przykład dla problemu producenta i konsumenta. Jednak ta tematyka bardziej podpada pod przedmiot kolejnego, ostatniego wymagania z rozdziału wątków. I tam właśnie zajmę się tym problemem.