3.4. Pavedieni

Ievads

Mērķi

  • Definēt pavediena jēdzienu
  • Izveidot vairākus pavedienus Javas tehnoloģiju programmā, atsevišķā pavediena vadības un datu kontrole
  • Drošas un platformneatkarīgas pavedienu programmēšanas pamati
  • Aprakstīt grūtības, kuras var rasties vairākiem pavedieniem izmantojot tos pašus datus
  • Lietot wait un notify pavedienu komunikācijai
  • Lietot synchronized, lai aizsargātu datus kritiskajās sekcijās
  • Izskaidrot, kāpēc suspend, resume un stop metodes ir novecinātas (deprecated) Java 2 SDK

    Pavedienu motivācija:

  • Kā panākt, lai programmas varētu nodarboties ar vairākiem uzdevumiem vienlaicīgi?

Pavedienu jēdziens

Pavediens ir virtuāls CPU process; to raksturo 3 sastāvdaļas:

  • Procesors
  • Kods
  • Dati

Atšķirībā no reāliem procesiem, kam ir katram pašam savas procesora taktis, kods un dati, pavedienam pieder vienīgi savas procesora taktis, bet vispārīgajā gadījumā ir jādalās ar citiem pavedieniem gan ar kopīgu kodu, gan arī ar datiem.

Pavediena izveidošana - piemērs

1  public class ThreadTester {
2    public static void main(String args[]) {
3      HelloRunner r = new HelloRunner();
4      Thread t = new Thread(r);
5      t.start();
6    }
7  }
8  
9  class HelloRunner implements Runnable {
10    int i;
11  
12    public void run() {
13      i = 0;
14  
15      while (true) {
16        System.out.println("Hello " + i++);
17        if ( i == 50 ) {
18          break;
19        }
20      }
21    }
22  }

Pavediena izveidošana - pārskats

Daudzpavedienu (multithreaded) programmēšana

  • Vairāki pavedieni var izmantot to pašu Runnable instanci
  • Šajā gadījumā pavedieniem ir kopīgs kods un dati

Piemērs:

Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
Pavedienam vienmēr ir pašam savs izpildes steks un CPU cikli

Pavediena uzsākšana

  • start() metodes lietošana
  • Pavediens nonāk izpildāmā (runnable) stāvoklī

Pavedienu plānošana (scheduling)

Vienkāršota pavediena stāvokļu diagramma
1  public class Runner implements Runnable {
2    public void run() {
3      while (true) {
4        // veic kaut ko interesantu
5        :
6        // palaiž citus pavedienus
7        try {
8          Thread.sleep(10);
9        } catch (InterruptedException e) {
10          // ja šī pavediena miegu iztraucēja
11          // cits pavediens
12        }
13      }
14    }
15  }

Pavediena beigšana

1  public class Runner implements Runnable {
2    private boolean timeToQuit=false;
3        
4    public void run() {
5      while ( ! timeToQuit ) {
6        ...
7      }
8      // veic resursu atbrīvošanu pirms iziet no run()
9    }
10  
11    public void stopRunning() {
12      timeToQuit=true;
13    }
14  }
1  public class ThreadController {
2    private Runner r = new Runner();
3    private Thread t = new Thread(r);
4     
5    public void startThread() {
6      t.start();
7    }
8  
9    public void stopThread() {
10      // uzstāda karodziņu konkrētai Runner instancei
11      r.stopRunning();
12    }
13  }

Pavedienu statusa noskaidrošana un vadība

  • Pavediena statusa pārbaudīšana notiek ar metodi isAlive()
  • Pavedienu prioritāti apskatās un uzstāda ar metodēm getPriority(), setPriority()
  • Pavedienu darbības pauzēšana ar metodi Thread.sleep()
  • Cita pavediena beigu sagaidīšana - join()
  • Piedāvājums CPU izmantot citiem pavedieniem, kuri ir gatavi - Thread.yield()

Metode join

1  public static void main(String[] args) {
2    Thread t = new Thread(new Runner());
3    t.start();
4    ...
5    // Dara kaut ko paralēli ar citu pavedienu
6    ...
7    // Pagaida, kamēr otrs pavediens beigs
8    try {
9      t.join();
10    } catch (InterruptedException e) {
11      // Ja cits pavediens mūs pārtrauca
12    }
13    ...
14    // Turpina viens pats
15    ...
16  }

Citi pavedienu radīšanas veidi

1  public class MyThread extends Thread {
2    public void run() {
3      while (running) {
4        // dara kaut ko saturīgu
5        try {
6          sleep(100);
7        } catch (InterruptedException e) {
8          // Ja cits pavediens pārtrauca
9        }
10      }
11    }
12  
13    public static void main(String args[]) {
14      Thread t = new MyThread();
15      t.start();
16    }
17  }

Kurā veidā radīt pavedienus?

Implementēt Runnable interfeisu. Priekšrocības:

  • Labāks OO dizains
  • Var būt nepieciešams vienkāršās mantošanas dēļ
  • Koda viendabīgums

Mantošana no Thread:

  • Kods izskatās vienkāršāks

synchronized atslēgvārda lietošana

1  public class MyStack {
2    int idx = 0;
3    char [] data = new char[6];
4  
5    public void push(char c) {
6      data[idx] = c;
7      idx++;
8    }
9  
10    public char pop() {
11      idx--;
12      return data[idx];
13    }
14  }

Objekta "lock" karodziņš

  • Katram Javas objektam piemīt karodziņš ("lock" karodziņš)
  • synchronized ļauj iedarboties uz šo karodziņu
Iekapsulēts objekts ar 'lock' karodziņu

Objekta "lock" karodziņš

Objekts sinhronizācijas blokā

Objekta "lock" karodziņš

Pavediens, kurš gaida uz objektu

"lock" karodziņa atbrīvošana

  • To atbrīvo, ja pavediens iziet no synchronized koda bloka
  • To atbrīvo arī, ja synchronized koda blokā izpildās break, return vai rodas izņēmums

synchronized - kopsavilkums

  • Katrai pieejai jūtīgiem koplietošanas datiem jābūt sinhronizētai
  • Jūtīgos datus, kurus aizsargā synchronized jādeklarē ar private.

synchronized - kopsavilkums

Sekojoši koda fragmenti ir ekvivalenti:

public void push(char c) {
  synchronized(this) {
    :
  }
}

un

public synchronized void push(char c) {
  :
}

Pavediena stāvokļu diagramma, kopā ar synchronized

Pavediena stāvokļu diagramma ar bloķēšanos uz sinhronizētu objektu

Strupceļš (deadlock)

  • Strupceļs rodas, ja divi pavedieni savstarpēji gaida uz otra pavediena uzstādīto "lock" jeb atslēgu

No strupceļa var izvairīties sekojoši:

  • Noteikt globālu kārtību, kādā jāiegūst atslēgas
  • Katrs pavediens atbrīvo atslēgas pretējā secībā, nekā tās rezervētas

Pavedienu mijiedarbība - wait un notify

  • Scenārijs: taksometra šofera un taksometra pasažiera sinhronizācija - kā noskaidrot, kad pasažieris sasniedzis savu mērķi
  • Atrisinājums: Pasažieris pasaka brauciena mērķi un "atslēdzas" ar wait(). Kad taksometra šoferis nonācis galā, tas "pamodina" pasažieri ar notify()

Alternatīvie risinājumi, ja šādu mijiedarbības funkciju nav, parasti izmantotu "busy waiting" - regulāru apjautāšanos, kas veltīgi tērē procesora ciklus.

Pavedienu mijiedarbība

wait() and notify() darbība balstās uz 2 pavedienu dīķiem jeb baseiniem (pool)

  • "Wait" baseins
  • "Lock" baseins

Pavedienu stāvokļu diagramma ar wait un notify

Pavediena stāvokļu diagramma

Monitora modelis sinhronizācijai

  • Koplietošanas dati arvien ir konsistentā stāvoklī
  • Nodrošina pret strupceļu
  • Pavedienus, kuri gaida dažādas notifikācijas, neliek tajā pašā "wait" baseinā

Producer piemērs

1    public void run() {
2      char c;
3  
4      for (int i = 0; i < 200; i++) {
5        c = (char)(Math.random() * 26 +'A');
6        theStack.push(c);
7        System.out.println("Producer" + num + ": " + c);
8        try {
9          Thread.sleep((int)(Math.random() * 300));
10        } catch (InterruptedException e) {
11          // ignorē to 
12        }
13      }
14    }

Consumer piemērs

1    public void run() {
2      char c;
3      for (int i = 0; i < 200; i++) {
4        c = theStack.pop();
5        System.out.println("Consumer" + num + ": " + c);
6  
7        try {
8          Thread.sleep((int)(Math.random() * 300));
9        } catch (InterruptedException e) { }
10  
11      }
12    }

Klase SyncStack

public class SyncStack {

private List buffer = new ArrayList(400);

public synchronized char pop() {
}

public synchronized void push(char c) {
}
}

Metode pop

1    public synchronized char pop() {
2      char c;
3      while (buffer.size() == 0) {
4        try {
5          this.wait();
6        } catch (InterruptedException e) {
7          // ignorē to 
8        }
9      }
10      c = ((Character)buffer.remove(buffer.size()-1)).charValue();
11      return c;
12    }

Metode push

public synchronized void push(char c) {
    this.notify();
    Character charObj = new Character(c);
    buffer.addElement(charObj);
}

SyncTest.java

1  package mod13;
2  
3  public class SyncTest {
4  
5    public static void main(String[] args) {
6  
7      SyncStack stack = new SyncStack();
8  
9      Producer p1 = new Producer(stack);
10      Thread prodT1 = new Thread (p1);
11      prodT1.start();
12  
13      Producer p2 = new Producer(stack);
14      Thread prodT2 = new Thread (p2);
15      prodT2.start();
16  
17      Consumer c1 = new Consumer(stack);
18      Thread consT1 = new Thread (c1);
19      consT1.start();
20  
21      Consumer c2 = new Consumer(stack);
22      Thread consT2 = new Thread (c2);
23      consT2.start();
24    }
25  }

Producer.java

1  package mod13;
2  
3  public class Producer implements Runnable {
4    private SyncStack theStack;
5    private int num;
6    private static int counter = 1;
7  
8    public Producer (SyncStack s) {
9      theStack = s;
10      num = counter++;
11    }
12  
13    public void run() {
14      char c;
15      for (int i = 0; i < 200; i++) {
16        c = (char)(Math.random() * 26 +'A');
17        theStack.push(c);
18        System.out.println("Producer" + num + ": " + c);
19        try {
20          Thread.sleep((int)(Math.random() * 300));
21        } catch (InterruptedException e) {
22          // ignorē to 
23        }
24      }
25    }
26  }

Consumer.java

1  package mod13;
2  
3  public class Consumer implements Runnable {
4    private SyncStack theStack;
5    private int num;
6    private static int counter = 1;
7  
8    public Consumer (SyncStack s) {
9      theStack = s;
10      num = counter++;
11    }
12  
13    public void run() {
14      char c;
15      for (int i = 0; i < 200; i++) {
16        c = theStack.pop();
17        System.out.println("Consumer" + num + ": " + c);
18  
19        try {
20          Thread.sleep((int)(Math.random() * 300));
21        } catch (InterruptedException e) { }
22  
23      }
24    }
25  }

SyncStack.java

1  package mod13;
2  
3  import java.util.*;
4  
5  public class SyncStack {
6    private List buffer = new ArrayList(400);
7  
8    public synchronized char pop() {
9      char c;
10      while (buffer.size() == 0) {
11        try {
12          this.wait();
13        } catch (InterruptedException e) {
14          // ignorē to
15        }
16      }
17      c = ((Character)buffer.remove(buffer.size()-1)).
18          charValue();
19      return c;
20    }
21  
22    public synchronized void push(char c) {
23      this.notify();
24      Character charObj = new Character(c);
25      buffer.add(charObj);
26    }
27  }

SyncStack piemērs

Producer2: F
Consumer1: F
Producer2: K
Consumer2: K
Producer2: T
Producer1: N
Producer1: V
Consumer2: V
Consumer1: N
Producer2: V
Producer2: U
Consumer2: U
Consumer2: V
Producer1: F
Consumer1: F
Producer2: M
Consumer2: M
Consumer2: T

Metodes suspend un resume

  • Ir novecinātas Java 2 SDK
  • To vietā jālieto wait un notify

Metode stop

  • Atbrīvo "lock" atslēgas pirms beidz darbu
  • Var atstāt koplietošanas datus nekonsistentā stāvoklī
  • Tā vietā jālieto wait un notify; vai arī atbilstoši jāprogrammē run() metode tā, lai izpildoties pavediena apstāšanās nosacījumam, pavediena vadība izietu no run() (t.i. pavediens varētu beigt darbību dabiskā veidā).

Pareizas pavedienu vadības piemērs

1  public class ControlledThread extends Thread {
2    static final int SUSP = 1;
3    static final int STOP = 2;
4    static final int RUN = 0;
5    private int state = RUN;
6  
7    public synchronized void setState(int s) {
8      state = s;
9      if ( s == RUN ) {
10        notify();
11      }
12    }
13  
14    public synchronized boolean checkState() {
15      while ( state == SUSP ) {
16        try {
17          wait();
18        } catch (InterruptedException e) {
19          // ignorē šo izņēmumu
20        }
21      }
22      if ( state == STOP ) {
23        return false;
24      }
25      return true;
26    }
27  
28    public void run() {
29      while ( true ) {
30        // doSomething();
31  
32        // Pārliecināties, ka koplietošanas dati ir konsistentā stāvoklī
33        // ja uz pavedienu gaida, vai tas iziet
34        // no run()
35        if ( !checkState() ) {
36          break;
37        }
38      }
39    }
40  }

Jautājumi paškontrolei

  • Definēt pavediena jēdzienu
  • Izveidot atsevišķus pavedienus Javas programmā, saprast programmas vadību un datu piekļuvi atsevišķajā pavedienā
  • Vadīt pavediena izpildi un rakstīt platformneatkarīgu kodu ar pavedieniem
  • Aprakstīt grūtības, kuras varētu rasties, ja vairāki pavedieni izmanto kopīgus datus
  • Lietot wait un notify, lai komunicētu starp pavedieniem
  • Lietot synchronized, lai aizsargātu jūtīgus datus, kas pieejami vairākiem pavedieniem
  • Izskaidrot, kāpēc suspend, resume un stop metodes ir novecinātas Java 2 SDK.

Diskusiju tēmas

  • Kuras aplikācijās jāprogrammē kā daudzpavedienu aplikācijas?
  • Kā testēt daudzpavedienu aplikācijas?