Salta el contingut
 

Programació de fils

Autor: Joan Puigcerver Ibáñez

Correu electrònic: j.puigcerveribanez@edu.gva.es

Llicència: CC BY-NC-SA 4.0

(Reconeixement - NoComercial - CompartirIgual) 🅭

Conceptes bàsics

Fil: Un fil (thread en anglés) és la unitat més xicoteta d'instruccions programades que són gestionades pel sistema operatiu. La implementació de fils i processos és diferent per cada sistema operatiu, però en la majoria dels casos, un fil és una part d'un procés.

Procés amb dos fils

Figura 1. Procés amb dos fils

Els fils són gestionats pel sistema operatiu, però més específicament pel planificador de tasques (scheduler). Els distints fils d'un procés es poden executar de manera concurrent, compartint recursos del procés (com la memòria), cosa que no ocorre entre diferents processos.

Avantatges de la multitasca davant de la multiprogramació
  • Major capacitat de resposta: pel fet que hi pot haver un fil atenent peticions mentre un altre realitza una altra tasca més llarga. S'usa a la programació de serveis als servidors, un fil s'encarrega de rebre peticions i per cada petició s'obre un altre fil per atendre-la.
  • Compartició de recursos, ja que tots els fils d'un procés tenen accés als recursos del procés, per la qual cosa no cal cap mecanisme addicional. El que s'haurà d'evitar són els problemes derivats que diversos fils accedeixin alhora a un recurs, per exemple, la memòria, així caldrà prestar més atenció a la sincronització entre fils.
  • Com que tots els fils d'un procés utilitzen el mateix espai de memòria, per crear nous fils no cal reservar memòria. Parlant d'ús de memòria i recursos és més barat crear fils que processos.
  • En sistemes amb diversos nuclis reals s'aconsegueix un paral·lelisme real en l'execució de fils.

Recursos compartits per fils

Un fils és molt paregut a un process, per tant, necessiten pràcticament les mateixes dades per funcionar. L'única diferencia és que hi ha alguns recursos que estan compartits entre tots els fils d'un procés, i d'altres, que cada fil té el seu.

Cada fil té el seu propi comprador de programa, seus registres i la seua pila. Dins d'un mateix procés, tots els fils comparteixen el codi font, les dades i el recursos.

Recursos compartits per fils

Figura 2. Recursos compartits per fils

Estats d'un fil

Els fils, com els processos, poden canviar d'estat durant l'execució. El comportament dependrà de l'estat:

  • Nou: el fil es troba preparat per ser executat però encara no s'ha cridat. Els fils s'inicien en la creació del procés, però no comencen fins que el procés ho indique.
  • A punt: El fil no s'està executant però està preparat, per fer-ho.
  • Executable (runnable): El fil està preparat per executar-se o fins i tot en execució. No es pot saber si s'està executant o no, perquè el maquinari no informa d'aquesta situació. Simplificant, es considera que tots els fils d'un procés s'executen de manera paral·lela.
  • Bloquejat: el fil està bloquejat, per exemple, esperant una operació E/S o una sincronització.
  • Acabat: El fil ha finalitzat, però no ha alliberat recursos, ja que no li pertanyen a ell sinó al procés que l'ha creat. Poden acabar per si mateix o perquè el procés que el ha creat el finalitza.

Gestió de fils en Java

De manera general, un fil és un procés que s'està executant en un moment determinat a la CPU. Hi ha programes que per la senzillesa només utilitzen un fil d'execució, mentre que altres programes més complexos utilitzen diversos fils d'execució. Als fils se'ls anomena processos lleugers, en contraposició als processos anomenats processos pesats.

Un exemple de la programació multifil és el d'un programari que utilitza una interfície gràfica. Un fil s'encarrega de gestionar la interfície gràfica i les peticions de l'usuari, i els altres fils s'encarreguen de realitzar les tasques en el background. Si no es fa d'aquesta manera, la interfície gràfica es queda bloquejada fins que acabe la tasca.

A Java, els fils es gestionen mitjançant la classe Thread.

Creació i llançament de fils

Quan un programa s'executa, es crea un procés que té un fil d'execució principal. En aquest fil d'execució, es poden crear nous fils per executar diferents tasques. Els nous fils no cal que executen el mateix codi que el fil principal.

A Java, per crear un nou fil cal crear un nou objecte, que ha de complir algun dels següents requisits:

  • Estendre la classe Thread:

    public class MyThread extends Thread {
        public void run() {
            // Codi del fil
        }
    }
    

  • Implementant la interfície Runnable:

    public class MyRunnable implements Runnable {
        public void run() {
            // Codi del fil
        }
    }
    

En qualsevol de les dues opcions, caldrà crear una classe nova. Aquesta classe ha d'implementar el mètode public void run(), que contindrà el codi que executarà el fil.

A més, caldrà crear una instància de la classe Thread per poder executar el fil.

Creació d'un fil implementant Runnable

La classe HelloRunnable implementa la interfície Runnable i defineix el mètode run amb el codi que s'executarà en el fil.

HelloRunnable.java
package ud2.examples.runnable;

import java.util.concurrent.ThreadLocalRandom;

class HelloRunnable implements Runnable {
    public void run(){
        for(int i = 0; i < 5; i++) {
            int sleepTime = ThreadLocalRandom.current().nextInt(500, 1000);
            try {
                Thread.sleep(sleepTime);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.printf("El fil %s et saluda per %d vegada.\n",
                    Thread.currentThread().getName(), i
            );
        }

    }
}

Després, la classe StartHelloRunnables crea diferents instàncies de Thread amb HelloRunnable i inicia els fils amb el mètode start.

StartHelloRunnables.java
package ud2.examples.runnable;

import java.util.concurrent.ThreadLocalRandom;

public class StartHelloRunnables {
    public static void main(String[] args) throws InterruptedException {
        HelloRunnable rc = new HelloRunnable();

        Thread thread1 = new Thread(rc);
        thread1.setName("Fil1");
        Thread thread2 = new Thread(rc);
        thread2.setName("Fil2");
        Thread thread3 = new Thread(rc);
        thread3.setName("Fil3");

        thread1.start();
        thread2.start();
        thread3.start();

        for(int i = 0; i < 5; i++) {
            int sleepTime = ThreadLocalRandom.current().nextInt(500, 1000);
            Thread.sleep(sleepTime);
            System.out.printf("El fil principal et saluda per %d vegada.\n", i );
        }

    }
}
Creació d'un fil estenent Thread

La classe HelloThread estén la classe Thread i sobreescriu el mètode run amb el codi que s'executarà en el fil.

HelloThread.java
package ud2.examples.thread;

import java.util.concurrent.ThreadLocalRandom;

class HelloThread extends Thread {
    public HelloThread(String name) {
        super(name);
    }

    @Override
    public void run(){
        try {
            for(int i = 0; i < 5; i++) {
                int sleepTime = ThreadLocalRandom.current().nextInt(500, 1000);
                Thread.sleep(sleepTime);
                System.out.printf("El fil %s et saluda per %d vegada.\n",
                        Thread.currentThread().getName(), i
                );
            }
        } catch (InterruptedException e) {
            System.out.printf("El fil %s ha segut interromput\n",
                    Thread.currentThread().getName()
            );
        }
    }
}

La classe StartHelloThreads crea diferents instàncies de HelloThread i les inicia amb el mètode start.

StartHelloThreads.java
package ud2.examples.thread;

import java.util.concurrent.ThreadLocalRandom;

public class StartHelloThreads {
    public static void main(String[] args) {
        HelloThread thread1 = new HelloThread("Fil1");
        HelloThread thread2 = new HelloThread("Fil2");
        HelloThread thread3 = new HelloThread("Fil3");

        thread1.start();
        thread2.start();
        thread3.start();

        try {
            for(int i = 0; i < 5; i++) {
                int sleepTime = ThreadLocalRandom.current().nextInt(500, 1000);
                    Thread.sleep(sleepTime);
                System.out.printf("El fil principal et saluda per %d vegada.\n", i);
            }
        } catch (InterruptedException e) {
            System.out.println("El fil principal ha segut interromput.");
        }
    }
}

Parar l'execució d'un fil: sleep

El mètode estàtic Thread sleep(long millis) permet parar l'execució d'un fil durant un temps determinat (també el fil principal).

Aquest mètode llança una excepció InterruptedException, que cal gestionar.

try {
    Thread.sleep(5000);
} catch (InterruptedException e) {
    System.out.println(Thread.currentThread().getName() +" interromput.");
}

Parar l'execució d'un fil: interrupt

El fil que ha creat un altre fil secundari, pot decidir acabar amb l'execució del segon.

Aquesta acció s'anomena interrompre l'execució d'un fil i es pot portar a terme mitjançant el mètode Thread interrupt(), que acaba la seua execució.

Si el fil interromput està en un mètode sleep o join, es llançarà l'excepció InterruptedException.

Interrompre l'execució d'un fil

El fil InterruptedThread saluda a l'usuari cada dècima de segon.

InterruptedThread.java
package ud2.examples.interrupt;

class InterruptedThread extends Thread {
    public InterruptedThread(String name) {
        super(name);
    }

    @Override
    public void run(){
        try {
            for(int i = 0; i < 1000; i++) {
                System.out.printf("El fil %s et saluda per %d vegada.\n",
                        Thread.currentThread().getName(), i
                );
                Thread.sleep(100);
            }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() +" interromput.");
        }
    }
}

El programa principal crea tres instàncies de InterruptedThread i les inicia. Després cada mig segon interromp un dels fils.

StartInterruptedThreads.java
package ud2.examples.interrupt;

public class StartInterruptedThreads {
    public static void main(String[] args) {
        Thread.currentThread().setName("Fil principal");

        InterruptedThread thread1 = new InterruptedThread("Fil1");
        InterruptedThread thread2 = new InterruptedThread("Fil2");
        InterruptedThread thread3 = new InterruptedThread("Fil3");

        thread1.start();
        thread2.start();
        thread3.start();

        try {
            Thread.sleep(2000);
            thread1.interrupt();
            Thread.sleep(1000);
            thread2.interrupt();
            Thread.sleep(500);
            thread3.interrupt();
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() +" interromput.");
        }
    }
}

Esperar a que un fil acabe: join

Igual que amb els processos, es pot forçar a què el fil pare espere que el fil fill finalitze.

Per fer-ho utilitzarem el mètode Thread join(long millis), que espera a que aquest Thread acabe per continuar.

El paràmetre long millis indica el temps màxim per esperar que el fill acabe. Si supera aquest temps, el fil que estava esperant continuarà la seua execució. Si s'invoca amb un 0 o sense paràmetre, esperarà indefinidament.

Aquest mètode llança una excepció InterruptedException, que cal gestionar.

Thread t1 = new SleepThread(5000);
try {
    t1.start(); // Llança el fil
    t1.join(); // El fil principal esperarà a que acabe el fil f1
    System.out.println("El fil1 ha acabat.");
} catch (InterruptedException e) {
    System.out.println(t1.getName() +" interromput.");
}
Esperar a que un fil acabe

El fil SleepThread espera els mil·lisegons que se li passen com a paràmetre.

SleepThread.java
package ud2.examples.join;

public class SleepThread extends Thread {
    private final int milliseconds;

    public SleepThread(String name, int milliseconds) {
        super(name);
        this.milliseconds = milliseconds;
    }

    @Override
    public void run() {
        try {
            System.out.printf("El fil %s dormint durant %.2f segons.\n", this.getName(), milliseconds/1000.0);
            Thread.sleep(milliseconds);
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() +" interromput.");
        }
    }
}

El programa principal crea tres instàncies de SleepThread. En ordre, va iniciant els fils, un després de l'altre.

StartSleepThreads.java
package ud2.examples.join;

import java.util.List;

public class StartSleepThreads {
    public static void main(String[] args) {
        List<SleepThread> threads = List.of(
                new SleepThread("Fil 1", 2000),
                new SleepThread("Fil 2", 1000),
                new SleepThread("Fil 3", 500)
        );

        try {
            for(SleepThread thread : threads) {
                thread.start();
                System.out.printf("El fil %s ha començat.\n", thread.getName());
                thread.join();
                System.out.printf("El fil %s ha acabat.\n", thread.getName());
            }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + " interromput.");
        }

        System.out.println("Tots els fils han acabat.");
    }
}

Comentaris