Thread

15809

Els threads permeten executar seccions de codi en paral.lel (a la vegada) enlloc de sequencialment (un després de l'altre).

Tots els programes tenen almenys un thread quan s'executen, que s'anomena "main", i és el responsable d'executar la funció main().

També hi ha altres threads que s'executen per defecte en una JVM, com pot ser el "garbage collector".

Thread

Un thread és una seqüència d'instruccions que s'executen de manera separada del reste del progrmama.

Cada thread està representat per un objecte: una instància de la classe java.lang.Thread (o d'una subclasse).

Si un programa crea un o més threads a partir del thread "main" parlem d'un progrmama multi-thread.

La classe Thread té un mètode estàtic anomenat currentThread que et permet obtenir una referència a l'objecte thread que s'està executant actualment:

val thread: Thread = Thread.currentThread() // the current thread

La classe Thread emmagatzema informació bàsica sobre el thread: el seu nom, identificador (long), prioritat i algunes altres característiques que es poden obtenir mitjançant els mètodes de la classe Thread.

La informació del thread principal

A continuació fem servir el thread main com exemple per obtenir les característiques fent-hi referències a través d'un objecte de la classe Thread.

fun main() {
    val thread: Thread = Thread.currentThread() // main thread

    println("Name: ${thread.name}")
    println("ID: ${thread.id}")
    println("Alive: ${thread.isAlive}")
    println("Priority: ${thread.priority}")
    println("Daemon: ${thread.isDaemon}")
}

Totes les declaracions d'aquest programa són executades pel thread principal.

Podeu veure la informació general sobre aquest fil:

  • thread.name retorna el nom del thread.

  • thread.id retorna l'identificador únic del thread.

  • thread.isAlive ens indica si el thread s'ha iniciat i encara no ha mort.

  • thread.priority retorna la prioritat del thread. Cada thread té una prioritat que determina l'ordre d'execució: els threads amb una prioritat més alta s'executen abans que els threads amb prioritats més baixes.

  • t.isDaemoncomprova si el thread és un dimoni. Un "daemon thread" (prové de la terminologia UNIX) és un thread de baixa prioritat que s'executa en segon pla per realitzar tasques com la "garbage collection", etc. La JVM no espera que els "daemon" threads s'aturin abans de sortir, mentre que si que espera als thread que no són dimoni per finalitzar l'execució del programa.

La sortida del programa:

Name: main
ID: 1
Alive: true
Priority: 5
Daemon: false

Cada característica es pot canviar configurant un valor nou:

val thread: Thread = Thread.currentThread() // main thread

t.name = "hello"
println("New name: ${thread.name}") // New name: hello

El mateix codi es pot aplicar a qualsevol thread en execució, no només al principal.

Activitat

1.- Quants threads pot tenir un programa?

  1. Almenys un
  2. D'un al nombre de nuclis de processador
  3. Exactament un
  4. Nombre exacte de nuclis de processador

Almenys un.

TODO

Custom threads

16006

A partir del thread principal (main) pots crear nous threads.

Per fer-ho, has de crear nous objectes threads, escriue codi per executar-lo en un thread separat i iniciar-lo.

Crear un thread

Pots crear un thread de dos maneres diferents:

  • Estendre la classe Thread i sobreesciure el mètode run.

  • Implementar la interfície Runnable i passar la implementació al constructor de la classe Thread.

A continuació tens un exemple en que estenem la classe Thread:

class HelloThread : Thread() {
    override fun run() {
        val msg = "Hello, I'm $name"
        println(msg)
    }
}

I aquí tens un exemple implementat la interfície Runnable:

class HelloRunnable : Runnable {
    override fun run() {
        val thread = Thread.currentThread()
        val msg = "Hello, i'm ${thread.name}"
        println(msg)
    }
}

Pots veure que en tots dos casos has de sobreescriure el mètode run amb el codi que vols que executi el thread.

Has de fer servir una opció o altre en funció de la tasca i de les teves preferències: si estens la classe Thread pots acceptar atributs i mètodes de la classe base, però no pots estendre altres classes.

La classe Thread té molts constructors: podeu trobar una llista completa en aquest document: Thread - Constructor Summary.

Si estenem la classe Thread podem crear un objecte thread directamentmitjançant el contructor de la subclasse:

val t1 = HelloThread() // a subclass of Thread

En canvi, si implementem la interfície Runnable hem de crear un objecte mitjançant un dels constructors de la classe Thread:

val t2 = Thread(HelloRunnable()) // passing runnable

Implementant la interfície Runnable podem especificar el nom del thread passant-lo al constructor:

val helloThread = Thread(HelloRunnable(), "hello-thread")

Pots veure que la intefície Runnable t'ofereix una manera més versàtil de treballar amb threads ja que no has de sobreescriure el constructor de la classe Thread per modificar el nom com tindries que fer amb la la classe HelloThread.

La composició sempre és més flexible que l'herència.

I amb una expressió lambda encara és mes senzill:

val t = Thread {
        println("Hello, I'm ${Thread.currentThread().name}")
    }

Una manera senzilla de crear un thread

Però, per què hem d'implementar una interfície o ampliar una classe per crear un thread?

Pots crear un thread amb la funció thread(...) del paquet kotlin.concurrent.

En aquest cas, el codi que s'ha d'executar es passa com un argument amb el nom block:

import kotlin.concurrent.thread

val t = thread(start = false, name = "Thread 4", block = {
        println("Hello, I'm ${Thread.currentThread().name}")
    })

Aquesta funció té uns quants paràmetres que et permeten configurar el thread:

  • start – si true, el thread es crea i s'inicia immediatament.

  • isDaemon – si true, el thread es crea com un thread dimoni.

  • contextClassLoader – un carregador de classes esepcífic per aquest thread.

  • name – el nom del thread.

  • priority – la prioritat del thread.

  • block – el codi que ha d'executar el thread.

La funció thread() és del paquet kotlin.concurrent, recorda que l'has d'importar.

La creació d'un thread no impplica que aquest s'executi, sinó que s'ha d'iniciar la seva execució de manera explícita.

Threads d'inici

La classe Thread té un mètode amb el nom start() que s'utilitza per iniciar un thread.

El thread no s'inicia de manera inmediata, sinó que després d'invocar aquest mètode en algún moment s'invocarà el mètode runde manera automàtica.

Suposem que dins de la funció main, crees un thread anomenat t utilitzant la funció thread() i després l'inicies:

fun main() {
    val t = thread(start = false, block = {
        println("Hello, I'm ${Thread.currentThread().name}")
    })
    t.start()
}

Si vols, pots configurar el valor del paràmetre start com a true, o no configurar-lo (true és el valor predeterminat).

En aquest cas el thread s'iniciarà sense tenir que invocar el mètode start():

fun main() {
    val t = thread(block = {
        println("Hello, I'm ${Thread.currentThread().name}")
    })
}

En ambdós casos el resultat serà:

Hello, i'm Thread-0

Com funciona un thread

Aquí tens un gràfic que explica com comença realment un thread i per què no s'executa de manera immediata.

TODO: gràfic

Com pots veure, hi ha un cert retard entre l'inici d'un thread i el moment en què realment comença a funcionar (executar-se).

De manera predeterminada, un thread nou s'executa en el mode no dimoni .

Recordatori: La diferència entre el mode dimoni i el mode no dimoni és que la JVM no finalitzarà un programa en execució mentre encara quedin threads que no són dimonis, mentre que els threads del dimoni no impediran que la JVM finalitzi. Per tant, els threads de dimonis solen fer alguna feina en segon pla.

No confonguis els mètodes run i start!:

  • Has d'invocar start si vols executar el codi que està dins el mètode run en un altre thread.

  • Si invoques el mètode run directament, el codi s'executarà en el mateix thread.

  • Si intentes iniciar un thread més d'una vegada invocant més d'un cop el mètode start, el mètode start tornarà una IllegalThreadStateException el segon i els demés cops.

Tot i que dins d'un únic thread totes les instruccions s'executen seqüencialment, és impossible determinar l'ordre relatiu de les sentències entre diversos threads sense mesures addicionals que no tindrem en compte en aquesta activitat.

Considera el codi següent:

fun main() {
    val t1 = HelloThread()
    val t2 = HelloThread()
    t1.start()
    t2.start()

    println("Finished")
}

L'ordre en que s'executaran els threads pot ser diferents cada cop que executes el programa.

Per exemple, el resultat podria ser aquest:

Hello, I'm Thread-1
Finished
Hello, I'm Thread-0

Fins i tot és possible que tots els threads puguin imprimir el seu text després que el fil principal imprimeixi "Finished":

Finished
Hello, I'm Thread-0
Hello, I'm Thread-1

Això vol dir que tot i que cridem el mètode start seqüencialment per a cada thread, no sabem quan es cridarà realment el mètode run.

Quan escrius un programa mai pots saber en quin ordre s'executaran els threads si no utilitzes mecanismes de concurrència que encara no hem explicat.

Un programa senzill multithreaded

A continuació tens un programa multithreaded amb dos threads:

  1. Un thread llegeix els números de l'entrada estàndard i imprimeix els seus quadrats.

  2. El thread principal de tant en tant imprimeix missatges a la sortida estàndard.

Els dos threads funcionen simultàniament.

A continuació tens un thread que llegeix els números en bucle i imprimeix el seu quadrat. Té una instrucció break per aturar el bucle si el nombre donat és 0.

class SquareWorkerThread(name: String) : Thread(name) {
    override fun run() {
        while (true) {
            val number = readln().toInt()
            if (number == 0) {
                break
            }
            println(number * number)
        }
        println("$name's finished")
    }
}

Dins de la funció main, el programa inicia un objecte de la classe SquareWorkerThread, que escriu missatges a la sortida estàndard del thread principal.

fun main() {
    val workerThread = SquareWorkerThread("square-worker")
    workerThread.start() // start a worker (not run!)

    for (i in 0 until 5_555_555_543L) {
        if (i % 1_000_000_000 == 0L) {
            println("Hello from the main!")
        }
    }
}

Aquí teniu un exemple d'entrades i sortides amb comentaris:

Hello from the main!   // the program outputs it
2                      // the program reads it
4                      // the program outputs it
Hello from the main!   // outputs it
3                      // reads it
9                      // outputs it
5                      // reads it
Hello from the main!   // outputs it
25                     // outputs it
0                      // reads it
square-worker finished // outputs it
Hello from the main!   // outputs it
Hello from the main!   // outputs it

Process finished with exit code 0

Com pots veure, aquest programa realitza dues tasques "al mateix temps ": una al thread principal i l'altra al thread de treball.

Pot ser que no sigui "al mateix temps" en el sentit físic si no s'executen en nuclis diferents de la CPU; no obstant això, a ambdues tasques se'ls proporciona temps de CPU per executar-se.

Activitat

1.- Tens una instància de la classe Thread anomenada thread.

Selecciona la declaració correcta:

  1. No podem invocar thread.run() diverses vegades.
  2. Si cridem al mètode thread.run(), aquest crida al mètode start() d'aquesta instància
  3. No podem invocar thread.start() més d'una vegada.
  4. No podem crear cap altra instància de la classe Thread

No podem invocar thread.start() més d'una vegada.

Cridar el mètode run() és simplement com cridar a qualsevol altre mètode normal: s'executa dins el thread actual.

Gestió

16200

El mètode start et permet iniciar un thread en l'objecte corresponent, però a vegades és necessari gestionar el cicle de vida d'un thread mentre està funcionant en lloc d'iniciar-lo i oblidar-se.

A continuació veurem dos mètodes d'ús habitual en la programació multithread: sleep() i join().

Tots dos mètodes poden llançar una InterruptedException que no gestionarem per simplificar el codi.

Sleep

El mètode Thread.sleep() fa que el thread que s'executa actualment suspengui l'execució durant el nombre especificat de mil·lisegons.

Aquest és un mitjà eficient per fer que el temps del processador estigui disponible per als altres fils d'una aplicació o altres aplicacions que es puguin executar en un ordinador.

Sovint fem servir aquest mètode per simular invocacions que requereixen molt temps de computació o tasques difícils.

println("Started")

Thread.sleep(2000) // suspend current thread for 2000 milliseconds
         
println("Finished")

Vegem què fa aquest codi:

  • Al començament imprimeix "Started"
  • A continuació el thread actual se suspèn durant 2000 mil·lisegons (pot ser més temps, però no menys del que s'indica).
  • Finalment, el thread es desperta i imprimeix "Finished".

Una altra manera de fer dormir el thread actual és utilitzar la classe especial TimeUnit del paquet java.util.concurrent:

  • TimeUnit.MILLISECONDS.sleep(2000) executa Thread.sleep durant 2000 mil·lisegons.
  • TimeUnit.SECONDS.sleep(3) executa Thread.sleep durant 3 segons (que és el mateix que 3000 mil.lisegons).

La classes TimeUnit té més períodes per escollir: NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, i DAYS.

Per exemple:

import java.util.concurrent.TimeUnit

println("Started")
TimeUnit.SECONDS.sleep(3) // suspend current thread for 3 seconds
println("Finished")

Join

El mètode join obliga el thread actual a esperar la finalització d'un altre thread en el qual es va cridar aquest mètode.

A l'exemple següent, l'string "The end" no s'imprimirà fins que el thred no acabi.

fun main() {
    val thread: Thread = thread(...)
    println("The start")

    thread.start() // start thread

    println("Do something useful")

    thread.join()  // waiting for thread to die

    println("The end")
}

La versió sobrecarregada del mètode join pren el temps d'espera en mil·lisegons:

thread.join(2000)

Això s'utilitza per evitar esperar massa temps o fins i tot infinitament en cas que el thread es penji.

Considerem un altre exemple.

La classe Worker simula que està resolent "una tasca difícil" que necessita molt de temps:

class Worker: Thread() {
    override fun run() {
        println("Starting a task")
        sleep(2000) // it solves a difficult task
        println("The task is finished")
    }
}

Aquí tens una funció main on el thred main espera que finalitzi worker:

fun main() {
    val worker = Worker()
    worker.start() // start the worker
    Thread.sleep(100)
    println("Do something useful")

    worker.join(3000)  // waiting for the worker
    println("The program stopped")
}

El thread principal espera worker i no pot imprimir el missatge "The program stopped" fins que worker finalitzi o superi el temps d'espera.

L'únic del que podem estar segurs és que:

  • "Starting a task" s'imprimirà abans que "The task is finished"
  • "Do something useful" s'imprimrà abans que "The program stopped".

Hi ha diverses sortides possibles:

1.- La tasca es completa abans que es superi el temps d'espera:

Starting a task
Do something useful
The task is finished
The program stopped

2.-

Do something useful
Starting a task
The task is finished
The program stopped

3.- La tasca es completa després que es supera el temps s'espera:

Do something useful
Starting a task
The program stopped
The task is finished

4.-

Starting a task
Do something useful
The program stopped
The task is finished

Activitats

1.- Imagina que tens un objecte t que és una instància d'una classe que estén Thread.

Quin és el resultat de la invocació de t.join()?

  1. Els threads es fusionaran en un de sol.
  2. t espera que finalitzi el thread actual
  3. El thread actual espera que finalitzi t.
  4. Atura el tread actual fins que continuees crida

TODO revisar

És impossible cridar a join en un objecte d'un altre fil, només el podeu cridar al fil actual

Excepcions

16422

Com ja saps, els programes poden llançar excepcions si hi ha errors en el codi i si aquesta exepció no es gestiona el programa s'atura.

El codi que s'executa dins d'un thread també pot llançar exepcions.

A continuació veurem com es comporten diversos threads quan tenen excepcions que no es gestiones dins el mateix bloc de codi..

Threads i excepcions

Si un dels threads del teu programa llança una excepció que cap mètode no detecta dins de la pila d'invocació, el thread s'acabarà.

Si aquesta excepció es produeix en un programa d'un sol thread, tot el programa s'aturarà perquè la JVM finalitza el programa en execució tan bon punt no quedin més thread que no siguin dimonis .

Aquí tens un petit exemple:

fun main() {
    print(2 / 0)
}

Si executes aquest programa es produeix una execpció:

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at ExampleKt.main(example.kt:6)
    at ExampleKt.main(example.kt)

Process finished with exit code 1

El codi 1 significa que el procés ha finalitzat a causa d'un error.

En canvi, si es produeix un error dins d'un thread que no és el principal, el procés no s'atura:

import kotlin.concurrent.thread

fun main() {
    val thread = thread(block = { print(2/0) })
    
    thread.join() // wait for the thread with an exception to terminate

    println("I am printed!") // this line will be printed
}

Encara que l'exepció no es gestioni, el programa finalitzara sense cap error.


Exception in thread "Thread-2" java.lang.ArithmeticException: / by zero
    at CustomThread.run(example.kt:14)
I am printed!

Process finished with exit code 0

El codi 0 significa que el procés ha finalitzat correctament.

Què passarà amb els altres threads si hi ha un error al thread principal?

fun main() {
    thread(block = {
        println("Hello from the custom thread!")
    })
    print(2 / 0)
    println("Hello from the main thread!")
}

La sortida del programa serà la següent:

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Thread_excKt.main(thread_exc.kt:7)
	at Thread_excKt.main(thread_exc.kt)
Hello from the custom thread!

Process finished with exit code 1

Pots veure que:

  • El procés ha finalitzar amb un error (codi de sortida 1).
  • El codi després de print(2/ 0) no s'ha executat
  • El bloc de codi del thread no principal s'ha executat.

Per tant, encara que hi hagi una excepció en el thread principal el programa no s'atura, i els altres threads es segueixen executant amb normalitat.

Activitats

1.- Suposem que es produeix una excepció que no es gestiona en el thread principal .

Què passarà amb els altres threads i amb tot el procés?

  1. Només el thread principal s'aturarà i el codi de sortida del procés el determinarà l'últim thred finalitzat
  2. Només s'aturarà el thread principal, però al final el procés acabarà amb el codi 1 (Error)
  3. Tots els threads en execució s'aturaran immediatament i el procés finalitzarà amb el codi 1 (error). 4 . Només s'aturarà el thread principal i el procés acabarà amb el codi 0 (OK)