|
Multitasca i Java: programar amb
més d'un fil |
|
|
|
Des de fa uns anys, els usuaris ens hem acostumat als sistemes
operatius multitasca. Podem imprimir mentres llegim el correu electrònic,
o podem passar un antivirus al mateix temps que escrivim un document.
De fet, un ordinador domèstic només és capaç
de fer una cosa cada vegada, però el sistema operatiu simula la
capacitat de fer vàries coses simultàniament; així
va repartint el temps d'utilització del
processador entre les diferents aplicacions. Com que això
són processos molt ràpids, l'usuari té la sensació
que està fent diverses coses a la vegada.
Aquesta forma de treballar s'anomena multitasca
i pot ser de dos tipus: apropiativa i
cooperativa. Els sistemes operatius més
moderns disposen de multitasca apropiativa.
Els més antics, com Windows 3.1
i Mac 9, implementaven la multitasca
cooperativa. Un sistema amb molt més perill perquè
un programa podia monopolitzar tots els recursos i, al fallar, bloquejar
el sistema operatiu.
Els programes multifil (multithreaded)
es fonamenten en el mateix principi que els sistemes
operatius multitasca: són programes que poden fer vàries
coses a la vegada, separant les diferents feines en sectors (els fils),
els quals van accedint alternativament als recursos que el sistema operatiu
posa a disposició del programa. Tots els programes comercials importants
són multifil (penseu, per exemple
en els processadors de text que utilitzeu, o en el navegador, que carrega
o descarrega diferents fitxers a la vegada). Un programa
multifil dóna a l'usuari una impressió d'agilitat
i qualitat impossible d'aconseguir d'altra manera.
L'inconvenient és que la programació
multifil no és fàcil ni a Java
ni a d'altres llenguatges. L'aproximació que en farem mitjançant
aquestes pràctiques l'heu de considerar introductòria. Haureu
de consultar documentació específica si us interessa aprofundir
en aquest aspecte de la programació.
|
|
|
|
La pilota que no ens deixa fer res i la que
coopera: |
|
|
|
Escriurem el programa de la pilota que bota, BolaNoFils.java:.
Una bola es desplaça per la finestra del programa i va rebotant
d'ample a ample durant una estona. El programa és una adaptació
del programa de Horstmann i Cornell
de la documentació de Sun.
Es tracta d'un programa codificat sense fils.
Cada cop que l'executem, l'únic que podem fer amb ell és
mirar el rebot de la bola fins al final. Posats a no poder, no podem ni
interrompre el moviment de la pilota fins que no s'acabi el cicle que
l'anima:
|
|
|
 |
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.*;
import javax.swing.*;
/**
* Programa que fa botar una pilota de banda a banda de la finestra
* Funciona sense fils (threads)
* ----------------------------------------------------------------
*/
public class BolaNoFils {
public static int AMPLE=450;
public static int ALT=450;
public static void main(String[] args) {
JFrame frame = new
BolaFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.show();
}
}
/**
* Frame principal de l'aplicació
*/
class BolaFrame extends JFrame {
private CanvasBola canvas;
public BolaFrame() {
setSize(BolaNoFils.AMPLE,BolaNoFils.ALT);
setTitle("Bola
que rebota");
Container
contentPane = getContentPane();
canvas = new
CanvasBola();
contentPane.add(canvas,BorderLayout.CENTER);
JPanel buttonPanel
= new JPanel();
addButton(buttonPanel,
"Comença",
new
ActionListener() {
public
void actionPerformed(ActionEvent evt) {
afegeixBola();
}
});
addButton(buttonPanel,
"Tanca",
new
ActionListener() {
public
void actionPerformed(ActionEvent evt) {
System.exit(0);
}
});
contentPane.add(buttonPanel,BorderLayout.SOUTH);
}
public void addButton(Container c, String
titol,
ActionListener listener) {
JButton boto
= new JButton(titol);
c.add(boto);
boto.addActionListener(listener);
}
public void afegeixBola() {
try
{
Bola
b = new Bola(canvas);
canvas.add(b);
for
(int i=1; i<=1000; i++) {
b.mou();
Thread.sleep(5);
}
}
catch (InterruptedException e) {
}
}
}
/**
* Pinta la bola
*/
class CanvasBola extends JPanel {
private ArrayList boles = new ArrayList();
public void add(Bola b) { boles.add(b);
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 =
(Graphics2D)g;
for
(int i=0; i<boles.size(); i++) {
Bola
b = (Bola)boles.get(i);
b.draw(g2);
}
}
}
/**
* L'objecte bola
*/
class Bola {
Random random = new Random();
private Component canvas;
private static final int XSIZE=15;
private static final int YSIZE=15;
private int x = 0;
private int y = random.nextInt(BolaNoFils.ALT-50);
private int dx = 2;
public Bola(Component c) {
canvas=c;
}
public void draw(Graphics2D g2) {
g2.fill(new Ellipse2D.Double(x,y,XSIZE,YSIZE));
}
public void mou() {
x += dx;
if
(x < 0) {
x
= 0;
dx
= -dx;
}
if
(x + XSIZE >= canvas.getWidth()) {
x
= canvas.getWidth() - XSIZE;
dx
= -dx;
}
canvas.paint(canvas.getGraphics());
}
}
|
|
|
|
|
Ara donarem capacitats multifil
al programa. Farem que tingui dos fils.
Un d'ells, creat implícitament,
que conté la interfície d'usuari
i un altre, creat explícitament,
que controla el bot de la pilota. Inclourem aquesta feina dins d'un objecte
java.lang.Thread que s'activa amb el mètode
run(). Cada cop que piquem sobre el botó
"comença", el programa
llança un nou fil (thread): |
|
|
|
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.*;
import javax.swing.*;
/**
* Programa que fa botar una pilota de banda a banda de la finestra
* Funciona amb fils (threads)
* ----------------------------------------------------------------
*/
public class BolaFils {
public static int AMPLE=450;
public static int ALT=450;
public static void main(String[] args) {
JFrame frame = new
BolaFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.show();
}
}
/**
* Frame principal de l'aplicació
*/
class BolaFrame extends JFrame {
private CanvasBola canvas;
public BolaFrame() {
setSize(BolaFils.AMPLE,BolaFils.ALT);
setTitle("Bola
que rebota amb fils");
Container contentPane
= getContentPane();
canvas = new CanvasBola();
contentPane.add(canvas,BorderLayout.CENTER);
JPanel buttonPanel
= new JPanel();
addButton(buttonPanel,
"Comença",
new
ActionListener() {
public
void actionPerformed(ActionEvent evt) {
afegeixBola();
}
});
addButton(buttonPanel,
"Tanca",
new
ActionListener() {
public
void actionPerformed(ActionEvent evt) {
System.exit(0);
}
});
contentPane.add(buttonPanel,BorderLayout.SOUTH);
}
public void addButton(Container c,
String titol,
ActionListener listener) {
JButton boto
= new JButton(titol);
c.add(boto);
boto.addActionListener(listener);
}
public void afegeixBola()
{
Bola b = new Bola(canvas);
canvas.add(b);
|
|
|
FilBola
fil = new FilBola(b);
fil.start();
}
}
// -------- FilBola és
el fil que activem quan afegim una bola
class FilBola extends Thread {
private Bola labola;
public FilBola (Bola bola) {
labola = bola;
}
//------------------
mètode que s'executa al llançar el fil ---
public void run() {
try
{
for
(int i=1; i<=1000; i++) {
labola.mou();
// Movem la bola
sleep(5);
// Adormim el fil 5 mil·lisegons
}
//
Excepció imprescindible quan cridem wait() o sleep()
}
catch (InterruptedException e) {
}
}
}
|
|
|
class CanvasBola extends JPanel {
private ArrayList boles = new ArrayList();
public void add (Bola b) {
boles.add(b);
}
public void paintComponent (Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g;
for
(int i=0; i<boles.size(); i++) {
Bola
b = (Bola)boles.get(i);
b.draw(g2);
}
}
}
class Bola {
Random random = new Random();
private Component canvas;
private static final int XSIZE=15;
private static final int YSIZE=15;
private int x = 0;
private int y = random.nextInt(BolaFils.ALT-50);
private int dx = 2;
public Bola (Component c) {
canvas=c;
}
public void draw (Graphics2D g2) {
g2.fill(new Ellipse2D.Double(x,y,XSIZE,YSIZE));
}
public void mou () {
x += dx;
if
(x < 0) {
x
= 0;
dx
= -dx;
}
if
(x + XSIZE >= canvas.getWidth()) {
x
= canvas.getWidth() - XSIZE;
dx
= -dx;
}
|
|
|
canvas.repaint();
// Treballant amb fils podem fer repaint() |
|
|
|
|
Analitzem el programa:
- Com que la interfície
d'usuari segueix un fil diferent
del que hem creat (és el fil
o thread per defecte: sempre
hi ha, com a mínim, aquest), quan cliquem sobre el botó
"tanca", el programa ens
obeeix instantàniament i, a més, podem activar tantes
boles com vulguem tot picant sobre el botó "comença".
- El que hem fet és posar el codi
de moviment de la bola dins el mètode
run() del fil.
El mètode run()
és el que s'encarrega de fer les accions del fil.
- Observeu que no hem cridat el mètode
run() directament. En primer lloc hem creat
un objecte
Thread (fil = new FilBola()),
i aquest ha estat el responsable de sol·licitar el mètode
run() a través de fil.start().Sempre
hem de procedir d'aquesta forma:
- declarar el fil
thread=new Thread();
- inicialitzarlo
amb thread.start().
- preveure les accions amb el mètode
run(), que serà cridat a l'inicialitzar-se
el fil.
- La crida a sleep(5)
és molt important. Quan cridem a wait()
o a sleep() estem comunicant
a la resta de fils de l'aplicació que
tenen l'oportunitat d'actuar. En cas contrari, el fil
actual monopolitzaria els recursos del programa.
- Si no tenim l'oportunitat de derivar
els nostres objectes de la classe Thread
perquè ja són fills d'una
altra classe, haurem de fer que implementin
la interfície Runnable.
Dins el programa que hem escrit, tot i que no és necessari, podem
fer aquesta adaptació de la classe
Bola i eliminar la classe
FilBola:
class
Bola implements Runnable{
private
Thread fil; |
...
|
public
void start() {
if
(fil==null) {
fil
= new Thread(this,"bola");
fil.start();
}
}
public void run() {
try
{
for
(int i=1; i<=1000; i++) {
mou();
// Movem la bola
Thread.sleep(5);
// Adormim el fil 5
//
mil·lisegons
}
}
catch (InterruptedException e) {
}
} |
}
|
Ara, per tal de llençar un nou
fil en el mètode
afegeixBola(), hem de fer:
public
void afegeixBola() {
Bola b = new Bola(canvas);
canvas.add(b);
b.start();
} |
i, després de crear l'objecte
Bola i afegir-lo al canvas
cridem el mètode
start() de la classe
Bola.
|
|
Estats d'un fil: |
|
|
 |
Un fil pot estar en aquests quatre estats
possibles:
- Nou:
quan un fil
l'hem instanciat
amb new Bola() però
no s'està executant (encara no hem fet start()).
- Executable: quan
un fil
ha rebut la instrucció start()
està en estat executable.
Que s'estigui executant o no depèn
de si el sistema operatiu li ha donat pas o no.
- Bloquejat: quan
un fil està adormit
-sleep()- o aturat
-wait()-, quan està
esperant acabar una operació
d'entrada-sortida o quan intenta accedir a
objectes bloquejats
per altres fils.
El fil surt
d'aquest estat
quan es desperta
després de l'aturada de l'sleep(),
quan un altre fil
invoca notify()
sobre el fil bloquejat
per wait(), o quan
s'acaba el procés d'entrada-sortida
que el bloquejava.
- Mort: quan un
fil acaba
amb les tasques de run(),
o quan salta alguna excepció
que el mètode
run() no pot interceptar.
|
|
|
|
Grups de fils (thread groups): |
|
|
 |
A la vida real els programes poden tenir molts fils,
tants que en ocasions és útil agrupar-los
en paquets que puguem obrir i tancar junts. La creació de
grups de fils segueix aquestes passes:
ThreadGroup grupdefils
= new ThreadGroup("nomqueposemdelgrup");
Thread fil1 = new Thread(grupdefils, "fil1");
Thread fil2 = new Thread(grupdefils, "fil2"); |
Amb aixó ja podem fer algunes operacions sobre tots els fils
del grup, per exemple, aturar-los tots:
|
|
|
|
Prioritats: |
|
|
 |
Encara que no ho definim explícitament, cada fil
té la seva prioritat, expressada
en un nombre que Java li assigna, entre
1 i 10. Si un fil té una prioritat
més alta que un altre i els dos estan en espera d'execució,
el planificador de fils posarà en
marxa sempre el de prioritat més
alta. Si els dos tenen la mateixa prioritat,
el planificador actuarà seguint
el seu propi criteri.
Decidir la prioritat d'un fil és
força senzill: immediatament després d'instanciar-lo,
executem el mètode setPriority():
Thread unfil =
new Thread("unfil");
unfil.setPriority(Thread.MAX_PRIORITY);
Thread unfil2 = new Thread("unaltrefil");
unfil.setPriority(Thread.MIN_PRIORITY);
Thread unfil3 = new Thread("untercerfil");
unfil.setPriority(Thread.NORM_PRIORITY); |
En qualsevol cas, no ens hem de refiar; l'assignació
de prioritats no és una característica estrictament
multiplataforma. Cada sistema
operatiu gestiona les prioritats de manera diferent: Linux
no farà massa cas de les prioritats
que definim, Windows 2000 les simplificarà
en una escala més restringida...
|
|
|
|
|
|
|
|
|
 |
|
|
|
|