1. Diseño de Algoritmos Distribuidos en Java
José Luis Pastrana Brincones (pastrana@lcc.uma.es)
Departamento de Lenguajes y Ciencias de la Computación de la Universidad de Málaga
Resumen
El diseño de algoritmos es una parte fundamental en las tecnologías de la
información, como se ha visto reflejado por el gran número de libros y artículos
relacionados con el tema. La gran mayoría de los mismos, tratan los algoritmos en el
contexto de una programación secuencial, donde el flujo de control va siempre en una
misma dirección y en cada paso del algoritmo se realiza una única acción. El gran auge
del concepto de redes de comunicación, junto con los avances en la metodología de
programación han conseguido que el concepto de comunicación y de diseño de
algoritmos distribuidos surjan como un nuevo aspecto en las técnicas de desarrollo del
software.
Los algoritmos distribuidos, clásicamente, han sido desarrollados mediante el
uso de lenguajes imperativos a los que se les ha añadido determinadas primitivas de
comunicación, pero si nos fijamos en la filosofía de los lenguajes orientados a objetos,
podemos ver la similitud existente entre procesos y objetos, comunicación y métodos, y
aún más, incluso podríamos ver nuestra red de comunicaciones como un objeto más en
nuestro sistema. En este trabajo, consideraremos dos clases de algoritmos distribuidos:
la primera en la que trataremos los algoritmos basados en una arquitectura
Cliente/Servidor y para la que usaremos una técnica de invocación de métodos remotos.
Y una segunda ,más genérica, en la que consideraremos un sistema distribuido como un
conjunto de objetos o procesos conectados a través de una red y que cooperan para la
obtención de un objetivo común. Para la resolución de este tipo de problemas,
consideraremos la red como un objeto más del sistema que tiene métodos para el envío
y recepción de mensajes a través de la misma.
A lo largo del trabajo, se describirán ambas técnicas y se implementarán varios
algoritmos como ejemplo para su mejor comprensión. Como lenguaje de soporte hemos
usado Java debido al gran auge que está teniendo en nuestros días, así como por
adaptarse a los requerimientos de orientación a objetos e incluir un paquete para la
2. invocación de métodos remotos, y la clase jPVM (David A. Thurman) que embebe PVM
en Java y que nos ha servido para modelar la red como un objeto del sistema.
Arquitectura Cliente/Servidor.
En una arquitectura Cliente/Servidor, el servidor es un proceso que repetidamente
maneja peticiones de los clientes (recibe una petición de un cliente, realiza el servicio y retorna
el resultado), y el cliente es el proceso que realiza dichas peticiones de servicios a otra
aplicación llamada cliente.
El desarrollo de aplicaciones Cliente/Servidor mediante sockets conlleva el diseño de
un protocolo que consiste en un lenguaje común entre el Cliente y el Servidor. El diseño de
dicho protocolo es una de las mayores fuentes de errores tales como el deadlock. En lugar de
trabajar directamente con sockets, una aplicación Cliente/Servidor puede ser desarrollada
usando una filosofía de orientación a objetos. Desde el punto de vista del Cliente,
consideramos el Servidor como un objeto remoto que nos ofrece una serie de servicios que
pueden ser llamados mediante la invocación remota de sus métodos. La invocación de
métodos remotos (Remote Methods Invocation, RMI) es muy similar (pero más general y
fácil de usar) que la llamada a procedimientos remotos (Remote Procedure Call, RPC).
Básicamente, el usuario trabaja con los objetos remotos como si estos fueran locales, gracias
a que los paquetes RMI incluyen un recolector de basura distribuido y una invocación
transparente de los métodos remotos (con la misma sintaxis que los locales).
Veamos a continuación cómo podríamos realizar esto mediante un sencillo ejemplo
realizado en Java. Vamos a desarrollar un Servidor que nos realiza operaciones sobre
vectores y matrices, así como un cliente que las utiliza. El desarrollo de aplicaciones
Cliente/Servidor en Java conlleva 6 etapas:
1. Definición de la Interfaz Remota. Permite al cliente conocer los servicios que puede utilizar,
por lo tanto debe ser pública.
2. Implementación de la interfaz remota. Esta será el cuerpo del servidor.
3. Escribir la aplicación cliente que use el servidor.
4. Generar los stubs y skeletons. Un stub es un cliente proxy y un skeleton es una entidad
servidor que realiza las llamadas a la implementación actual del objeto remoto
5. Ejecutar el registry. Es un Servidor de nombres.
3. 1. Ejecutar el Servidor y el/los Cliente.
Remote interface.
public interface MatrixInterface extends java.rmi.Remote
{ public int[] add(int a[], int b[] ) throws java.rmi.RemoteException;
public int[] sub(int a[], int b[] ) throws java.rmi.RemoteException;
....................................................................................................................
public int[][] add(int a[][], int b[][] ) throws java.rmi.RemoteException;
public int[][] sub(int a[][], int b[][] ) throws java.rmi.RemoteException;
....................................................................................................................}
Remote interface Implementation
import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;
public class MatrixServer extends UnicastRemoteObject implements MatrixInterface
{ private String ServerName;
public MatrixServer ( String s ) throws RemoteException
{ ServerName = s; }
public int[] add(int a[], int b[] ) throws RemoteException
{ ................... }
public int[] sub(int a[], int b[] ) throws java.rmi.RemoteException
{ ................... }
.................................................................................................................
public int[][] add(int a[][], int b[][] ) throws java.rmi.RemoteException
{ ................... }
public int[][] sub(int a[][], int b[][] ) throws java.rmi.RemoteException
{ ................... }
.................................................................................................................
public static void main(String argv[])
{ MatrixServer server;
System.setSecurityManager( new RMISecurityManager() );
try
{ server = new MatrixServer ( " MatrixServer " );
Naming.rebind("//host/ MatrixServer ",server);
System.out.println("MatrixServer running ");
}
catch (Exception e)
{ System.out.println("Sorry, error succeeded:" + e.getMessage());
e.printStackTrace();
}
}
}
Cliente Application
import java.rmi.*;
import java.net.*;
public class Cliente
{ public static void main(String argv[])
{ int a[] = { 1, 2, 3, 4, 5 };
int b[] = { 6, 7, 8, 9, 0 };
int result [] = new int[5];
MatrixInterface RemoteServer;
int i;
try
{ String url = "//host/MatrixServer";
RemoteServer = (MatrixInterface)Naming.lookup(url);
result = RemoteServer.add(a,b);
System.out.print(“(”);
for(i=0;i<4;++i) System.out.print(a[i] + “, ”);
System.out.print(a[4] + “) + ( ”);
for(i=0;i<4;++i) System.out.print(b[i] + “, ”);
System.out.print(b[4] + “) = ( ”);
for(i=0;i<4;++i) System.out.print(c[i] + “, ”);
System.out.println(c[4] + “)”);
}
catch (Exception e)
{ System.out.println("Sorry, error succeeded:" + e.getMessage());
e.printStackTrace();
}
}
}
4. Conjunto e Procesos que Cooperan para lograr un Objetivo Común.
Un algoritmo distribuido ha sido definido clásicamente como un conjunto de procesos,
conectados a través de una red, que cooperan para lograr un objetivo común. Nuestra
aproximación consiste en considerar cada proceso como un objeto, e incluso la red de
conexión es considerada como un objeto más de nuestro sistema. Cada objeto-proceso tiene
sus propios métodos y objetos locales para la resolución de su parte de la tarea y la red es un
objeto especial que tiene métodos para el envío y recepción de mensajes entre objetos.
Veamos cómo podemos utilizar esta aproximación para la resolución de un problema
clásico para sistemas distribuidos: El Problema de la Exclusión Mutua Distribuida. Hemos
utilizado Java como soporte junto con la clase jPVM (desarrollada por David A. Thurman,
Center for Human-Machine Systems Research, School of Industrial and Systems Engineering,
Georgia Institute of Technology) para el modelado de la red como objeto. Esta clase
implementa todas las primitivas de las librerías de PVM mediante la incorporación de métodos
en código nativo.
El Problema de la Exclusión Mutua Distribuida.
Este es uno de los primeros problemas que se plantean en programación distribuida.
Una serie de procesos, que trabajan en paralelo, compiten por recursos que no pueden ser
accedidos al mismo tiempo. El problema de la exclusión mutua fue debido a Dekker y usaba
únicamente operaciones primitivas para la lectura y escritura de una palabra en memoria. En
un sistema distribuido, la especificación de un algoritmo de exclusión mutua no puede ser
realizada en términos de dependencia de un acceso a memoria central, debe ser realizada en
términos de intercambio de mensajes, y el algoritmo debe tener las características de
equitabilidad y ausencia de bloqueo, es decir, que cualquier proceso que desee entrar en su
sección crítica debe ser capaz de hacerlo en un intervalo finito de tiempo.
Circulación de un Token en un Anillo Lógico.
Este es un algoritmo simple para el problema de la exclusión mutua distribuida,
aplicable cuando la topología de nuestra red es un anillo. Cada proceso solamente podrá
entrar en su sección crítica cuando posea el Token, que siempre recibirá de su vecino de la
izquierda, y dará al de su derecha al salir de la sección crítica.
5. public class TokenRing
{
public static void main(String argv[])
{ Net net;
int max;
int me;
int left;
int right;
boolean token_present;
try { Net = new Net((Integer.valueOf
(argv[0])).intValue()); }
catch (ArrayIndexOutOfBoundsException
e) { Net = new Net(1); }
me = net.mi_id();
max = net.numProcs();
right = (me+1)%max;
left = me -1;
if (left<0) left = max -1;
if (me==0) token_present=true;
else token_present = false;
/* Algorithm Body */
while(true)
{
if (!token_present)
{net.wait_token(left);
token_present=true;
}
/* Critical Section */
System.out.println("Process "+me+
" in Critical Section");
/* Critical Section */
net.send_token(right);
token_present=false;
}
}
}
public class Net extends jPVM
{ private int my_id;
private int my_father;
private int maxProc;
private int processes[];
private Net pvm;
public final static int Initialise = 0;
public final static int Token = 1;
public Net() { super(); }
public Net(int max)
{ String javaexe ="/jdk/bin/java";
String args[];
int max_array[];
int i;
args = new String [3];
args[0]= "-classpath";
args[1]= " /jdk/lib/classes.zip”
+ “:/jPVM-v1.1.4:/TokenRing_pvm";
args[2]="TokenRing";
max_array = new int[1];
pvm = new Net();
my_id = pvm.mytid();
my_father = pvm.parent();
if (my_father==PvmNoParent)
{ maxProc = max;
processes = new int[maxProc];
processes[0]=my_id;
for(i=1;i<maxProc;++i)
{ pvm.spawn(javaexe,args,
PvmTaskDefault,"",max_array);
processes[i]=max_array[0];
}
max_array[0] = maxProc;
pvm.initsend(PvmDataDefault);
pvm.pkint(max_array, 1, 1);
pvm.pkint(processes, maxProc, 1);
for(i=1;i<maxProc;++i)
pvm.send(processes[i], Initialise);
}
else
{ pvm.recv(my_father, Initialise);
pvm.upkint(max_array, 1, 1);
maxProc = max_array[0];
processes = new int[maxProc];
pvm.upkint(processes, maxProc, 1);
}
}
public int mi_id()
{ int i;
for (i=0;i<maxProc;++i)
if (processes[i]==my_id) return i;
return -1;
}
public int numProcs()
{ return maxProc; }
public void wait_token(int left)
{ pvm.recv(processes[left],Token); }
public void send_token(int right)
{ pvm.initsend(PvmDataDefault);
pvm.send(processes[right],Token);
6. } }
El Algoritmo de Ricart and Agrawala/Suzuki Kasami .
En este algoritmo, el privilegio que permite a un proceso entrar en su sección crítica
esta representado por un Token; cualquier proceso que posea el Token podrá entrar en su
sección crítica sin pedir permiso a los demás. Inicialmente el Token es asignado a uno de los
procesos (el proceso número cero en nuestro caso). Un proceso que desea entrar en su
sección crítica no sabrá qué otro proceso tiene el Token en dicho instante y lo pedirá
mediante la difusión todos los demás de un mensaje que estará etiquetado en el tiempo.
Cuando el proceso que tiene el Token (Pj) deja su sección crítica, busca en el vector de
peticiones en orden j+1,j+2,...,n,1,2,...j-1 el primer valor de k tal que su etiqueta de tiempo de su
última petición del Token sea mayor que el valor almacenado en el Token para dicho proceso,
y entonces transfiere el Token de Pj a Pk.
public class Ricart
{ public static void main(String argv[])
{ Net net;
int max,me,i,j clock,token[],requests[];
boolean token_present, token_held;
try { net = new Red( (Integer.valueOf
(argv[0])).intValue()); }
catch (ArrayIndexOutOfBoundsException
e ) { net = new Red(1); }
me = net.mi_id();
max = net.numProcs();
if (me==0) token_present=true;
else token_present = false;
token_held=false;
clock=0;
token = new int[max];
requests = new int[max];
for(i=0;i<max;++i)
{ token[i]=0;
requests[i]=0;
}
/* Algorithm Body */
while(true)
{ if (!token_present)
{ ++clock;
net.broadcast(requests,clock);
token = net.wait_access();
token_present=true;
}
token_held=true;
/* Critical Section */
System.out.println("Process "+me+
" in Critical Section");
/* Critical Section */
token[me] = clock;
token_held=false;
requests = net.wait_request();
if (requests!=null) /* any request */
for(i=0;i<max;++i)
{ j = (i+me+1)%max;
if ( (requests[j] > token[j]) &&
(token_present) )
{ token_present=false;
net.send_access(token,j);
break;
}
}
}
}
}
7. public class Net extends jPVM
{ private int my_id,my_father,maxProc,
processes[];
private Net pvm;
public final static int Initialise = 0;
public final static int Request = 1;
public final static int Access = 2;
public final static int Any = -1;
public Net() { super(); }
public Net(int max)
{ String javaexe = "/jdk/bin/java";
String args[];
int max_array[],i;
args = new String [3];
args[0]= "-classpath";
args[1]= "/jdk/lib/classes.zip:”+
“/jPVM-v1.1.4: /Ricart_pvm";
args[2]="Ricart";
max_array = new int[1];
pvm = new Net();
my_id = pvm.mytid();
my_father = pvm.parent();
if (my_father==PvmNoParent)
{ maxProc = max;
processes = new int[maxProc];
processes[0]=my_id;
for(i=1;i<maxProc;++i)
{ pvm.spawn(javaexe,args,
PvmTaskDefault,"",max_array);
processes[i]=max_array[0];
}
max_array[0] = maxProc;
pvm.initsend(PvmDataDefault);
pvm.pkint(max_array, 1, 1);
pvm.pkint(processes, maxProc, 1);
for(i=1;i<maxProc;++i)
pvm.send(processes[i],Initialise);
}
else
{ pvm.recv(my_father,Initialise);
pvm.upkint(max_array, 1, 1);
maxProc = max_array[0];
processes = new int[maxProc];
pvm.upkint(processes, maxProc, 1);
}
}
public int mi_id()
{ int i;
for (i=0;i<maxProc;++i)
if (processes[i]==my_id) return i;
return -1;
}
public int numProcs()
{ return maxProc; }
public void broadcast(int requests[],int clock)
{ int i,ck[],me[];
me = new int[1];
me[0] = mi_id();
ck = new int[1];
ck[0]=clock;
pvm.initsend(PvmDataDefault);
pvm.pkint(me, 1, 1);
pvm.pkint(ck, 1, 1);
pvm.pkint(requests, maxProc, 1);
for (i=0;i<maxProc;++i)
if (processes[i]!=my_id)
pvm.send(processes[i],Request);
}
public int[] wait_request()
{ int rq[],info,ck[],me[];
info = pvm.nrecv(Any,Request);
if (info<=0) return null; /* No requests */
rq = new int[maxProc];
me = new int[1];
ck = new int[1];
pvm.upkint(me, 1, 1);
pvm.upkint(ck, 1, 1);
pvm.upkint(rq, maxProc, 1);
if (rq[me[0]] < ck[0]) rq[me[0]] = ck[0];
return rq;
}
public int[] wait_access()
{ int token[];
token = new int[maxProc];
pvm.recv(Any,Access);
pvm.pkint(token, maxProc, 1);
return token;
}
public void send_access(int token[],int j)
8. { pvm.initsend(PvmDataDefault);
pvm.pkint(token, maxProc, 1);
pvm.send(processes[j],Access);
}
}
Pérdida del Token: Algoritmo de Misra.
El principal problema de la solución de un Token circulando por un anillo es que la
pérdida de dicho Token implica un bloqueo del sistema. El Algoritmo de Misra fue
desarrollado para la detección y regeneración del Token en el caso de que éste se pierda.
Usaremos dos tokens, llamados ping y pong, y asociados con ellos dos números enteros
llamados nbping y nbpong respectivamente, iguales en valor absoluto pero de signo opuesto,
que almacenan el número de veces que ambos tokens se han encontrado en un mismo
proceso. Dichos números estarán entonces relacionados por la restricción nbping + nbpong
= 0. Inicialmente, ambos tokens están en un mismo proceso y por tanto, nbping = 1, nbpong
= -1. Cada proceso Pi almacena en una variable local, inicializadas a cero, el valor de nbping
o nbpong, asociado con el Token. Esencialmente, el algoritmo conserva la relación nbping +
nbpong = 0 y cuando esta relación se rompe, significa que el Token ping (o pong) se ha
perdido y hay que regenerarlo.
public class Misra
{ public final static int ping = 0;
public final static int pong = 1;
public static void main(String argv[])
{ Net net;
int max,me,left,right,
nbping=1,nbpong=-1,m=0;
boolean token_ping,token_pong;
try { net = new net( (Integer.valueOf
(argv[0])).intValue()); }
catch (ArrayIndexOutOfBoundsException
e) { net = new net(1); }
me = net.mi_id();
max = net.numProcs();
right = (me+1)%max;
left = me -1;
if (left<0) left = max -1;
if (me==0) { token_ping=true;
token_pong=true; }
else { token_ping = false;
token_pong = false; }
/* Algorithm Body */
while(true)
{ if (!token_ping) token_ping =
net.wait_token(left,ping,nbping);
if (!token_pong) token_pong =
net.wait_token(left,pong,nbpong);
if (token_ping) /* Critical Section */
{ System.out.println("Process "+me
+" in Critical Section ");
}
if ((token_ping) && (token_pong) )
{ ++ nbping;
-- nbpong;
net.send_token(right,ping,nbping);
net.send_token(right,pong,nbpong);
}
else
{ if (token_ping)
{ if (m == nbping) /* Token Lost */
{ ++nbping;
nbpong = -nbping;
net.send_token(right,pong,
nbpong); /* Regenerate it */
}
else { m = nbping; }
net.send_token(right,ping,nbping);
}
if (token_pong)
9. { if (m == nbpong) /* Token Lost */
{ ++nbpong;
nbping = -nbpong;
net.send_token(right,ping,
nbping); /* Regenerate it */
}
else { m = nbpong; }
net.send_token(right,pong,nbpong);
}
}
}
}
}
public class Net extends jPVM
{ private int my_id,my_father,
maxProc,processes[];
private Net pvm;
public final static int Initialise = 0;
public Net() { super(); }
public Net(int max)
{ String args[],javaexe = "/jdk/bin/java";
int i,max_array[];
args = new String [3];
args[0]= "-classpath";
args[1]= "/jdk/lib/classes.zip”+
“:/jPVM-v1.1.4:/Misra_pvm";
args[2]="Misra";
max_array = new int[1];
pvm = new Net();
my_id = pvm.mytid();
my_father = pvm.parent();
if (my_father==PvmNoParent)
{ maxProc = max;
processes = new int[maxProc];
processes[0]=my_id;
for(i=1;i<maxProc;++i)
{ pvm.spawn(javaexe,args,
PvmTaskDefault,"",max_array);
processes[i]=max_array[0];
}
max_array[0] = maxProc;
pvm.initsend(PvmDataDefault);
pvm.pkint(max_array, 1, 1);
pvm.pkint(processes, maxProc, 1);
for(i=1;i<maxProc;++i)
pvm.send(processes[i],Initialise);
else
{ pvm.recv(my_father,Initialise);
pvm.upkint(max_array, 1, 1);
maxProc = max_array[0];
processes = new int[maxProc];
pvm.upkint(processes, maxProc, 1);
}
}
public int mi_id()
{ int i;
for (i=0;i<maxProc;++i)
if (processes[i]==my_id) return i;
return -1;
}
public int numProcs() { return maxProc; }
public boolean wait_token(int i,int tag, int nb)
{ int info,numb[];
info = pvm.nrecv(processes[i],tag);
if (info<=0) return false;
numb = new int[1];
pvm.upkint(numb,1,1);
nb = numb[0];
return true;
}
public void send_token(int i,int tag, int nb)
{ int numb[];
numb = new int[1];
numb[0] = nb;
pvm.initsend(PvmDataDefault);
pvm.pkint(numb,1,1);
pvm.send(processes[i],tag);
}
}
Referencias
[1] M. Raynal, Distributed Algorithms and Protocols: John Wiley & Sons Ltd., 1988.
[2] R. Andrews, Concurrent Programming. Gregory: Benjamin/Cummings, 1991.
10. [3] Remote Method Invocation System. Sun Microsystems Inc. ,1996-1997.
[4] Getting Started using RMI. Sun Microsystems Inc. ,1996-1997.
[5] RMI and Object Serialisation. Sun Microsystems Inc. ,1996-1997.
[6] Q. H. Mahmond, Distributed Object Programming Using Java:Java Resource
Center,1997.