Skip to content

7. Wątki wykonawcze

7.1. Wprowadzenie

Po uruchomieniu aplikacja działa w strumieniu wykonania zwanym wątkiem. Klasą modelującą thread jest klasa java.lang.Thread, której niektóre właściwości i metody przedstawiono poniżej:

currentThread()
zwraca wątek aktualnie wykonywany
setName()
ustawia nazwę wątku
getName()
nazwa wątku
isAlive()
wskazuje, czy wątek jest aktywny (true), czy nie (false)
start()
uruchamia wątek
run()
metoda wykonywana automatycznie po wykonaniu poprzedniej metody start
sleep(n)
zatrzymuje działanie wątku na n milisekund
join()
operacja blokująca – oczekuje na zakończenie wątku, aby przejść do następnej instrukcji

Najczęściej stosowane konstruktory to:

Thread()
tworzy odwołanie do zadania asynchronicznego. Zadanie to jest nadal nieaktywne. Utworzone zadanie musi posiadać metodę run: najczęściej będzie to klasa pochodna od Thread.
Thread(Runnable object)
To samo, ale to obiekt Runnable przekazany jako parametr implementuje metodę run.

Przyjrzyjmy się pierwszej aplikacji, która ilustruje istnienie głównego wątku wykonania, w którym wykonywana jest funkcja main klasy:

// wykorzystanie wątków

import java.io.*;
import java.util.*;

public class thread1{
    public static void main(String[] arg)throws Exception {
         // inicjalizacja bieżącego wątku
        Thread main=Thread.currentThread();
         // wyświetlanie
        System.out.println("Thread courant : " + main.getName());
         // zmiana nazwy
        main.setName("myMainThread");
         // sprawdzanie
        System.out.println("Thread courant : " + main.getName());

         // pętla nieskończona
        while(true){
        // pobieranie godziny
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
             // wyświetlanie
            System.out.println(main.getName() + " : " +H);
             // tymczasowe zatrzymanie
            Thread.sleep(1000);
        }//while
    }//main
}//klasa

Wyniki na ekranie:


Thread courant : main
Thread courant : myMainThread
myMainThread : 15:34:9
myMainThread : 15:34:10
myMainThread : 15:34:11
myMainThread : 15:34:12
Terminer le programme de commandes (O/N) ? o

Powyższy przykład ilustruje następujące kwestie:

  • funkcja main działa poprawnie w wątku
  • dostęp do właściwości tego wątku uzyskuje się za pomocą funkcji Thread.currentThread()
  • rolę metody sleep. W tym przypadku wątek wykonujący funkcję main regularnie przechodzi w stan uśpienia na 1 sekundę pomiędzy kolejnymi wyświetleniami.

7.2. Tworzenie wątków wykonawczych

Możliwe jest tworzenie aplikacji, w których fragmenty kodu są wykonywane „równocześnie” w różnych wątkach wykonawczych. Stwierdzenie, że thread są wykonywane równocześnie, jest często nadużyciem językowym. Jeśli komputer ma tylko jeden procesor, co nadal często ma miejsce, procesy thread dzielą ten procesor: każdy z nich ma do niego dostęp po kolei przez krótką chwilę (kilka milisekund). To właśnie stwarza iluzję równoległości wykonywania. Czas przydzielony procesowi thread zależy od różnych czynników, w tym od jego priorytetu, który ma wartość domyślną, ale może być również ustalony programowo. Gdy proces thread ma dostęp do procesora, zazwyczaj wykorzystuje go przez cały przydzielony mu czas. Może jednak zwolnić go przed upływem tego czasu:

  • przechodząc w stan oczekiwania na zdarzenie (wait, join)
  • przechodząc w stan uśpienia na określony czas (sleep)
  • Wątek T można utworzyć na różne sposoby
    • poprzez utworzenie klasy pochodnej od klasy Thread i przedefiniowanie metody run tej klasy
    • poprzez zaimplementowanie interfejsu Runnable w klasie i użycie konstruktora new Thread(Runnable). Runnable jest interfejsem, który definiuje tylko jedną metodę: public void run(). Argumentem powyższego konstruktora jest zatem dowolna instancja klasy implementującej tę metodę run.

W poniższym przykładzie wątki są tworzone za pomocą anonimowej klasy dziedziczącej po klasie Thread:

            // tworzymy wątek i
            tâches[i]=new Thread() {
          public void run() {
            affiche();
        }
      };//definiowanie zadań[i]

Metoda run ogranicza się tutaj do przekierowania do metody affiche.

  • Wykonanie wątku T jest uruchamiane przez metodę T.start(): metoda ta należy do klasy Thread i przeprowadza pewną liczbę operacji inicjalizacyjnych, a następnie automatycznie wywołuje metodę run wątku lub interfejsu Runnable. Program wykonujący instrukcję T.start() nie czeka na zakończenie zadania T: natychmiast przechodzi do następnej instrukcji. W ten sposób mamy dwa zadania wykonywane równolegle. Często muszą one mieć możliwość komunikowania się między sobą, aby wiedzieć, na jakim etapie znajduje się wspólna praca do wykonania. Jest to problem synchronizacji wątków.
  • Po uruchomieniu wątek thread działa autonomicznie. Zostanie zatrzymany, gdy funkcja run, którą wykonuje, zakończy swoją pracę.
  • Można poczekać na zakończenie wykonywania wątku T przez funkcję T.join(). Mamy tu do czynienia z instrukcją blokującą: program, który ją wykonuje, jest zablokowany do momentu zakończenia pracy zadania T. Jest to również sposób na synchronizację.

Przyjrzyjmy się następującemu programowi:

// korzystanie z wątków

import java.io.*;
import java.util.*;

public class thread2{
    public static void main(String[] arg) {
         // inicjowanie bieżącego wątku
        Thread main=Thread.currentThread();
         // nadanie nazwy bieżącemu wątku
        main.setName("myMainThread");
         // początek funkcji main
        System.out.println("début du thread " +main.getName());

         // tworzenie wątków wykonawczych
        Thread[] tâches=new Thread[5];
        for(int i=0;i<tâches.length;i++){
             // tworzenie wątku i
            tâches[i]=new Thread() {
          public void run() {
            affiche();
        }
      };//zadania[i]
             // ustalanie nazwy wątku
            tâches[i].setName(""+i);
             // uruchamianie wątku i
            tâches[i].start();
        }//for

         // koniec funkcji main
        System.out.println("fin du thread " +main.getName());
    }//Main

    public static void affiche() {
         // pobieranie godziny
    Calendar calendrier=Calendar.getInstance();
    String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
         // wyświetlenie informacji o rozpoczęciu wykonywania
        System.out.println("Début d'exécution de la méthode affiche dans le Thread " + 
        Thread.currentThread().getName()+ " : " + H);
         // przejście w stan uśpienia na 1 s
        try{
        Thread.sleep(1000);
    }catch (Exception ex){}
     // odczyt czasu
    calendrier=Calendar.getInstance();    
    H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
         // wyświetlenie zakończenia wykonywania
        System.out.println("Fin d'exécution de la méthode affiche dans le Thread " 
    +Thread.currentThread().getName()+ " : " + H);
    }// wyświetla
}//klasa

Wątek główny, który wykonuje funkcję main, tworzy 5 dodatkowych wątków odpowiedzialnych za wykonanie metody statycznej affiche. Wyniki są następujące:


début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 15:48:3
fin du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 1 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 2 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 3 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 4 : 15:48:3
Fin d'exécution de la méthode affiche dans le Thread 0 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 1 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 2 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 3 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 4 : 15:48:4

Wyniki te są bardzo pouczające:

  • przede wszystkim widać, że uruchomienie wątku nie powoduje blokady. Metoda main uruchomiła równolegle 5 wątków i zakończyła swoje działanie przed nimi. Operacja
             // uruchamia się wątek i
            tâches[i].start();

uruchamia wątek tâches[i], ale po wykonaniu tej czynności realizacja kodu przechodzi natychmiast do kolejnej instrukcji, nie czekając na zakończenie działania wątku.

  • Wszystkie utworzone wątki muszą wykonać metodę affiche. Kolejność wykonywania jest nieprzewidywalna. Nawet jeśli w tym przykładzie kolejność wykonywania wydaje się odpowiadać kolejności uruchamiania wątków, nie można z tego wyciągać ogólnych wniosków. System operacyjny dysponuje tutaj 6 wątkami i jednym procesorem. Rozdzieli on zasoby procesora między te 6 wątków zgodnie z własnymi regułami.
  • W wynikach widać efekt działania metody sleep. W tym przykładzie to wątek 0 jako pierwszy wykonuje metodę affiche. Wyświetlany jest komunikat o rozpoczęciu wykonywania, a następnie wątek ten wykonuje metodę sleep, która zawiesza go na 1 sekundę. W ten sposób traci on dostęp do procesora, który staje się dostępny dla innego wątku. Przykład pokazuje, że procesor ten przejmie wątek 1. Wątek 1 przejdzie tę samą ścieżkę, podobnie jak pozostałe wątki. Gdy upłynie sekunda zawieszenia wątku 0, jego wykonywanie może zostać wznowione. System przydziela mu procesor i może on zakończyć wykonywanie metody affiche.

Zmodyfikujmy nasz program, aby zakończyć metodę main za pomocą instrukcji:

         // koniec funkcji
        System.out.println("fin du thread " +main.getName());
     // zatrzymanie aplikacji
    System.exit(0);

Wynik wykonania nowego programu wygląda następująco:


début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 1 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 2 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 3 : 16:5:45
fin du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 4 : 16:5:45

Gdy tylko metoda main wykona instrukcję:

    System.exit(0);

zatrzymuje wszystkie wątki aplikacji, a nie tylko wątek main. Metoda main może chcieć poczekać na zakończenie działania wątków, które utworzyła, zanim sama się zakończy. Można to zrobić za pomocą metody join klasy Thread:

     // oczekiwanie na wszystkie wątki
        for(int i=0;i<tâches.length;i++){
            // oczekiwanie na wątek i
            tâches[i].join();
    }//for  

         // koniec funkcji main
        System.out.println("fin du thread " +main.getName());
     // zatrzymanie aplikacji
    System.exit(0);

Otrzymujemy wówczas następujące wyniki:

début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 1 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 2 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 3 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 4 : 16:11:9
Fin d'exécution de la méthode affiche dans le Thread 0 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 1 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 2 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 3 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 4 : 16:11:10
fin du thread myMainThread

7.3. Znaczenie wątków

Skoro już wykazaliśmy istnienie domyślnego wątku, który wykonuje metodę Main, i wiemy, jak tworzyć kolejne, zastanówmy się, jakie korzyści płyną z wątków i dlaczego je tutaj omawiamy. Istnieje pewien rodzaj aplikacji, które dobrze nadają się do wykorzystania wątków – są to aplikacje typu klient-serwer w Internecie. W takiej aplikacji serwer znajdujący się na komputerze S1 odpowiada na żądania klientów znajdujących się na zdalnych komputerach C1, C2, ..., Cn.

Image

Codziennie korzystamy z aplikacji internetowych działających zgodnie z tym schematem: serwisy internetowe, poczta elektroniczna, fora dyskusyjne, przesyłanie plików… Na powyższym schemacie serwer S1 musi obsługiwać klientów Ci jednocześnie. Jeśli weźmiemy za przykład serwer FTP (File Transfer Protocol), który dostarcza pliki swoim klientom, wiemy, że transfer pliku może czasami trwać kilka godzin. Oczywiście nie wchodzi w grę, aby jeden klient samodzielnie monopolizował serwer przez tak długi czas. Zazwyczaj serwer tworzy tyle wątków wykonawczych, ile jest klientów. Każdy wątek jest wówczas odpowiedzialny za obsługę konkretnego klienta. Ponieważ procesor jest cyklicznie dzielony między wszystkie aktywne wątki na maszynie, serwer poświęca nieco czasu każdemu klientowi, zapewniając w ten sposób równoczesność obsługi.

Image

7.4. Graficzny zegar

Rozważmy następującą aplikację, która wyświetla okno z zegarem i przyciskiem służącym do zatrzymania lub ponownego uruchomienia zegara:

Aby zegar działał, proces musi co sekundę aktualizować czas. Jednocześnie należy monitorować zdarzenia zachodzące w oknie: gdy użytkownik kliknie przycisk „Zatrzymaj”, zegar musi zostać zatrzymany. Mamy tu do czynienia z dwoma równoległymi i asynchronicznymi zadaniami: użytkownik może kliknąć w dowolnym momencie.

Rozważmy sytuację, w której zegar nie został jeszcze uruchomiony, a użytkownik kliknie przycisk „Uruchom”. Mamy tu do czynienia z klasycznym zdarzeniem i można by pomyśleć, że metoda wątku, w którym działa okno, może wówczas zająć się obsługą zegara. Jednak gdy wykonywana jest metoda aplikacji graficznej, jej wątek nie nasłuchuje już zdarzeń interfejsu graficznego. Zdarzenia te występują i są umieszczane w kolejce, aby zostały przetworzone przez aplikację po zakończeniu aktualnie wykonywanej metody. W naszym przykładzie z zegarem metoda będzie zawsze w trakcie wykonywania, ponieważ zatrzymać ją może jedynie kliknięcie przycisku „Zatrzymaj”. Jednak to zdarzenie zostanie przetworzone dopiero po zakończeniu wykonywania tej metody. W ten sposób wpadamy w błędne koło.

Rozwiązaniem tego problemu byłoby uruchomienie zadania do obsługi zegara w momencie kliknięcia przez użytkownika przycisku „Uruchom”, przy czym aplikacja mogłaby nadal nasłuchiwać zdarzeń występujących w oknie. Mielibyśmy wówczas dwa odrębne zadania wykonywane równolegle:

  • zarządzanie zegarem
  • nasłuchiwanie zdarzeń w oknie

Wróćmy do naszego zegaru graficznego:

nr
typ
nazwa
rola
1
JTextField (Editable=false)
txtHorloge
wyświetla godzinę
2
JButton
btnGoStop
zatrzymuje lub uruchamia zegar

Kod źródłowy aplikacji stworzonej przy użyciu JBuilder wygląda następująco:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;

public class interfaceHorloge extends JFrame {
  JPanel contentPane;
  JTextField txtHorloge = new JTextField();
  JButton btnGoStop = new JButton();

  // atrybuty instancji
  boolean finHorloge=true;

   //Tworzenie ramki
  public interfaceHorloge() {
    enableEvents(AWTEvent.WINDOW_EVENT_MASK);
    try {
      jbInit();
    }
    catch(Exception e) {
      e.printStackTrace();
    }
  }

  private void runHorloge(){
    // pętla trwa, dopóki nie otrzymamy polecenia zatrzymania
    while( ! finHorloge){
       // pobieramy godzinę
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
       // wyświetlamy ją w polu T
      txtHorloge.setText(H);
       // czekamy jedną sekundę
      try{
        Thread.sleep(1000);
      } catch (Exception e){
        // wyjście z błędem
        System.exit(1);
      }//try
    }// while
  }// runHorloge

   //Zainicjuj komponent
  private void jbInit() throws Exception  {
...................
  }

   //Zastąpiono, dzięki czemu możemy zakończyć działanie po zamknięciu okna
  protected void processWindowEvent(WindowEvent e) {
.............
  }

  void btnGoStop_actionPerformed(ActionEvent e) {
    // uruchamiamy/zatrzymujemy zegar
     // pobieramy etykietę przycisku
    String libellé=btnGoStop.getText();
    // uruchomić?
    if(libellé.equals("Lancer")){
      // tworzymy wątek, w którym będzie działał zegar
      Thread thHorloge=new Thread(){
        public void run(){
          runHorloge();
        }
      };//zakończenie wątku
       // zezwalamy na uruchomienie wątku
      finHorloge=false;
       // zmieniamy tekst przycisku
      btnGoStop.setText("Arrêter");
      // uruchamiamy wątek
      thHorloge.start();
      // koniec
      return;
    }//if
    // zatrzymaj
    if(libellé.equals("Arrêter")){
      // nakazujemy wątku zatrzymanie się
      finHorloge=true;
       // zmieniamy tekst przycisku
      btnGoStop.setText("Lancer");
       // koniec
      return;
    }//if
  }
} 

Gdy użytkownik kliknie przycisk „Uruchom”, tworzony jest wątek przy użyciu klasy anonimowej:

      Thread thHorloge=new Thread(){
        public void run(){
          runHorloge();
        }

Metoda run wątku odwołuje się do metody runHorloge aplikacji. Po wykonaniu tej operacji wątek zostaje uruchomiony:

      // uruchamiamy wątek
      thHorloge.start();

Następnie zostanie wykonana metoda runHorloge:

  private void runHorloge(){
    // pętla trwa, dopóki nie otrzymamy polecenia zatrzymania
    while( ! finHorloge){
       // pobieramy godzinę
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
       // wyświetla się ją w polu T
      txtHorloge.setText(H);
       // czekamy jedną sekundę
      try{
        Thread.sleep(1000);
      } catch (Exception e){
        // wyjście z błędem
        System.exit(1);
      }//spróbuj
    }// while
  }// runHorloge

Zasada działania tej metody jest następująca:

  1. wyświetla aktualną godzinę w polu tekstowym txtHorloge
  2. zatrzymuje się na 1 sekundę
  3. powtarza krok 1, po uprzednim sprawdzeniu wartości logicznej finHorloge, która zostanie ustawiona na „prawda”, gdy użytkownik kliknie przycisk Arrêter.

7.5. Aplet z zegarem

Przekształcamy poprzednią aplikację graficzną w aplet w zwykły sposób i tworzymy następujący dokument HTML appletHorloge.htm:

<html>
  <head>
    <title>Applet Horloge</title>
  </head>
  <body>
      <h2>Une applet horloge</h2>
    <applet
      code="appletHorloge.class"
      width="150"
      height="130"
    ></applet>
    </center>
  </body>
</html>

Gdy wczytujemy ten dokument bezpośrednio do IE, klikając go dwukrotnie, otrzymujemy następujący widok:

Image

W tym przykładzie wszystkie elementy niezbędne do działania apletu znajdują się w tym samym folderze:

E:\data\serge\Jbuilder\horloge\1>dir
13/06/2002  12:17                3 174 appletHorloge.class
13/06/2002  12:17                  658 appletHorloge$1.class
13/06/2002  12:17                  512 appletHorloge$2.class
13/06/2002  12:20                  245 appletHorloge.htm

Nasz aplet można ulepszyć. Stwierdziliśmy, że podczas ładowania apletu wykonywana jest metoda init, a następnie metoda start, o ile istnieje. Ponadto, gdy użytkownik opuszcza stronę, wykonywana jest metoda stop, o ile istnieje. Gdy powraca on na stronę z apletem, ponownie wywoływana jest metoda start. Gdy aplet wykorzystuje wątki animacji wizualnej, często stosuje się metody start i stop apletu do uruchamiania i zatrzymywania wątków. Nie ma bowiem sensu, aby wątek animacji wizualnej nadal działał w tle, gdy animacja jest ukryta.

Dodajemy zatem do naszego apletu następujące metody: start i stop:

  public void stop(){
    // strona jest ukryta
     // śledzenie
    System.out.println("Page stop");
     // strona jest ukryta – zatrzymujemy wątek
    finHorloge=true;
  }

  public void start(){
     // strona pojawia się ponownie
     // kontynuacja
    System.out.println("Page start");
     // w razie potrzeby uruchamia się nowy wątek zegara
    if(btnGoStop.getText().equals("Arrêter")){
      // zmieniamy treść etykiety
      btnGoStop.setText("Lancer");
       // i traktuje się to tak, jakby użytkownik na nią kliknął
      btnGoStop_actionPerformed(null);
    }//if
  }//start

Ponadto dodaliśmy monitorowanie w metodzie run wątku, aby sprawdzić, kiedy się uruchamia i zatrzymuje:

  private void runHorloge(){
    // śledzenie
    System.out.println("Thread horloge lancé");
    // pętla trwa, dopóki nie otrzymamy polecenia zatrzymania
    while( ! finHorloge){
       // pobieramy godzinę
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
       // wyświetlamy ją w polu T
      txtHorloge.setText(H);
       // czekamy sekundę
      try{
        Thread.sleep(1000);
      } catch (Exception e){
        // wyjście z błędem
        return;
      }//try
    }// while
    // kontynuacja
    System.out.println("Thread horloge terminé");
  }// runHorloge

Teraz uruchamiamy aplet za pomocą AppletViewer:

E:\data\serge\Jbuilder\horloge\1>appletviewer appletHorloge.htm
Page start    // aplet uruchomiony – strona wyświetlona
Thread horloge lancé    // wątek został uruchomiony zgodnie z oczekiwaniami
Page stop    // aplet zminimalizowany
Thread horloge terminé    // wątek został odpowiednio zatrzymany
Page start    // aplet ponownie wyświetlony
Thread horloge lancé    // wątek został ponownie uruchomiony
Thread horloge terminé    // naciśnięcie przycisku zatrzymania
Thread horloge lancé    // naciśnięcie przycisku uruchom
Page stop    // aplet w postaci ikony    e
Thread horloge terminé    // wątek został w związku z tym zatrzymany
Page start    // ponowne wyświetlenie apletu
Thread horloge lancé    // wątek ponownie uruchomiony w związku z tym

W przypadku AppletViewer zdarzenie start występuje, gdy okno AppletViewer jest widoczne, a zdarzenie stop – gdy okno to zostanie zminimalizowane. Powyższe wyniki pokazują, że gdy dokument HTML jest ukryty, wątek zostaje prawidłowo zatrzymany, jeśli był aktywny.

7.6. Synchronizacja zadań

W naszym poprzednim przykładzie występowały dwa zadania:

  • zadanie główne reprezentowane przez samą aplikację
  • zadanie odpowiedzialne za zegar

Koordynację między tymi dwoma zadaniami zapewniało zadanie główne, które ustawiało zmienną logiczną w celu zatrzymania wątku zegara. Teraz zajmiemy się problemem współbieżnego dostępu zadań do wspólnych zasobów, znanym również jako „współdzielenie zasobów”. Aby to zilustrować, najpierw przeanalizujemy przykład.

7.6.1. Niesynchronizowane liczenie

Rozważmy następujący interfejs graficzny:

Image

nr
typ
nazwa
rola
1
JTextField
txtAGénérer
określa liczbę wątków do wygenerowania
2
JTextfield
(nieedytowalne)
txtGénéres
wskazuje liczbę wygenerowanych wątków
3
JTextField
(nieedytowalne)
txtStatus
zawiera informacje o napotkanych błędach oraz o samej aplikacji
4
JButton
btnGénérer
uruchamia generowanie wątków

Aplikacja działa w następujący sposób:

  • użytkownik określa liczbę wątków do wygenerowania w polu 1
  • uruchamia generowanie tych wątków za pomocą przycisku 4
  • wątki odczytują wartość z pola 2, zwiększają ją o 1 i wyświetlają nową wartość. Początkowo pole to zawiera wartość 0.

Wygenerowane wątki współdzielą jeden zasób: wartość pola 2. Chcemy tutaj pokazać problemy, które pojawiają się w takiej sytuacji. Oto przykładowe wykonanie:

Image

Widać, że zlecono wygenerowanie 1000 wątków, a zliczono tylko 7. Kod aplikacji wygląda następująco:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class interfaceSynchro extends JFrame {
  JPanel contentPane;
  JLabel jLabel1 = new JLabel();
  JTextField txtAGénérer = new JTextField();
  JButton btnGénérer = new JButton();
  JTextField txtStatus = new JTextField();
  JTextField txtGénérés = new JTextField();
  JLabel jLabel2 = new JLabel();

   // zmienne instancji
    Thread[] tâches=null;   // wątki
    int[] compteurs=null;   // liczniki

   //Utworzenie ramki
  public interfaceSynchro() {
..........
  }

   //Zainicjowanie komponentu
  private void jbInit() throws Exception  {
......................
  }

   //Zastąpiono, dzięki czemu możemy zakończyć działanie po zamknięciu okna
  protected void processWindowEvent(WindowEvent e) {
..................
  }

  void btnGénérer_actionPerformed(ActionEvent e) {
     //generowanie wątków

     // odczyt liczby wątków do wygenerowania
    int nbThreads=0;
    try{
      // odczyt pola zawierającego liczbę wątków
      nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
      // wartość dodatnia >
      if(nbThreads<=0) throw new Exception();
    }catch(Exception ex){
       //błąd
      txtStatus.setText("Nombre invalide");
       // próbujemy ponownie
      txtAGénérer.requestFocus();
      return;
    }//przechwycenie

     // na początku nie wygenerowano żadnych wątków
    txtGénérés.setText("0");  // licznik zadań wynosi 0

     // generujemy i uruchamiamy wątki
    tâches=new Thread[nbThreads];
    compteurs=new int[nbThreads];
    for(int i=0;i<tâches.length;i++){
      // tworzymy wątek i
      tâches[i]=new Thread() {
          public void run() {
          incrémente();
        }
      };//wątek i
       // określono jego nazwę
      tâches[i].setName(""+i);
      // uruchamia się jego wykonanie
      tâches[i].start();
    }//for
  }//generowanie

   // inkrementuje
  private void incrémente(){
     // odzyskuje się numer wątku
    int iThread=0;
    try{
      iThread=Integer.parseInt(Thread.currentThread().getName());
    }catch(Exception ex){}
     // odczyt wartości licznika zadań
    try{
      compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
    } catch (Exception e){}
     // inkrementujemy go
    compteurs[iThread]++;

     // czekamy 100 milisekund – wątek straci wtedy dostęp do procesora
    try{
      Thread.sleep(100);
    } catch (Exception e){
      System.exit(0);
    }

     // wyświetla się nowa wartość licznika
    txtGénérés.setText("");
    txtGénérés.setText(""+compteurs[iThread]);
    // monitorowanie
    System.out.println("Thread " + iThread + " : " + compteurs[iThread]);
  }// inkrementuje

}// klasa

Przyjrzyjmy się bliżej kodowi:

  • w oknie zadeklarowano dwie zmienne instancji:
   // zmienne instancji
    Thread[] tâches=null;   // wątki
    int[] compteurs=null;   // liczniki

Tablica tâches będzie tablicą wygenerowanych wątków. Tablica compteurs będzie powiązana z tablicą tâches. Każde zadanie będzie miało własny licznik służący do pobierania wartości pola txtGénérés z interfejsu graficznego.

  • Po kliknięciu przycisku Générer wykonywana jest metoda btnGénérer_actionPerformed.
  • Metoda ta rozpoczyna się od pobrania liczby wątków, które mają zostać wygenerowane. W razie potrzeby zgłaszany jest błąd, jeśli liczba ta jest nieprawidłowa. Następnie generuje żądane wątki, zapisując ich odwołania w tablicy i przypisując każdemu z nich numer. Metoda run dotycząca wygenerowanych wątków odwołuje się do metody incrémente tej klasy. Wszystkie wątki są uruchamiane (start). Tworzona jest również tablica liczników powiązanych z wątkami.
  • metoda incrémente:
  • odczytuje aktualną wartość pola txtGénérés i zapisuje ją w liczniku należącym do aktualnie uruchomionego wątku
  • zatrzymuje się na 100 ms, aby celowo zwolnić procesor
  • wyświetla nową wartość w polu txtGénérés

Wyjaśnijmy teraz, dlaczego zliczanie wątków jest nieprawidłowe. Załóżmy, że mamy do wygenerowania 2 wątki. Działają one w nieprzewidywalnej kolejności. Jeden z nich przechodzi jako pierwszy i odczytuje wartość 0 z licznika. Następnie zmienia ją na 1, ale nie zapisuje tej wartości w oknie: celowo przerywa działanie na 100 ms. W ten sposób traci dostęp do procesora, który zostaje przekazany innemu wątku. Ten działa tak samo jak poprzedni: odczytuje licznik z okna i pobiera wartość 0, która nadal tam się znajduje. Zmienia wartość licznika na 1 i, podobnie jak poprzedni wątek, przerywa działanie na 100 ms. Procesor zostaje wówczas ponownie przydzielony pierwszemu wątkowi: ten zapisuje wartość 1 w liczniku okna i kończy działanie. Procesor jest teraz przydzielony drugiemu wątkowi, który również zapisuje 1. Otrzymujemy nieprawidłowy wynik.

Skąd bierze się ten problem? Drugi wątek odczytał nieprawidłową wartość, ponieważ pierwszy został przerwany przed zakończeniem swojej pracy, polegającej na zaktualizowaniu licznika w oknie. Prowadzi nas to do pojęcia zasobu krytycznego i sekcji krytycznej programu:

  • zasób krytyczny to zasób, który w danym momencie może być posiadany tylko przez jeden wątek. W tym przypadku zasobem krytycznym jest licznik 2 w oknie.
  • Sekcja krytyczna programu to sekwencja instrukcji w strumieniu wykonania wątku, podczas której wątek uzyskuje dostęp do zasobu krytycznego. Należy zapewnić, aby w trakcie tej sekcji krytycznej tylko ten wątek miał dostęp do zasobu.

7.6.2. Zsynchronizowane liczenie za pomocą metody

W poprzednim przykładzie każdy wątek wykonywał metodę incrémente okna. Metoda incrémente została zadeklarowana w następujący sposób:

    private void incremente()

Teraz zadeklarujemy ją inaczej:

   // inkrementuje
  private synchronized void incrémente(){

Słowo kluczowe synchronized oznacza, że w danym momencie tylko jeden wątek może wykonywać metodę *incrémente*. Rozważmy następujące notacje:

  • obiekt okna F, który tworzy wątki w metodzie btnGénérer_actionPerformed
  • dwa wątki T1 i T2 utworzone przez F

Oba wątki są tworzone przez F, a następnie uruchamiane. Oba wykonają zatem metodę F.run. Załóżmy, że T1 dotrze jako pierwszy. Wykonuje on metodę F.run, a następnie metodę F.incremente, która jest metodą zsynchronizowaną. Odczytuje wartość 0 licznika, zwiększa ją o 1, a następnie zatrzymuje się na 100 ms. Następnie procesor zostaje przekazany do wątku T2, który z kolei wykonuje metodę F.run, a następnie F.incremente. W tym momencie następuje zablokowanie, ponieważ wątek T1 właśnie wykonuje funkcję F.incremente, a słowo kluczowe synchronized gwarantuje, że tylko jeden wątek na raz może wykonywać funkcję F.incremente. W ten sposób T2 z kolei traci dostęp do procesora, nie zdążywszy odczytać wartości licznika. Po upływie 100 ms T1 odzyskuje dostęp do procesora, wyświetla wartość 1 licznika, opuszcza F.incremente, a następnie F.run i kończy działanie. Następnie T2 odzyskuje procesor i tym razem może wykonać F.incremente, ponieważ T1 nie wykonuje już tej metody. Następnie T2 odczytuje wartość 1 licznika, zwiększa ją o 1 i zatrzymuje się na 100 ms. Po upływie 100 ms przejmuje procesor, wyświetla wartość 2 licznika i również kończy działanie. Tym razem uzyskana wartość jest poprawna. Oto przetestowany przykład:

Image

7.6.3. Zliczanie zsynchronizowane przez obiekt

W poprzednim przykładzie dostęp do licznika txtGénérés został zsynchronizowany za pomocą metody. Jeśli okno tworzące wątki nazywa się F, można również powiedzieć, że metoda F.incremente stanowi zasób, z którego w danym momencie powinien korzystać tylko jeden wątek. Jest to zatem zasób krytyczny. Zsynchronizowany dostęp do tego zasobu został zapewniony przez słowo kluczowe synchronized:

    private synchronized void incrémente()

Można by również powiedzieć, że zasobem krytycznym jest sam obiekt F. Jest to bardziej restrykcyjne niż w przypadku, gdy zasobem krytycznym jest F.incremente. W tym ostatnim przypadku, jeśli wątek T1 wykonuje metodę F.incremente, wątek T2 nie będzie mógł wykonać F.incremente, ale będzie mógł wykonać inną metodę obiektu F, niezależnie od tego, czy jest ona zsynchronizowana, czy nie. W przypadku, gdy sam obiekt F jest zasobem krytycznym, gdy wątek T1 wykonuje zsynchronizowaną sekcję tego obiektu, wszystkie inne zsynchronizowane sekcje obiektu stają się niedostępne dla pozostałych wątków. Tak więc, jeśli wątek T1 wykonuje zsynchronizowaną metodę F.incremente, wątek T2 nie będzie mógł wykonać nie tylko metody F.incremente, ale również żadnej innej zsynchronizowanej sekcji obiektu F, nawet jeśli żaden wątek z niej nie korzysta. Jest to zatem metoda bardziej restrykcyjna.

Załóżmy zatem, że okno staje się zasobem krytycznym. Napiszemy wówczas:

   // inkrement
  private void incrémente(){
     // sekcja krytyczna
    synchronized(this){
       // odczytujemy numer wątku
      int iThread=0;
      try{
        iThread=Integer.parseInt(Thread.currentThread().getName());
      }catch(Exception ex){}
       // odczyt wartości licznika zadań
      try{
        compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
      } catch (Exception e){}
       // inkrementujemy go
      compteurs[iThread]++;

       // czekamy 100 milisekund – wątek straci wtedy dostęp do procesora
      try{
        Thread.sleep(100);
      } catch (Exception e){
        System.exit(0);
      }

       // wyświetla się nowa wartość licznika
      txtGénérés.setText("");
      txtGénérés.setText(""+compteurs[iThread]);
    }//synchronized
  }// inkrementuje

Wszystkie wątki wykorzystują okno this do synchronizacji. Podczas wykonywania uzyskuje się te same poprawne wyniki, co poprzednio. W rzeczywistości można synchronizować się z dowolnym obiektem znanym wszystkim wątkom. Oto na przykład inna metoda, która daje te same wyniki:

   // zmienne instancji
    Thread[] tâches=null;   // wątki
    int[] compteurs=null;   // liczniki
    Object synchro=new Object(); // obiekt synchronizacji wątków

   // inkrementuje
  private void incrémente(){
     // sekcja krytyczna
    synchronized(synchro){
..............
    }//zsynchronizowane
  }// inkrement

Okno tworzy obiekt typu Object, który posłuży do synchronizacji wątków. Ta metoda jest lepsza od tej, która synchronizuje się z obiektem this, ponieważ jest mniej restrykcyjna. W tym przypadku, jeśli wątek T1 znajduje się w sekcji zsynchronizowanej obiektu incrémente, awątek T2 chce wykonać inną sekcję zsynchronizowaną tego samego obiektu „this”, ale zsynchronizowaną przez obiekt inny niż synchro, będzie mógł to zrobić.

7.6.4. Synchronizacja zdarzeniowa

Tym razem używamy zmiennej logicznej peutPasser, aby poinformować wątek, czy może wejść do sekcji krytycznej, czy nie. Kod bez synchronizacji mógłby wyglądać następująco:

while(! peutPasser);        // czekamy, aż peutPasser przyjmie wartość true
peutPasser=false;            // żaden inny wątek nie może przejść
section critique;            // tutaj wątek jest sam
peutPasser=true;            // inny wątek może przejść do sekcji krytycznej

Pierwsza instrukcja, w której wątek krąży w pętli, czekając, aż peutPasser przyjmie wartość true, jest nieefektywna: wątek niepotrzebnie zajmuje procesor. Mówimy tu o oczekiwaniu aktywnym. Kod można ulepszyć w następujący sposób:

while(! peutPasser){        // czekamy, aż peutPasser przyjmie wartość „prawda”
   Thread.sleep(100);    // zatrzymanie na 100 ms
}
peutPasser=false;            // żaden inny wątek nie powinien przejść
section critique;            // tutaj wątek jest sam
peutPasser=true;            // inny wątek może przejść do sekcji krytycznej

Pętla oczekiwania jest tutaj lepsza: jeśli wątek nie może przejść, przechodzi w stan uśpienia na 100 ms, a następnie ponownie sprawdza, czy może przejść, czy nie. W międzyczasie procesor zostanie przydzielony innym wątkom systemu.

Obie te metody są w rzeczywistości nieprawidłowe: nie zapobiegają one jednoczesnemu wkroczeniu dwóch wątków do sekcji krytycznej. Załóżmy, że wątek T1 wykrywa, że wartość peutPasser jest prawdziwa. Przejdzie wówczas do następnej instrukcji, gdzie ponownie ustawi peutPasser na wartość fałsz, aby zablokować pozostałe wątki. Problem polega jednak na tym, że w tym momencie może zostać przerwany – albo dlatego, że wyczerpał się jego czas procesora, albo dlatego, że zadanie o wyższym priorytecie zażądało procesora, albo z innego powodu. W rezultacie traci dostęp do procesora. Odzyska go nieco później. W międzyczasie dostęp do procesora otrzymają inne zadania, w tym być może wątek T2, który krąży w pętli, czekając, aż peutPasser przyjmie wartość „prawda”. On również stwierdzi, że peutPasser ma wartość „true” (pierwszy wątek nie zdążył ustawić go na „false”) i również przejdzie do sekcji krytycznej. A tego nie powinno się było zrobić.

Sekwencja


while(! peutPasser){            // czekamy, aż peutPasser przyjmie wartość „prawda”
   try{
        Thread.sleep(100);    // przerwa na 100 ms
    } catch (Exception e) {}
}// while
peutPasser=false;                // żaden inny wątek nie może przejść

jest sekwencją krytyczną, którą należy zabezpieczyć za pomocą synchronizacji. Wzorując się na poprzednim przykładzie, można zapisać:


    synchronized(synchro){
        while(! peutPasser){            // czekamy, aż peutPasser przyjmie wartość true
            try{
                Thread.sleep(100);    // przerwa na 100 ms
            } catch (Exception e) {}
        }//while
        peutPasser=false;                // żaden inny wątek nie może przejść
    }// synchronized
    section critique;                    // tutaj wątek jest sam
    peutPasser=true;                    // inny wątek może przejść przez sekcję krytyczną

Ten przykład działa poprawnie. Można go ulepszyć, unikając półaktywnego oczekiwania wątku, gdy regularnie monitoruje on wartość zmiennej logicznej peutPasser. Zamiast budzić się regularnie co 100 ms w celu sprawdzenia stanu zmiennej peutPasser, wątek może przejść w stan uśpienia i poprosić o wybudzenie go, gdy wartość zmiennej peutPasser będzie prawdziwa. Zapisuje się to w następujący sposób:


synchronized(synchro){
    if (! peutPasser) {
        try{
            synchro.wait();            // jeśli nie można przejść, to czekamy
        } catch (Exception e){
            
        }
    }
    peutPasser=false;            // żaden inny wątek nie może przejść
}// synchronized

Operacja synchro.wait() może być wykonana wyłącznie przez wątek, który w danym momencie jest „właścicielem” obiektu synchro. W tym przypadku jest to sekwencja:


synchronized(synchro){

}// synchronized

która gwarantuje, że wątek jest właścicielem obiektu synchro. Poprzez operację synchro.wait() wątek zwalnia blokadę synchronizacyjną. Dlaczego tak się dzieje? Zazwyczaj dlatego, że brakuje mu zasobów niezbędnych do kontynuowania pracy. Zamiast więc blokować inne wątki oczekujące na zasób synchro, oddaje go i przechodzi w stan oczekiwania na brakujący zasób. W naszym przykładzie czeka, aż wartość logiczna peutPasser przyjmie wartość „prawda”. W jaki sposób zostanie powiadomiony o tym zdarzeniu? W następujący sposób:


synchronized(synchro){
    if (! peutPasser) {
        try{
            synchro.wait();            // jeśli nie można przejść, to czekamy
        } catch (Exception e){
            
        }
    }
    peutPasser=false;            // żaden inny wątek nie może przejść
}// synchronized
section critique...
synchronized(synchro){
      synchro.notify();
    }

Rozważmy pierwszy wątek, który przechodzi przez blokadę synchronizacyjną. Nazwijmy go T1. Załóżmy, że stwierdza on, iż wartość logiczna peutPasser ma wartość „prawda”, ponieważ jest pierwszym wątkiem. Zmienia ją zatem na „fałsz”. Następnie opuszcza sekcję krytyczną zablokowaną przez obiekt synchro. Inny wątek będzie mógł wówczas wejść do sekcji krytycznej, aby sprawdzić wartość obiektu peutPasser. Stwierdzi, że jest on fałszywy, i w związku z tym przejdzie w stan oczekiwania na zdarzenie (wait). Czyniąc to, przekazuje własność obiektu synchro. Inny wątek może wówczas wejść do sekcji krytycznej: on również przejdzie w stan oczekiwania, ponieważ wartość peutPasser jest fałszywa. Możemy więc mieć kilka wątków oczekujących na zdarzenie dotyczące obiektu synchro.

Wróćmy do wątku T1, który otrzymał dostęp. Wykonuje on sekcję krytyczną, a następnie sygnalizuje, że inny wątek może teraz uzyskać dostęp. Czyni to za pomocą sekwencji:


synchronized(synchro){
      synchro.notify();
    }

Najpierw musi odzyskać kontrolę nad obiektem synchro za pomocą instrukcji synchronized. Nie powinno to stanowić problemu, ponieważ konkuruje on z wątkami, które – jeśli chwilowo uzyskają obiekt synchro – muszą go zwolnić za pomocą instrukcji wait, ponieważ stwierdzą, że peutPasser jest nieprawidłowy. Zatem nasz wątek T1 w końcu uzyska własność obiektu synchro. Gdy to zrobi, za pomocą operacji synchro.notify sygnalizuje, że jeden z wątków zablokowanych przez synchro.wait musi zostać wybudzony. Następnie ponownie rezygnuje z własności obiektu synchro, która zostanie wówczas przekazana jednemu z oczekujących wątków. Wątek ten kontynuuje wykonywanie od instrukcji następującej po wait, która spowodowała jego zawieszenie. Następnie sam wykona sekcję krytyczną i uruchomi synchro.notify, aby zwolnić kolejny wątek. I tak dalej.

Przyjrzyjmy się temu sposobowi działania na przykładzie już omówionego liczenia.

  void btnGénérer_actionPerformed(ActionEvent e) {
     //generowanie wątków

     // odczyt liczby wątków do wygenerowania
    int nbThreads=0;
    try{
      // odczyt pola zawierającego liczbę wątków
      nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
      // dodatni >
      if(nbThreads<=0) throw new Exception();
    }catch(Exception ex){
       //błąd
      txtStatus.setText("Nombre invalide");
       // próbujemy ponownie
      txtAGénérer.requestFocus();
      return;
    }//przechwycenie

     // RAZ licznik zadań
    txtGénérés.setText("0");  // licznik zadań na 0
    // pierwszy wątek może przejść
    peutPasser=true;
     // generujemy i uruchamiamy wątki
    tâches=new Thread[nbThreads];
    compteurs=new int[nbThreads];
    for(int i=0;i<tâches.length;i++){
      // tworzy się wątek i
      tâches[i]=new Thread() {
          public void run() {
          synchronise();
        }
      };//wątek i
       // definiuje się jego nazwę
      tâches[i].setName(""+i);
      // uruchamia się jego wykonanie
      tâches[i].start();
    }//for
  }//generowanie

Obecnie wątki nie wykonują już metody incrémente, lecz następującą metodę synchronise:

   // etap synchronizacji wątków
  public void synchronise(){
     // żądanie dostępu do sekcji krytycznej
    synchronized(synchro){
      try{
        // czy można przejść?
        if(! peutPasser){
          System.out.println(Thread.currentThread().getName()+ " en attente");
          synchro.wait();
        }
         // przechodzimy – blokujemy dostęp innym wątkom
        peutPasser=false;
      } catch(Exception e){
        txtStatus.setText(""+e);
                    return;
      }//try
    }// synchronized

    // sekcja krytyczna
    System.out.println(Thread.currentThread().getName()+ " passé");
    incrémente();

     // skończyliśmy – zwalniamy ewentualny wątek zablokowany przy wejściu do sekcji krytycznej
    peutPasser=true;
    System.out.println(Thread.currentThread().getName()+ " terminé");
    synchronized(synchro){
      synchro.notify();
    }// synchronized
  } // synchronizuje

Metoda synchronise służy do przetwarzania wątków pojedynczo. W tym celu wykorzystuje zmienną synchronizacyjną synchro. Metoda incrémente nie jest już chroniona przez słowo kluczowe synchronized:

   // inkrementuje
  private void incrémente(){
     // pobieramy numer wątku
    int iThread=0;
    try{
      iThread=Integer.parseInt(Thread.currentThread().getName());
    }catch(Exception ex){}
     // odczyt wartości licznika zadań
    try{
      compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
    } catch (Exception e){}
     // inkrementuje się go
    compteurs[iThread]++;
     // czekamy 100 milisekund – wątek straci wtedy dostęp do procesora
    try{
      Thread.sleep(100);
    } catch (Exception e){
      System.exit(0);
    }
     // wyświetla się nowa wartość licznika
    txtGénérés.setText("");
    txtGénérés.setText(""+compteurs[iThread]);
  }// inkrementowanie

Dla 5 wątków uzyskano następujące wyniki:

0 passé
1 en attente
2 en attente
3 en attente
4 en attente
0 terminé
1 passé
1 terminé
2 passé
2 terminé
3 passé
3 terminé
4 passé
4 terminé

Niech T0 do T4 to 5 wątków wygenerowanych przez aplikację. T0 jako pierwszy przejmuje blokadę synchro i stwierdza, że peutPasser ma wartość true. Ustawia peutPasser na fałsz i przechodzi dalej: taki jest sens pierwszej wiadomości passé. Najprawdopodobniej kontynuuje i wykonuje sekcję krytyczną, a w szczególności metodę incrémente. W tej sekcji program przechodzi w stan uśpienia na 100 ms (sleep). W ten sposób zwalnia procesor. Procesor zostaje przydzielony innemu wątku, wątku T1, który następnie uzyskuje własność obiektu synchro. Następny wątek (wait) stwierdza, że nie może przejść dalej i przechodzi w stan oczekiwania. Zwalnia wówczas własność obiektu synchro oraz procesor. Procesor zostaje przydzielony wątkowi T2, który spotyka ten sam los. W ciągu 100 ms przestoju wątku T0 wątki od T1 do T4 są zatem zawieszone. Takie jest znaczenie czterech komunikatów „w stanie oczekiwania”. Po upływie 100 ms proces T0 odzyskuje procesor i kończy swoją pracę: to właśnie oznacza komunikat „0 terminé”. Następnie zwalnia jeden z zablokowanych wątków i kończy działanie. Uwolniony procesor zostaje następnie przydzielony do dostępnego wątku: tego, który właśnie został uwolniony. W tym przypadku jest to T1. Wątek T1 wchodzi wówczas do sekcji krytycznej: to właśnie oznacza komunikat „1 passé”. Wykonuje on swoje zadanie, a następnie sam zatrzymuje się na 100 ms. Procesor jest wówczas dostępny dla innego wątku, ale wszystkie wątki oczekują na zdarzenie: żaden z nich nie może przejąć procesora. Po upływie 100 ms wątek T1 odzyskuje procesor i kończy działanie: takie jest znaczenie komunikatu „1 terminé”. Wątki od T1 do T4 będą zachowywać się tak samo jak T1: takie jest znaczenie trzech serii komunikatów: „przekroczono”, „zakończono”.