This is an old revision of the document!


Laboratorul 4 - Introducere în Java Multithreading

Responsabili: Alexandru-Ionuț Mustață, Gabriel Guțu-Robu

Suportul de multithreading oferit de Java

Spre deosebire de limbajul C unde implementarea unui thread și a mecanismelor de sincronizare depinde de o bibliotecă ce este specifică unui anumit tip de sistem de operare (Linux, Windows, MacOSX), Java oferă suport pentru lucrul cu thread-urile direct din cadrul SDK său.

Implementarea unui nou thread

În Java există două modalități prin care putem să implementăm un nou thread.

Prima variantă de implementare are la bază crearea unei clase ce implementează interfața Runnable ce conține metoda void run(). Codul ce va reprezenta logica noului thread va fi plasat în interiorul acestei metode.

public class Task implements Runnable {
    public void run() {
        System.out.println("Hello from my new thread!");
    }
}

O altă metodă de implementare a unui thread constă din crearea unei clase ce extinde clasa Thread și suprascrie (overrides) metoda void run() din cadrul acesteia. La fel ca în primul caz, logica noului thread va fi implementată în cadrul acestei metode.

public class MyThread extends Thread {
    public void run() {
        System.out.println("Hello from my new thread!");
    }
}

Rularea în paralel a noului thread

În cazul în care s-a folosit mecanismul de implementare a interfeței Runnable, atunci pentru a putea crea un thread nou ce va conține logica definită în clasa Task se va crea o instanță a clasei Task ce va fi furnizată ca parametru constructorului clasei Thread. Pentru a rula în paralel noul thread creat cu ajutorul constructorului se va apela metoda public void start() a acestuia.

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(new Task());
        t.start();
    }
}

În cazul în care s-a extins clasa Thread pentru implementarea noului tip de thread se va putea crea un thread nou prin instanțierea directă a clasei MyThread și pentru a se porni excecuția în paralel a acestuia se va apela metoda public void start() moștenită din cadrul clasei Thread.

public class Main {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
    }
}

Atenție! Există o distincție foarte importantă între metodele de start() și run() ale clasei Thread! Atunci când este apelată metoda run() codul prezent în aceasta se va executa secvențial în cadrul thread-ului care a apelat-o. Atunci când este apelată metoda start() JVM (Java Virtual Machine) va crea un nou thread ce va executa instrucțiunile prezente în cadrul metodei run() în paralel cu thread-ul care a apelat metoda start().

Așteptarea terminării execuției unui thread

Pentru a aștepta terminarea execuției unui thread, Java ne pune la dispoziție metoda public final void join() a clasei Thread. Trebuie ținut cont de faptul că această metodă poate arunca excepții de tipul InterruptedException.

public class Main {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
 
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Trimiterea de parametrii unui thread și obținerea de rezultate de la acesta

Pentru a trimite parametrii unui thread se va folosi constructorul clasei care inglobeaza logica thread-ului pe care dorim sa il implementam (indiferent de metoda de implementare a acestuia - prin moștenire sau implementeare de interfață). Pentru a obține un rezultat de la un thread care și-a terminat execuția (apelul metodei join() a întors) putem folosi fie metode de tip getter care vor întoarce rezultatul fie putem accesa în mod direct câmpul rezultat în cazul în care acest este definit ca public.

public class Task extends Thread {
 
    private int id;
    private int result;
 
    public Task(int id) {
        this.id = id;
    }
 
    public void run() {
        result = id * id;
    }
 
    public int getResult() {
        return result;
    }
 
}
public class Main {
 
    public static void main(String[] args) {
        int NUMBER_OF_THREADS = 4;
        Thread[] t = new Thread[NUMBER_OF_THREADS];
 
        for (int i = 0; i < NUMBER_OF_THREADS; ++i) {
            t[i] = new Task(id);
            t[i].start();
        }
 
        for (int i = 0; i < NUMBER_OF_THREADS; ++i) {
            try {
                t[i].join();
                System.out.println("Thread " + i + " computed result " + ((Task)t[i]).getResult() + ".");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Synchronized

Cuvântul rezervat synchronized are rolul de a defini blocuri de cod și metode ce reprezintă secțiuni/regiuni critice.

public class MyConcurrentArray<T> {
 
    private static int numberOfInstances = 0;
    private T[] content;
 
    public MyConcurrentArray(int size) {
        if (size > 0) {
            content = new T[size];
        } else {
          throw new RuntimeException("Negative size provided for MyConcurrentArray instantiation.");
        }
 
        synchronized(MyConcurrentArray.class) {
            ++numberOfInstances;
        }
    }
 
    //Metodă sincronizată.
    public synchronized T get(int index) {
        if (index < content.length) {
            return content[index];
        }
        throw new IndexOutOfBoundsException(index + " is out of bounds for MyConcurrentArray of size " + content.length);
    }
 
    public void set(int index, T newT) {
        //Bloc de cod sincronizat ce folosește instanța curentă (this) pe post de zăvor.
        synchronized(this) {
            if (index < content.length) {
                content[index] = newT;
            }
            throw new IndexOutOfBoundsException(index + " is out of bounds for MyConcurrentArray of size " + content.length);
        }
    }
 
    //Metodă sincronizată statică.
    public static synchronized int getNumberOfInstances(){
        return numberOfInstances;
    }
 
    public void size() {
        return content.length;
    }
}

Pentru a putea discuta comportamentul blocurilor de cod sincronizate vom folosi exemplul de mai sus în care este definită o clasă ce implementează sumar conceptul de vector de dimensiune fixă ce poate fi folosit într-un program multithreading (structură de date thread-safe).

Se observă faptul ca metoda get este definită ca fiind synchronized. În momentul când un thread va apela aceasta metodă pe o instanță a clasei MyConcurrentArray va trebui mai întâi să obțină monitorul asociat acesteia (obiectului) pentru a putea executa corpul metodei. Dacă monitorul nu este deținut de nici un alt thread, atunci thread-ul apelant va putea să execute corpul de instrucțiuni al metodei, altfel se va bloca (va aștepta) până când acesta devine disponibil. La încheierea execuției corpului metodei acesta restituie accesul la monitor.

În cazul metodei set este prezent un bloc de instrucțiuni sincronizat. Acesta va folosi monitorul obiectului desemnat între paranteze pentru a oferi thread-ului curent acces excluziv în cadrul regiunii critice. În cazul nostru se folosește instanța curentă a obiectului (this). Mecanismul de intrare și ieșire în/din secțiunea critică este același ca cel prezentat mai sus pentru cazul metodelor sincronizate.

În cazul apelării metodelor sincronizate statice se va încerca obținerea monitorului asociat clasei pentru a executa codul acestora. Acest lucru se întâmplă deoarce o metodă statică aparține clasei, ea nu aparține nici unei instanțe a clasei. De aceea când este necesar accesul exclusiv la un câmp static dintr-o clasă se va folosi clasa în antetul blocului synchronized (MyClass.class, după cum este prezentat și în constructorul din exemplu).

Atenție! Metodele și blocurile de cod synchronized din Java sunt reentrante. Dacă un thread a obținut monitorul unui obiect, atunci el va putea intra în orice alt bloc și metodă sincronizate ce sunt asociate cu acel obiect (implicit cu acel monitor). Acest comportament nu este activat în mod implicit pentru pthread_mutex_t definit în C (se poate obține prin specificarea de atribute la creare, Hint: PTHREAD_MUTEX_RECURSIVE).

CyclicBarrier

Bariera ciclică reprezintă un mecanism de (re)sincronizare al mai multor thread-uri care are scopul de a bloca un număr specificat de thread-uri și de a le lăsa sa își continua execuția într-un mod sincron după ce toate au apelat metoda acesteia de resincronizare. În Java aceast mecanism este reprezentat de clasa CyclicBarrier. La instanțierea acesteia se va specifica numărul de thread-uri pe care aceasta le va resincroniza. Acest mecanism de sincronizare al thread-urilor este util în cazul algoritmilor iterativi ce sunt rulați în paralel și au nevoie de o etapă de resincronizare a firelor de execuția înainte de a trece la noua iterație de calcul. Trebuie ținut cont de faptul că apelul metodei await() pe bariera ciclică poate arunca o excepție de tipul BrokenBarrierException sau InterruptedException.

public class Task extends Thread {
 
    private int id;
 
    public Task(int id) {
        this.id = id;
    }
 
    public void run() {
        iterationCounter = 0;
        while (!solved) {
            executeAlgorithmStep();
 
            try {
                //Resincronizarea thread-urilor pentru urmatorul pas al algoritmului.
                Main.barrier.await();
                ++iterationCounter;
            } catch (BrokenBarrierException | InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
 
}
public class Main {
 
    public static CyclicBarrier barrier;
 
    public static void main(String[] args) {
        int NUMBER_OF_THREADS = 4;
        barrier = new CyclicBarrier(NUMBER_OF_THREADS);
        Task[] t = new Task[NUMBER_OF_THREADS];
 
        for (int i = 0; i < NUMBER_OF_THREADS; ++i) {
            t[i] = new Task(id);
            t[i].start();
        }
 
        for (int i = 0; i < NUMBER_OF_THREADS; ++i) {
            try {
                t[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

CheatSheet CyclicBarrier

Volatile

Cuvântul rezervat volatile asociat unei variabile specifică faptul că acea variabilă nu va intra în procesul de cacheing (nici la nivel de regiștrii, nici la nivel de cache L1, L2, L3) și fiecare scriere și citire asociată acesteia se va realiza direct lucrând cu memoria RAM. Acest lucru este util în prevenția citirii unei date vechi din cache-ul sau registrul asociat core-ului pe care rulează un thread în momentul în care variabila a căpătat o valoare nouă pe alt thread ce lucrează pe alt core.

public static volatile int counter = 0;

Este important de menționat faptul că variabilele volatile cresc timpul de execuție al programului, deoarece fiecare modificare adusă lor trebuie propagată și celorlalte thread-uri, în timp ce variabilele normale pot fi cache-uite la un moment dat chiar până la nivelul regiștrilor procesorului (în cazul unui contor de buclă).

Variabile atomice

În cadrul laboratorului 2 am observat cum operația de incrementare (exemplul cu instrucțiunea a+=2) nu este o operație atomică (operație ce nu poate fi divizată atunci când este executată de către un thread). Java oferă suport pentru o serie de tipuri de date (tipuri atomice) ce au asociate operații atomice [1], [2]. Acestea sunt utile pentru a fi folosite pe post de contori sau acumulatori fără a mai folosi mecanisme de sincronizate.

Exerciții

1. Creați un program care să lanseze un număr de thread-uri egal cu numărul de core-uri de care dispune calculatorul vostru. Fiecare thread trebuie să afișeze la consolă un text de tipul ”Hello from thread #id”.

2. Rezolvați bug-urile prezente în scheletul de laborator. Folosiți-vă de hint-uri din surse.

3. Paralelizați dublarea elementelor unui vector.

4. Paralelizați algoritmul Floyd-Warshall.

apd/laboratoare/04.1635323786.txt.gz · Last modified: 2021/10/27 11:36 by florin.mihalache
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0