Salta el contingut
 

Programació de fils

Joan Puigcerver Ibáñez

Llicència: CC BY-NC-SA 4.0

(Reconeixement - NoComercial - CompartirIgual) 🅭

Conceptes bàsics

Un fil (thread en anglés) és la unitat més xicoteta d'instruccions programades que gestiona el sistema operatiu. La implementació de fils i processos és diferent en 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 els gestiona el sistema operatiu, més específicament el planificador de tasques (scheduler). Els diferents fils d'un procés es poden executar de manera concurrent i compartir recursos del procés, com la memòria. Això no ocorre entre processos diferents.

Avantatges de la multitasca davant de la multiprogramació

Treballar amb fils en lloc de processos independents aporta diversos avantatges:

  • Major capacitat de resposta: un fil pot atendre peticions mentre un altre realitza una tasca més llarga. En la programació de serveis, per exemple, un fil s'encarrega de rebre peticions i, per cada petició, s'obri un altre fil per atendre-la.
  • Compartició de recursos: tots els fils d'un procés tenen accés als recursos del procés, per la qual cosa no cal cap mecanisme addicional. No obstant això, cal evitar els problemes que genera que diversos fils accedisquen alhora a un mateix recurs (per exemple, la memòria) i, per tant, cal prestar més atenció a la sincronització entre fils.
  • Menor cost de creació: 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. En ús de memòria i recursos, crear fils és més barat que crear processos.
  • Paral·lelisme real: en sistemes amb diversos nuclis reals, els fils s'executen de manera realment paral·lela.

Recursos compartits per fils

Un fil és molt paregut a un procés i, per tant, necessita pràcticament les mateixes dades per funcionar. L'única diferència és que alguns recursos estan compartits entre tots els fils d'un procés, mentre que altres són propis de cada fil.

Cada fil té el seu propi comptador de programa, els seus registres i la seua pila. Dins d'un mateix procés, tots els fils comparteixen el codi font, les dades i els 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 depén de l'estat en què es troben:

  • Nou: el fil està preparat per ser executat, però encara no s'ha iniciat. Els fils es creen amb el procés, però no comencen fins que aquest 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ó. Per simplificar, 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ó d'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. Pot acabar per si mateix o perquè el procés que l'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 seua senzillesa, només utilitzen un fil d'execució, mentre que altres, més complexos, en utilitzen diversos. Els fils s'anomenen processos lleugers, en contraposició als processos, anomenats processos pesats.

Un exemple de programació multifil és un programari amb interfície gràfica. Un fil s'encarrega de gestionar la interfície i les peticions de la persona usuària, i la resta de fils realitzen les tasques en segon pla (background). Si no es fa d'aquesta manera, la interfície gràfica es queda bloquejada fins que acabe la tasca.

En 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. Des d'aquest fil es poden crear nous fils per executar diferents tasques, i no cal que executen el mateix codi que el fil principal.

En Java, per crear un nou fil cal crear un objecte que complisca algun dels requisits següents:

  • Estendre la classe Thread:

    public class MyThread extends Thread {
        public void run() {
            // Codi del fil
        }
    }
    
  • Implementar la interfície Runnable:

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

En qualsevol de les dues opcions, cal crear una classe nova que implemente el mètode public void run(), que conté el codi que executarà el fil. A més, cal crear una instància de la classe Thread per poder executar el fil.

Exemple: 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 );
        }

    }
}
Exemple: 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.");
}

Interrompre l'execució d'un fil (interrupt)

El fil que ha creat un fil secundari pot decidir acabar amb l'execució d'aquest. Aquesta acció s'anomena interrompre l'execució d'un fil i es porta a terme amb el mètode Thread.interrupt(). Si el fil interromput està en un mètode sleep o join, es llança l'excepció InterruptedException.

Exemple: Interrompre l'execució d'un fil

El fil InterruptedThread saluda la persona usuària 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 que un fil acabe (join)

Igual que amb els processos, es pot forçar que el fil pare espere que el fil fill finalitze. Per fer-ho, s'utilitza el mètode Thread.join(long millis), que espera que el Thread acabe per continuar.

El paràmetre long millis indica el temps màxim d'espera. Si se supera aquest temps, el fil que estava esperant continua la seua execució. Si s'invoca amb un 0 o sense paràmetre, espera indefinidament. Aquest mètode també 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 espera que acabe el fil t1
    System.out.println("El fil1 ha acabat.");
} catch (InterruptedException e) {
    System.out.println(t1.getName() + " interromput.");
}
Exemple: Esperar 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 i, 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.");
    }
}
📌 Aquest document pot quedar desactualitzat després d’imprimir-lo. Pots consultar la versió més recent a la pàgina web.
🌿 Abans d’imprimir aquest document, considera si és realment necessari. Redueix el consum de paper i ajuda a protegir el nostre entorn.