Está en la página 1de 157

Diego Rodrguez-Losada Gonzlez

Pablo San Segundo Carrillo



Programacin Avanzada,
Concurrente y Distribuida
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 2
Universidad Politcnica de Madrid -UPM


































Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 3
Universidad Politcnica de Madrid -UPM




PRLOGO 7
PARTE I. Desarrollo de una aplicacin distribuida y
concurrente en LINUX
1 1. . EDICIN, COMPILACIN, Y DEPURACIN DE UNA APLICACIN C/C++ BAJO LINUX 11
1.1. INTRODUCCIN 11
1.2. LOGIN EN MODO TEXTO 12
1.3. MANEJO DE ARCHIVOS Y DIRECTORIOS EN MODO TEXTO. 12
1.4. EL EDITOR DE TEXTO 15
1.5. DESARROLLO C/C++ EN LINUX EN MODO TEXTO 15
1.6. EL PROCESO DE CREACIN DE UN EJECUTABLE 16
1.7. LAS HERRAMIENTAS DE DESARROLLO 17
1.8. EL COMPILADOR GCC 17
1.9. MAKEFILE Y LA HERRAMIENTA MAKE 19
1.10. TIPOS DE ERROR 20
1.11. DEPURACIN DE LA APLICACIN. 21
1.12. CREACIN DE UN SCRIPT 22
1.13. DESARROLLO EN UN ENTORNO GRAFICO 23
1.14. EJERCICIO PRCTICO 24
1.15. EJERCICIO PROPUESTO 25
2 2. . INTRODUCCIN A LOS SISTEMAS DISTRIBUIDOS. COMUNICACIN POR SOCKETS 27
2.1. OBJETIVOS 27
2.2. SISTEMA DISTRIBUIDO 28
2.3. SERVICIOS DE SOCKETS EN POSIX 29
2.3.1 PROGRAMA CLIENTE 30
2.3.2 SERVIDOR 32
2.4. ENCAPSULACIN DE UN SOCKET EN UNA CLASE C++ 35
2.4.1 ENVO DE MLTIPLES MENSAJES 36
2.4.2 CONEXIONES MLTIPLES. 38
2.5. ESTRUCTURA DE FICHEROS 42
2.6. TRANSMITIENDO EL PARTIDO DE TENIS 44
2.6.1 CONEXIN 44
2.6.2 ENVO DE DATOS 45
2.7. EJERCICIOS PROPUESTOS 45
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 4
Universidad Politcnica de Madrid -UPM
3 3. . COMUNICACIONES Y CONCURRENCIA 47
3.1. INTRODUCCIN 47
3.2. REQUISITOS 49
3.3. FUNCIONAMIENTO DE GLUT 49
3.3.1 LANZANDO UN HILO 50
3.4. ESTRUCTURA DEL SERVIDOR 51
3.5. MLTIPLES CONEXIONES SIMULTANEAS 52
3.6. MOSTRAR LOS CLIENTES CONECTADOS 53
3.7. RECEPCIN COMANDOS MOVIMIENTO 55
3.8. GESTIN DESCONEXIONES 56
3.9. FINALIZACIN DEL PROGRAMA 56
3.10. EJERCICIO PROPUESTO 57
4 4. . COMUNICACIN Y SINCRONIZACIN INTERPROCESO 59
4.1. INTRODUCCIN 59
4.2. EL PROBLEMA DE LA SINCRONIZACION 60
4.3. COMUNICACIN INTERPROCESO 61
4.4. TUBERAS CON NOMBRE 62
4.5. MEMORIA COMPARTIDA 64
4.6. EJERCICIOS PROPUESTOS 68




PARTE II. Programacin avanzada
5 5. . PROGRAMACIN DE CDIGO EFICIENTE 73
5.1. INTRODUCCIN 73
5.2. MODOS DE DESARROLLO 77
5.3. TIPOS DE OPTIMIZACIONES 77
5.4. VELOCIDAD DE EJECUCIN 78
5.5. ALGUNAS TCNICAS 79
5.5.1 CASOS FRECUENTES 79
5.5.2 BUCLES 80
5.5.3 GESTIN DE MEMORIA 83
5.5.4 TIPOS DE DATOS 85
5.5.5 TCNICAS EN C++ 86
5.6. CASOS PRCTICOS 87
5.6.1 ALGORTMICA VS. MATEMTICAS 87
5.6.2 GENERACIN DE NMEROS PRIMOS 88
5.6.3 PRE-COMPUTACIN DE DATOS 90
5.7. OBTENIENDO PERFILES (PROFILING) DEL CDIGO 93
5.8. CONCLUSIONES 95
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 5
Universidad Politcnica de Madrid -UPM
6 6. . SERIALIZACIN DE DATOS 97
6.1. INTRODUCCIN 97
6.2. REPRESENTACIN OBJETOS EN MEMORIA 102
6.3. SERIALIZACIN EN C 103
6.3.1 CON FORMATO (TEXTO) 104
6.3.2 SIN FORMATO (BINARIA) 104
6.4. SERIALIZACIN EN C++ 107
6.4.1 CON FORMATO (TEXTO) 108
6.4.2 SIN FORMATO (BINARIA) 111
6.5. CONCLUSIONES 112
7 7. . BSQUEDAS EN UN ESPACIO DE ESTADOS MEDIANTE RECURSIVIDAD 113
7.1. INTRODUCCIN 113
7.2. BSQUEDA PRIMERO EN PROFUNDIDAD 115
7.2.1 TERMINOLOGA 116
7.2.2 ESTRUCTURAS DE DATOS 116
7.2.3 ANLISIS 117
7.3. BSQUEDA PRIMERO EN ANCHURA 119
7.4. METODOLOGA GENERAL DE RESOLUCIN DE UN PROBLEMA DE BSQUEDA MEDIANTE COMPUTACIN
120
7.5. IMPLEMENTACIN DE UNA BSQUEDA DFS MEDIANTE RECURRENCIA 121
7.5.1 LA PILA DE LLAMADAS 122
7.5.2 BSQUEDA DFS COMO RECURSIN 124
8 8. . EJECUCIN DISTRIBUIDA DE TAREAS 133
8.1. INTRODUCCIN 133
8.2. EL PROBLEMA DE LAS N-REINAS 134
8.2.1 HISTORIA 134
8.2.2 CARACTERSTICAS 135
8.2.3 2ESTRUCTURAS DE DATOS 136
8.3. IMPLEMENTACIN CENTRALIZADA 138
8.3.1 DESCRIPCIN 139
8.3.2 ESTRUCTURAS DE DATOS 140
8.3.3 CONTROL DE LA BSQUEDA 141
8.3.4 ALGORITMO DE BSQUEDA 145
8.4. IMPLEMENTACIN DISTRIBUIDA 147
8.4.1 ARQUITECTURA CLIENTE-SERVIDOR 147
8.4.2 PROTOCOLO DE COMUNICACIN 148
8.4.3 IMPLEMENTACIN DEL CLIENTE 148
8.5. IMPLEMENTACIN DEL SERVIDOR 153
8.5.1 COMUNICACIN CON EL CLIENTE 153




Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 6
Universidad Politcnica de Madrid -UPM



























Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 7
Universidad Politcnica de Madrid -UPM

PRLOGO
Generalmente la formacin en informtica de un ingeniero (industrial,
automtica, telecomunicaciones o similar) comienza por la programacin estructurada,
en lenguajes como C o Matlab, y luego se complementa con Programacin Orientada a
Objetos (POO) e Ingeniera del Software, con Anlisis y Diseo Orientados a Objetos,
UML, etc.
Sin embargo, existen una serie de tcnicas y tecnologas software que escapan
del alcance de los anteriores cursos. La programacin de tareas concurrentes, los
sistemas distribuidos, la programacin de cdigo eficiente o algortmica avanzada son
temas que quedan a menudo relegados, y sin embargo son muy necesarios en tareas
de ingeniera industrial, comunicaciones y similares.
Este libro trata de cubrir dichos aspectos, de una manera prctica y aplicada. La
primera parte desarrolla una aplicacin grfica distribuida: un tpico juego de
computador en red. En esta aplicacin se requiere el uso de comunicaciones por red
(con sockets), as como la utilizacin de tcnicas de programacin concurrente con
multi-proceso y multi-hilo, de una manera que esperamos que sea atractiva y
motivadora para el lector. El desarrollo se realiza en Linux (Posix), presentando una
introduccin al manejo bsico, desarrollo y depuracin con herramientas GNU como
g++, make y gdb. El cdigo de soporte para estos captulos se encuentra en
www.elai.upm.es
La segunda parte cubre algunos tpicos genricos avanzados como la
programacin de cdigo eficiente, la serializacin de datos, la recurrencia o la
computacin distribuida, tpicos que muchas veces estn ntimamente relacionados
con los anteriores.



Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 8
Universidad Politcnica de Madrid -UPM


































Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 9
Universidad Politcnica de Madrid -UPM












Parte I. Desarrollo de
una aplicacin
distribuida y
concurrente en LINUX
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 11
Universidad Politcnica de Madrid -UPM
1
1
.
.

EDICIN, COMPILACIN, Y
DEPURACIN DE UNA APLICACIN C/C++
BAJO LINUX
1.1. INTRODUCCIN
En este primer tema realizamos una aproximacin al SO operativo linux, y
fundamentalmente al desarrollo de aplicaciones en C/C++, desarrolladas, depuradas y
ejecutadas en un computador con Linux. Aunque el objetivo de este curso es el
aprendizaje de programacin concurrente y sistemas distribuidos, en este primer tema
nos ceiremos al trabajo de desarrollo convencional en linux, para aprender tanto el
desarrollo sin interfaz grafica de ventanas, como algunas de las herramientas graficas.
Tambin se manejaran algunos comandos o mandatos bsicos de linux para crear,
editar y manejar archivos, y se introducir el uso de las herramientas de desarrollo
bsico como son gcc, g++, make y gdb.
Este tema comienza por la descripcin de los comandos bsicos para trabajar
en modo texto, para despus desarrollar y depurar una pequea aplicacin ejemplo en
modo texto. Por ultimo, se trabajara en modo grafico, completando un cdigo ya
avanzado para terminar con el juego del tenis que funcione en modo local, para dos
jugadores, esto es, los dos jugadores utilizan el mismo teclado y la misma pantalla.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 12
Universidad Politcnica de Madrid -UPM

Figura 1-1. Objetivo del captulo: Desarrollo del juego del Tenis en modo local
1.2. LOGIN EN MODO TEXTO
Aunque el computador arranque en modo grafico, la primera parte de esta
prctica se va a desarrollar en modo texto. Para ello cmbiese del terminal grafico al
primer terminal de texto, mediante la correspondiente combinacin de teclas
(Ctrl+Alt+F1)
Entrar en la cuenta de usuario correspondiente. Consejo: Aunque dispongas de
la contrasea de administrador es absolutamente recomendable no utilizarla para
trabajar normalmente. En caso de que seas el administrador del sistema, crea una
cuenta de usuario normal para realizar la prctica.
Probar a realizar el login en los distintos terminales virtuales (saliendo luego
con el comando exit de los que no se vayan a utilizar)
1.3. MANEJO DE ARCHIVOS Y DIRECTORIOS EN MODO TEXTO.
Para familiarizarse con el manejo de archivos y directorios en linux se va a crear
la siguiente estructura de archivos, en la que los archivos de texto contienen el texto
Hola que tal:
/home/usuario/
|------------->carpeta1
| |------->subcarpeta11
| | |------->archivo11.txt
| |------->archivo1.txt
|------------->carpeta2
|------->archivo2.txt
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 13
Universidad Politcnica de Madrid -UPM

Utilizar y explorar los comandos y opciones siguientes:

Tabla 1-1. Comandos bsicos consola linux
Comando Accin Opciones
pwd muestra el directorio actual
ls muestra el contenido del
directorio actual
-a (muestra todos los
archivos, incluidos ocultos)
l, muestra detalles de los
archivos
mkdir [directorio] crea el directorio con el
nombre dado

cd [ruta] cambia al directorio que
indica la ruta
correspondiente

cat [fichero] concatena el fichero a salida
estndar
- significa entrada
estndar. Para crear un
archivo se puede
redireccionarla de la
siguiente forma cat -
>nombre_fichero.txt

chmod usuario+permiso
[fichero]
cambia los permisos
(r=read, w=write,
x=execute) a usuario (a=all,
o=others, u=user, g=group)

rm [archivo] Borra el archivo -r = borra recursivamente el
directorio seleccionado
(OJO, usar con mucha
precaucin)
cp [origen] [destino] Copia el archivo o archivos
origen al destino
seleccionado

mv [origen] [destino] Mueve el archivo o archivos
origen al destino
seleccionado
Tambin sirve para
renombrar un archivo
rmdir [directorio] Borra el directorio, que
previamente debe estar
vaco

exit o logout Termina la sesin (salir)





Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 14
Universidad Politcnica de Madrid -UPM
Tabla 1-2. Caracteres comodin (wildcars)
* Una cadena de caracteres
cualesquiera

? Un carcter cualquiera

Tabla 1-3. Directorios importantes
. Directorio actual Opcion
.. Directorio superior
cd Vuelve al directorio inicial
raiz del usuario
\home\usuario


Tabla 1-4. Ayuda
Comando funcin Opcion
man [comando] Muestra las paginas man
del comando seleccionado

comando Muestra una ayuda breve
del comando al que se
aplica
--help -h
info [comando] Muestra las paginas info
del comando al que se
aplica

whatis [comando] Busca en una base de datos
descripciones cortas del
comando



Tabla 1-5. Ayudas del shell bash
Teclas funcin Opcion
Tab Autocompletar, rellena el
nombre del comando o
archivo segn las posibles
opciones que conozca

Tab+Tab Muestra todas las opciones
que tiene autocompletar

Arrow Up Sube en la historia de
comandos

Arrow Down Baja en la historia de
comandos

Una vez creada la estructura, quitar el permiso de escritura al archivo11.txt e
intentar concatenarle la cadena Muy bien gracias. Volver a reinstaurar el permiso y
repetir la operacin.
Borrar primero el archivo2.txt y luego la carpeta2. Borrar a continuacin todo el
rbol de la carpeta1.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 15
Universidad Politcnica de Madrid -UPM

1.4. EL EDITOR DE TEXTO
Se va a utilizar el editor vi o vim para crear y modificar los archivos de
cdigo fuente necesarios, por ser el editor incluido por defecto en linux, y del que
conviene tener al menos unas nociones bsicas que nos permitan sacarnos de un
apuro en caso de necesidad.
Para crear un archivo nuevo en la carpeta actual teclear:
vi [fichero]
Si el archivo no existe lo crea y si existe lo abre para editar.
vi tiene dos modos de funcionamiento:
Modo comando: cada tecla realiza una funcin especfica (borrar,
mover) Este es el modo por defecto al arrancar el editor.
Modo insercin: cada tecla inserta el carcter correspondiente en el
texto. Para entrar en este modo se debe pulsar la tecla i y para salir de
l se debe pulsar Esc.
Operaciones bsicas
:w graba el archivo al disco
:q salir de editor
:q! salir del editor sin grabar los cambios (forzar la salida)
:wq grabar y salir
1.5. DESARROLLO C/C++ EN LINUX EN MODO TEXTO
Vamos a construir una aplicacin con dos ficheros fuente, que muestre por
pantalla una tabla de senos de varios ngulos. Para ello seguiremos los siguientes
pasos:
1. Verificar mediante pwd que se encuentra en el directorio de usuario
adecuado
2. Crear una carpeta pract1 que va a contener los archivos de la prctica,
y cambiar el directorio actual a la misma
3. Crear los archivos fuente siguientes:




Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 16
Universidad Politcnica de Madrid -UPM
/*
* archivo: principal.c
*/

#include <stdio.h>
#include misfunc.h
int main(void)
{
int i;
for(i=0;i<10;i++)
{
printf("Seno de %d es %f \n",i,seno(i));
}
return 1;
}

/*
* archivo: misfunc.h
*/

#ifndef _MIS_FUNC_H_INCLUDED
#define _MIS_FUNC_H_INCLUDED

float seno(float num);

#endif //_MIS_FUNC_H_INCLUDED

/*
* archivo: misfunc.c
*/

#include misfunc.h
#include <math.h>

float seno(float num)
{
return sin(num);
}

1.6. EL PROCESO DE CREACIN DE UN EJECUTABLE
El compilador genera un fichero o modulo objeto (binario) por cada uno de los
ficheros fuentes contenidos en el proyecto. Estos mdulos objeto no necesitan para
ser compilados ms que el fichero fuente de origen, aunque se referencien funciones
externas a dicho fichero.
El proceso de enlazado une los mdulos objeto resolviendo las referencias
entre ellos, as como las referencias a posibles bibliotecas o libreras externas al
proyecto, y generando el archivo ejecutable.
El sistema operativo es el encargado de unir el ejecutable con las libreras
dinmicas cuando el programa es cargado en memoria.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 17
Universidad Politcnica de Madrid -UPM

Figura 1-2. Proceso de creacin de un ejecutable
1.7. LAS HERRAMIENTAS DE DESARROLLO
Se van a utilizar a partir de ahora los compiladores y distintas herramientas.
Puede ser que en su sistema linux no vengan instaladas por defecto. Si ese es el caso,
debe de instalarlas. El gestor de aplicaciones o paquetes de su distribucin le ayudara a
hacerlo. En cualquier caso es importante remarcar que las herramientas de desarrollo
utilizadas son GNU, con licencia GPL, es decir son gratuitas y su instalacin es
totalmente legal. Si utiliza un sistema basado en Debian, la forma ms sencilla de
instalar estas herramientas seria:
sudo apt-get install build-essential
1.8. EL COMPILADOR GCC
El compilador utilizado en linux se llama gcc. La sintaxis adecuada para la
compilacin y linkado del anterior programa seria:
gcc o prueba principal.c misfunc.c lm
Fichero fuente A
.c, .cpp
Fichero fuente B
.c, .cpp
COMPILADOR
LINKADO
Biblioteca
esttica A
.a
Biblioteca
esttica B
.a

EJECUCION
Ejecutable
Modulo objeto A
.o
Modulo objeto B
.o

Libreras dinmicas
.so
Proceso en
ejecucin
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 18
Universidad Politcnica de Madrid -UPM
Ejecutar el programa mediante:
./prueba

Figura 1-3. Salida por pantalla de nuestra aplicacin
Comprobar con ls al los permisos de ejecucin del archivo
ls -al
La sintaxis es la siguiente:
gcc o [nombre_ejecutable] [ficheros_fuente] l[librera]
Realmente este comando ha realizado la compilacin y el linkado todo seguido,
de forma transparente para el usuario. Si se desea desacoplar las dos fases se realiza
de la siguiente manera:
Compilacin fichero a fichero :
gcc c principal.c
gcc c misfunc.c
(Ntese que aqu no es necesario especificar que se va a linkar con la librera
matemtica, ya que solo se esta compilando en un modulo objeto .o)
Compilacin de varios ficheros en la misma lnea
gcc c principal.c misfunc.c
Enlazado
gcc o prueba principal.o misfunc.o lm
Ntese que la opcin lm hace referencia a linkar l con la librera m o de
nombre completo libm.a o libm.so que es la librera estndar matemtica en sus
versiones estticas o dinmicas. Buscar con find / -name libm.*
Eliminar archivos objeto
rm *.o
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 19
Universidad Politcnica de Madrid -UPM
1.9. MAKEFILE Y LA HERRAMIENTA MAKE
Hemos visto un ejemplo sencillo, en el que teclear el comando para compilar y
crear el ejecutable es muy sencillo. Sin embargo este procedimiento puede ser largo y
tedioso en el caso de grandes proyectos con muchos ficheros fuente y mltiples
opciones de compilacin.
Por ello existe una herramienta, el make, que haciendo uso de la
configuracin de un fichero denominado Makefile (sin extensin, tpicamente
situado en la carpeta en la que tenemos el proyecto), se encarga de todo este trabajo.
Entre otras cosas, se encarga de realizar la comprobacin de que ficheros han sido
modificados, para solo compilar dichos archivos, ahorrando mucho tiempo al usuario.
La sintaxis del Makefile es muy potente y compleja, por lo que aqu se realiza
solamente la descripcin de una configuracin bsica para el proyecto de esta practica.
Para ello crear y editar con el vi el archivo siguiente:
#Makefile del proyecto

CC=gcc
CFLAGS= -g
LIBS= -lm
OBJS=misfunc.o principal.o

prueba: $(OBJS)
$(CC) $(OBJS) $(LIBS) o prueba

principal.o: principal.c misfunc.h
$(CC) c principal.c
misfunc.o: misfunc.c misfunc.h
$(CC) c principal.c

clean:
rm f *.o prueba

Los comentarios en un Makefile se preceden de #
#Makefile del proyecto
El Makefile permite la definicin de variables, mediante una simple asignacin.
En la primera parte del Makefile establecemos algunas variables de conveniencia. Se
define la cadena CC que nos definir el compilador que se va a usar
CC=gcc
Se define la cadena CFLAGS que nos definir las opciones de compilacin, en
este caso habilita la informacin que posibilita la depuracin del ejecutable
CFLAGS= -g
La cadena LIBS almacena las libreras con las que hay que linkar para generar el
ejecutable
LIBS= -lm
La cadena OBJS define los mdulos objeto que componen el ejecutable. Aqu se
deben listar todos los archivos objeto necesarios, si nos olvidamos alguno, el enlazador
encontrara un error.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 20
Universidad Politcnica de Madrid -UPM
OBJS=misfunc.o principal.o
A partir de aqu comienzan las reglas, cada regla tiene la siguiente estructura:
objetivo (target): prerequisitos o dependencias
comando
Cada regla mira si los prerrequisitos o dependencias han sido modificados, y
caso de que lo hayan sido, construye el objetivo utilizando el comando. La siguiente
cadena establece la construccin del ejecutable a partir de los objetos, y linkando con
las libreras LIBS y generando el ejecutable prueba
prueba: $(OBJS)
$(CC) $(OBJS) $(LIBS) o prueba
Que es totalmente equivalente a:
prueba: misfunc.o principal.o
gcc misfunc.o principal.o -lm o prueba
Que significa: Si alguno o ambos de los ficheros objeto han cambiado, se tiene
que volver a linkar el ejecutable prueba, a partir de los ficheros objeto y enlazando
con la librera matemtica lm.
A su vez especificamos la compilacin de cada uno de los mdulos objeto:
principal.o: principal.c misfunc.h
$(CC) c principal.c
misfunc.o: misfunc.c misfunc.h
$(CC) c principal.c
Las dos primeras lneas, analizan si han sido modificados principal.c o
misfunc.h, y en su caso, significa que hay que volver a compilar el modulo objeto a
partir del cdigo fuente.
El Makefile analiza las dependencias recursivas, esto es, si el fichero
misfunc.h ha sido modificado, primero compilara con las dos ultimas reglas los
ficheros objeto principal.o y misfunc.o. Como estos ficheros han sido modificados,
invocara a su vez a la regla superior, linkando y obteniendo el ejecutable prueba.
La regla clean (make clean) elimina los objetos y el ejecutable
clean:
rm f *.o prueba
Lo que significa que si tecleamos en la lnea de comandos:
make clean
en vez de construir el ejecutable, se borran los archivos binarios temporales y el
ejecutable
1.10. TIPOS DE ERROR
Existen dos tipos de errores en un programa, errores en tiempo de ejecucin y
errores en tiempo de compilacin. Vamos a ver la diferencia entre ambos:
Errores en tiempo de compilacin. Son errores, principalmente de
sintaxis. El compilador los detecta y nos informa de ello, no produciendo
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 21
Universidad Politcnica de Madrid -UPM
un ejecutable. Vamos a provocar un error de este estilo. Realizamos el
cambio:
printf("Seno de %d es %f \n",i,seno(float(i));
Quitamos el punto y coma del final:
printf("Seno de %d es %f \n",i,seno(float(i))
Y compilamos de nuevo. Nos saldr un mensaje informndonos del error
sintctico y en que lnea se produce.
Errores en tiempo de ejecucin. Tambin llamados errores lgicos o
run-time error. Es un error que no es capaz de detectar el compilador
porque no es un fallo en la sintaxis, pero que produce un error al
ejecutar el programa por un fallo lgico. Por ejemplo, la divisin por
cero, sintcticamente no es un error en el programa, pero al realizar la
divisin, se produce un error en tiempo de ejecucin. En todo caso, si el
compilador detecta la divisin por cero (por ejemplo al hacer int a=3/0;)
puede emitir un warning.
int a=0;
int b=3;
int c=b/a;
Compilamos este programa y lo ejecutamos. El programa fallara y nos saldr un
mensaje informndonos de ello. Tambin cabe la posibilidad de que un fallo en el
cdigo del programa produzca un comportamiento no deseado, pero que este no
resulte en un fallo fatal y el programa finalice bruscamente.
1.11. DEPURACIN DE LA APLICACIN.
Para depurar un programa se debe ejecutar el depurador seguido del nombre
del ejecutable (que debe haber sido creado con la opcin g)
gdb prueba
El depurador arranca y muestra un nuevo prompt (gdb) que espera a recibir
los comandos adecuados para ejecutar el programa paso a paso o como se le indique.
Los comandos que puede recibir este prompt se dividen en distintos grupos,
mostrados por el comando
(gdb) help
Si se desea ver los comandos que pertenecen a cada grupo se debe escribir
(p.ej. para ver los comandos que permiten gestionar la ejecucin del programa)
(gdb) help [nombre grupo] (ejemplo: running )
Y para ver la ayuda de un comando en particular:
(gdb) help [comando]
Caben destacar por su utilidad los siguientes comandos pertenecientes a los
grupos:
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 22
Universidad Politcnica de Madrid -UPM
Tabla 1-6. Comandos bsicos de gdb
Grupo Comando Accin
running run comienza la depuracin del
programa
step ejecuta un paso, entrando en
funciones
next ejecuta un paso, sin entrar en
funciones
finish termina la ejecucin del programa
continue continua la ejecucin del programa,
hasta el siguiente breakpoint
data display [exp] muestra el contenido de la variable
exp cada vez que el programa se
para
undisplay [exp] quita el comportamiento anterior
print [exp] Muestra el contenido de exp
breakpoint break [num_linea] inserta un punto de parada o
Breakpoint en la lnea
correspondiente
clear [num_linea] Eliminan el breakpoint de la lnea
correspondiente
delete break Pregunta si se desea eliminar todos
los breakpoints
status info [opcion] Muestra informacin acerca de la
opcin elegida, por ejemplo info
break muestra los breakpoints.
ninguno quit sale del debugger
Realizar la depuracin del programa anterior, viendo el valor de las posibles
variables, ejecutando paso a paso.
1.12. CREACIN DE UN SCRIPT
Se puede crear un archivo de texto que sirva para ejecutar una serie de
comandos consecutivos en el shell, en lo que se llama un script. Para ver un ejemplo se
va a crear un script que muestre el nombre de la carpeta actual y a continuacin
muestre el contenido de dicha carpeta, para termina ejecutando el programa prueba.
Para ello creamos un archivo:
vi miscript
echo La carpeta actual es
pwd
echo Y contiene lo siguiente
ls
Si intentamos ejecutar el script, nos dir que no tiene permisos de ejecucin.
Para eso realizamos el cambio:
chmod a+x miscript
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 23
Universidad Politcnica de Madrid -UPM
Debe de quedar claro que con un script no se tiene cdigo mquina, ni se
compila, ni se inicia un proceso. Simplemente se la pasan al shell unos comandos en
lotes.
1.13. DESARROLLO EN UN ENTORNO GRAFICO
Existen distintas herramientas para el desarrollo C/C++ en linux, entre las que
se podran destacar el Kdevelop, Anjuta, o Eclipse. Para el desarrollo de nuestra
aplicacin hemos optado por Geany, que realmente es ms un editor de texto que un
entorno de desarrollo, pero sin embargo tiene las caractersticas necesarias para
nuestra aplicacin. Geany dispone de resaltado en colores del cdigo, y de gestion de
la compilacin mediante Makefile, que permite mediante la pulsacin de F9 la
invocacin automtica de Makefile (aunque el fichero Makefile lo debemos proveer
nosotros), as como la gestin de los posibles errores de compilacin, con la posibilidad
de saltar a la lnea del error simplemente haciendo doble click en el mensaje de error.

Figura 1-4. El editor Geany
Si se desea instalar el editor, as como las libreras necesarias de Glut, es
necesario:
sudo apt-get install geany glutg3-dev

Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 24
Universidad Politcnica de Madrid -UPM
1.14. EJERCICIO PRCTICO
Se suministra en una carpeta un conjunto de ficheros de cdigo fuente, con
algunas clases de C++, necesarias para el desarrollo del juego del Tenis,
fundamentalmente las clases Mundo, Esfera, Plano, Raqueta, y la clase auxiliar
Vector2D. Todas las clases estn completas, exceptuando la clase Mundo.
#include "Vector2D.h"
class Esfera
{
public:
Esfera();
virtual ~Esfera();

Vector2D centro;
Vector2D velocidad;
float radio;

void Mueve(float t);
void Dibuja();
};

#include "Esfera.h"
#include "Vector2D.h"
class Plano
{
public:
bool Rebota(Esfera& e);
bool Rebota(Plano& p);
void Dibuja();
Plano();
virtual ~Plano();

float x1,y1;
float x2,y2;
float r,g,b;
protected:
float Distancia(Vector2D punto, Vector2D *direccion);
};

#include "Plano.h"
#include "Vector2D.h"
class Raqueta : public Plano
{
public:
void Mueve(float t);
Raqueta();
virtual ~Raqueta();

Vector2D velocidad;
};

class CMundo
{
public:
void Init();
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 25
Universidad Politcnica de Madrid -UPM
CMundo();
virtual ~CMundo();

void InitGL();
void OnKeyboardDown(unsigned char key, int x, int y);
void OnTimer(int value);
void OnDraw();
};
Se solicita al alumno que complete la clase Mundo para obtener el juego del
tenis funcional. Se debe escribir un Makefile para la construccin del ejecutable.
1.15. EJERCICIO PROPUESTO
El alumno debe de completar el juego con alguna funcionalidad extra, como por
ejemplo, que cada una de las raquetas sea capaz de disparar un disparo, que cuando
impacta al oponente lo inmoviliza, o disminuye el tamao de su raqueta.
Tambin se propone el desarrollo de cualquier otro juego de complejidad
similar.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 27
Universidad Politcnica de Madrid -UPM
2
2
.
.

INTRODUCCIN A LOS SISTEMAS
DISTRIBUIDOS. COMUNICACIN POR
SOCKETS

2.1. OBJETIVOS
En el captulo anterior se ha desarrollado el juego bsico del tenis en el que dos
jugadores, compartiendo el mismo teclado y el mismo monitor, cada uno con distintas
teclas puede controlar su raqueta arriba y abajo para jugar la partida. El objetivo final
es la consecucin del juego totalmente distribuido, es decir, cada jugador podr jugar
en su propio ordenador, con su teclado y su monitor, y los dos ordenadores estarn
conectados por la red.
En este captulo se presenta una introduccin a los sistemas distribuidos, los
servicios proporcionados en POSIX para el manejo de Sockets, que son los conectores
necesarios (el recurso software) para la comunicacin por la red, y su uso en nuestra
aplicacin. No pretende ser una gua exhaustiva de dichos servicios sino una
descripcin prctica del uso ms sencillo de los mismos, y como integrarlos en nuestra
aplicacin para conseguir nuestros objetivos. De hecho, en el curso del captulo se
desarrolla una clase C++ que encapsula los servicios de Sockets, permitiendo al usuario
un uso muy sencillo de los mismos que puede valer para numerosas aplicaciones,
aunque obviamente no para todo.
Como primera aproximacin al objetivo final se va a realizar en este captulo la
retransmisin del partido de tenis por la red. Esto es, los dos jugadores van a seguir
jugando en la misma mquina con el mismo teclado, pero sin embargo otro usuario
desde otra mquina podr conectarse remotamente a travs de la red a la mquina y a
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 28
Universidad Politcnica de Madrid -UPM
la aplicacin en la que juegan los jugadores (el servidor), y esta le enviara
constantemente los datos necesarios para que la mquina remota (el cliente) pueda
simplemente dibujar el estado actual de la partida. De esta forma lo que se permite es
que los clientes sean meros espectadores de la partida. Inicialmente se plantea la
solucin para un nico espectador, y finalmente se aborda la solucin para mltiples
espectadores. No obstante esta ltima requerir para su correcto funcionamiento el
uso de programacin concurrente (hilos) que se abordara en sucesivos captulos.

Figura 2-1. Objetivo del captulo: Retransmisin de la partida de tenis a ordenadores
remotos conectados a travs de la red al servidor
En sucesivos captulos se completar el desarrollo del juego distribuido
haciendo que los jugadores puedan realmente jugar en dos mquinas distintas, que
transmitirn los comandos de los jugadores por la misma red al servidor, para que este
los ejecute sin necesidad de tener a dichos jugadores utilizando el mismo teclado fsico
de la mquina en la que corre el servidor.
2.2. SISTEMA DISTRIBUIDO
Llamaremos sistema distribuido a una solucin software cuya funcionalidad es
repartida entre distintas mquinas, teniendo cada mquina su propio procesador (o
propios procesadores), su propia memoria, y corriendo su propio sistema operativo.
Adems, no es necesario que las mquinas sean iguales, ni ejecuten el mismo SO ni el
mismo software. Las mquinas estarn interconectadas por una red que sirve para el
intercambio de mensajes entre dichas mquinas.









RED
Retransmisin
partido
N posibles
clientes
que se
conectan
al servidor
para ver el
partido
Servidor, en el que
juegan los dos
jugadores con el
mismo teclado
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 29
Universidad Politcnica de Madrid -UPM
2.3. SERVICIOS DE SOCKETS EN POSIX
A continuacin se presenta el cdigo de un programa cliente y de un programa
servidor, para describir breve y generalmente los servicios de sockets implicados. Este
cdigo es prcticamente el ms bsico posible, sin comprobacin de errores. El
funcionamiento ser como sigue: Primero se arranca el programa servidor, que
inicializa el socket servidor y se queda a la espera de una conexin. A continuacin se
debe lanzar el programa cliente que se conectar al servidor. Una vez que ambos estn
conectados, el servidor enviara al cliente unos datos (una frase) que el cliente
mostrar por pantalla, y a finalmente terminarn ambos programas. El funcionamiento
en lneas generales queda representado en la siguiente figura:



Figura 2-2. Conexin sockets
Servidor Cliente
Se crea el socket
de conexin
Se le asigna una
direccin y un
puerto y se pone
a la escucha
El socket de
conexin se
queda bloqueado
a la espera
Aceptando una
conexin
socket()
bind()
listen()
accept()
Se crea el socket
de conexin y
comunicacin (es
el mismo)
Se conecta a la
direccin del
servidor
socket()
connect()
Comunicacin Comunicacin
send()
recv()
send()
recv()
TCP/IP
Cierre
shutdown()
close()
Cierre
shutdown()
close()
Cuando el cliente se conecta al
socket de conexin que esta
Aceptando, este devuelve un
socket de conexin que es con el
que se realiza la comunicacin
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 30
Universidad Politcnica de Madrid -UPM
2.3.1 Programa cliente
El cdigo del programa cliente bsico es el siguiente:
//includes necesarios para los sockets
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define INVALID_SOCKET -1

int main()
{
//declaracion de variables
int socket_conn;//the socket used for the send-receive
struct sockaddr_in server_address;
char address[]="127.0.0.1";
int port=12000;

// Configuracion de la direccion IP de connexion al servidor
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = inet_addr(address);
server_address.sin_port = htons(port);

//creacion del socket
socket_conn=socket(AF_INET, SOCK_STREAM,0);

//conexion
int len= sizeof(server_address);
connect(socket_conn,(struct sockaddr *) &server_address,len);

//comunicacion
char cad[100];
int length=100; //read a maximum of 100 bytes

int r=recv(socket_conn,cad,length,0);
std::cout<<"Rec: "<<r<<" contenido: "<<cad<<std::endl;

//cierre del socket
shutdown(socket_conn, SHUT_RDWR);
close(socket_conn);
socket_conn=INVALID_SOCKET;
return 1;
}
A continuacin se describe brevemente el programa:
Las primeras lneas son algunos #includes necesarios para el manejo de
servicios de sockets. En el caso de querer utilizar los sockets en Windows, el fichero de
cabecera y la librera con la que hay que enlazar se podran establecer con las lneas:
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib")
En las primeras lneas del main() se declaran las variables necesarias para el
socket.
int socket_conn;//the socket used for the send-receive
struct sockaddr_in server_address;
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 31
Universidad Politcnica de Madrid -UPM
La primera lnea declara el descriptor del socket (de tipo entero) que se utiliza
tanto para la conexin como para la comunicacin. La segunda declaracin declara una
estructura de datos que sirve para almacenar la direccin IP y el nmero de puerto del
servidor y la familia de protocolos que se utilizaran en la comunicacin. La asignacin
de esta estructura a partir de la IP definida como una cadena de texto y el puerto
definido como un entero se hace como sigue:
char address[]="127.0.0.1";
int port=12000;
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = inet_addr(address);
server_address.sin_port = htons(port);
Ntese que la IP que utilizaremos ser la 127.0.0.1. Esta IP es una IP especial
que significa la mquina actual (direccin local). Realmente ejecutaremos nuestras 2
aplicaciones (cliente y servidor) en la misma mquina, utilizando la direccin local de la
mquina. No obstante esto se puede cambiar. Para ejecutar el servidor en una
mquina que tiene la IP 192.168.1.13 por ejemplo, basta poner dicha direccin en
ambos programas, ejecutar el servidor en esa mquina, y el cliente en cualquier otra
(que sea capaz de enrutar mensajes hacia esa IP).
A continuacin se crea el socket, especificando la familia de protocolos (en este
caso protocolo de Internet AF_INET) y el tipo de comunicacin que se quiere
emplear (fiable=SOCK_STREAM, no fiable=SOCK_DGRAM). En nuestro caso
utilizaremos siempre comunicacin fiable.
//creacion del socket
socket_conn=socket(AF_INET, SOCK_STREAM,0);
Esta funcin generalmente no produce errores, aunque en algn caso podra
hacerlo. Como regla general conviene comprobar su valor, que ser igual a -1
(INVALID_SOCKET) si la funcin ha fallado. A continuacin se intenta la conexin
con el socket especificado en la direccin del servidor.
//conexion
int len= sizeof(server_address);
connect(socket_conn,(struct sockaddr *) &server_address,len);
Esta funcin connect() fallar si no esta el servidor preparado por algn
motivo (lo que sucede muy a menudo). Por lo tanto es ms que conveniente
comprobar el valor de retorno de connect() para actuar en consecuencia. Se podra
hacer algo como:
if(connect(socket_conn,(struct sockaddr *) &server_address,len)!=0)
{
std::cout<<"Client could not connect"<<std::endl;
return -1;
}
Si la conexin se realiza correctamente, el socket ya esta preparado para enviar
y recibir informacin. En este caso hemos decidido que va a ser el servidor el que enva
datos al cliente. Esto es un convenio entre el cliente y el servidor, que adopta el
programador cuando disea e implementa el sistema. Como el cliente va a recibir
informacin, utilizamos la funcin de recepcin. En esta funcin, se le suministra un
buffer en el que guarda la informacin y el nmero de bytes mximo que se espera
recibir. La funcin recv() se bloquea hasta que el servidor enve alguna informacin.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 32
Universidad Politcnica de Madrid -UPM
Dicha informacin puede ser menor que el tamao mximo suministrado. El valor de
retorno de la funcin recv() es el numero de bytes recibidos.
//comunicacion
char cad[100];
int length=100; //read a maximum of 100 bytes

int r=recv(socket_conn,cad,length,0);
std::cout<<"Rec: "<<r<<" contenido: "<<cad<<std::endl;
Por ultimo se cierra la comunicacin y se cierra el socket.
shutdown(socket_conn, SHUT_RDWR);
close(socket_conn);
socket_conn=INVALID_SOCKET;

2.3.2 Servidor
El cdigo del programa servidor es algo ms complejo, ya que debe realizar ms
tareas. La principal caracterstica es que se utilizan 2 sockets diferentes, uno para la
conexin y otro para la comunicacin. El servidor comienza enlazando el socket de
conexin a una direccin IP y un puerto (siendo la IP la de la mquina en la que corre el
servidor), escuchando en ese puerto y quedando a la espera Accept de una
conexin., en estado de bloqueo. Cuando el cliente se conecta, el Accept se
desbloquea y devuelve un nuevo socket, que es por el que realmente se envan y
reciben datos.
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <iostream>
#define INVALID_SOCKET -1

int main()
{
int socket_conn=INVALID_SOCKET;//used for communication
int socket_server=INVALID_SOCKET;//used for connection
struct sockaddr_in server_address;
struct sockaddr_in client_address;

// Configuracion de la direccion del servidor
char address[]="127.0.0.1";
int port=12000;
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = inet_addr(address);
server_address.sin_port = htons(port);

//creacion del socket servidor y escucha
socket_server = socket(AF_INET, SOCK_STREAM, 0);
int len = sizeof(server_address);

int on=1; //configuracion del socket para reusar direcciones
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 33
Universidad Politcnica de Madrid -UPM

//escucha
bind(socket_server,(struct sockaddr *) &server_address,len);
// Damos como maximo 5 puertos de conexion.
listen(socket_server,5);

//aceptacion de cliente (bloquea hasta la conexion)
unsigned int leng = sizeof(client_address);
socket_conn = accept(socket_server,
(struct sockaddr *)&client_address, &leng);

//notese que el envio se hace por el socket de communicacion
char cad[]="Hola Mundo";
int length=sizeof(cad);
send(socket_conn, cad, length,0);

//cierre de los dos sockets, el servidor y el de comunicacion

shutdown(socket_conn, SHUT_RDWR);
close(socket_conn);
socket_conn=INVALID_SOCKET;

shutdown(socket_server, SHUT_RDWR);
close(socket_server);
socket_server=INVALID_SOCKET;

return 1;
}
Hasta la creacin del socket del servidor, el programa es similar al cliente,
quitando la excepcin de que se declaran los 2 sockets, el de conexin y el de
comunicacin. La primera diferencia son las lneas:
//configuracion del socket para reusar direcciones
int on=1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
Estas lneas se utilizan para que el servidor sea capaz de re-usar la direccin y el
puerto que han quedado abiertos sin ser cerrados correctamente en una ejecucin
anterior. Cuando esto sucede, el sistema operativo deja la direccin del socket
reservada y por tanto un intento de utilizarla para un servidor acaba en fallo. Con estas
lneas podemos configurar y habilitar que se re-usen las direcciones previas.
La segunda diferencia es que en vez de intentar la conexin con connect(),
el servidor debe establecer primero en que direccin va a estar escuchando su socket
de conexin, lo que se establece con las lneas:
int len = sizeof(server_address);
bind(socket_server,(struct sockaddr *) &server_address,len);
// Damos como maximo una cola de 5 conexiones.
listen(socket_server,5);
La funcin bind() enlaza el socket de conexin con la IP y el puerto
establecidos anteriormente. Esta funcin tambin es susceptible de fallo. El fallo ms
comn es cuando se intenta enlazar el socket con una direccin y puerto que ya estn
ocupados por otro socket. En este caso la funcin devolver -1, indicando el error. A
veces es posible que si no se cierra correctamente un socket (por ejemplo, si el
programa finaliza bruscamente), el SO piense que dicho puerto esta ocupado, y al
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 34
Universidad Politcnica de Madrid -UPM
volver a ejecutar el programa, el bind() falle, no teniendo sentido continuar la
ejecucin. La gestin bsica de este error podra ser:
if(0!=bind(socket_server,(struct sockaddr *) &server_address,len))
{
std::cout<<Fallo en el Bind()<<std::endl;
return -1;
}
La funcin listen() permite definir cuantas peticiones de conexin al
servidor sern encoladas por el sistema. Ntese que esto no significa que realmente se
atiendan las peticiones de conexin. Es el usuario a travs de la funcin accept() el
que acepta una conexin. El numero de conexiones depender de cuantas veces
ejecute el programa dicho accept().
//aceptacion de cliente (bloquea hasta la conexion)
unsigned int leng = sizeof(client_address);
socket_conn = accept(socket_server,
(struct sockaddr *)&client_address, &leng);
Lo ms importante del accept() es que en su modo normal bloquea el
programa hasta que realmente se realiza la conexin por parte del cliente. A esta
funcin se le suministra el socket de conexin, y devuelve el socket que realmente se
utilizar para la comunicacin. Si algo falla en la conexin, la funcin devolver -1, lo
que corresponde a nuestra definicin de socket invalido INVALID_SOCKET, lo que
podemos comprobar:
if(socket_conn==INVALID_SOCKET)
{
std::cout<<Error en el accept<<std::endl;
return -1;
}
Una vez que se ha realizado la conexin, la comunicacin se hace por el nuevo
socket, utilizando las mismas funciones de envo y recepcin que se podran usar en el
cliente. Como en el ejemplo actual, por convenio hemos establecido que ser el
servidor el que enva un mensaje al cliente, el cdigo siguiente enva el mensaje Hola
Mundo por el socket:
char cad[]="Hola Mundo";
int length=sizeof(cad);
//notese que el envio se hace por el socket de communicacion
send(socket_conn, cad, length,0);
La funcin send() tambin puede fallar, si el socket no esta correctamente
conectado (se ha desconectado el cliente por ejemplo). La funcin devuelve el nmero
de bytes enviados correctamente o -1 en caso de error. Tpicamente, si la conexin es
buena, la funcin devolver como retorno un valor igual a length, aunque tambin
es posible que no consiga enviar todos los datos que se le ha solicitado. Una solucin
completa debe contemplar esta posibilidad y reenviar los datos que no han sido
enviados. No obstante y por simplicidad, realizamos ahora una gestin sencilla de este
posible error:
if(lenght!=send(socket_conn, cad, length,0))
{
std::cout<<Fallo en el send()<<std::endl;
return -1;
}
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 35
Universidad Politcnica de Madrid -UPM
El cierre de los sockets se realiza de la misma manera que en el cliente,
exceptuando que se deben cerrar correctamente los 2 sockets, el de conexin y el de
comunicacin. La salida por pantalla al ejecutar las aplicaciones (primero arrancar el
servidor y luego el cliente) debera ser (en el lado del cliente):
Rec: 11 contenido: Hola Mundo
Ntese que los bytes recibidos son 11 porque incluyen el carcter nulo \0 de
final de la cadena
2.4. ENCAPSULACIN DE UN SOCKET EN UNA CLASE C++
La API vista en el apartado anterior es C, y aparte de las funciones descritas,
tiene otras funcionalidades que no se vern en este curso. Es una prctica habitual
cuando se puede desarrollar en C++ encapsular la funcionalidad de la API en una clase
o conjunto de clases que oculten parcialmente los detalles ms complejos, facilitando
la tarea al usuario. As, por ejemplo, las Microsoft Fundation Classes (MFC) tienen sus
clases CSocket y CAsyncSocket para estas tareas. Tambin se pueden
encontrar en Internet numerosos envoltorios (wrappers) de C++ para los sockets en
linux.
Vamos a desarrollar una clase C++ que encapsule la funcionalidad vista en los
programas anteriores. Es comn encontrar, bajo una perspectiva estricta de
Programacin Orientada a Objetos (POO) que el cliente y servidor se implementan en
clases separadas. No obstante, se adopta ahora un enfoque ms sencillo con una sola
clase, que utiliza diferentes mtodos en caso del cliente y del servidor.
EJERCICIO: Desarrollar la clase Socket, de acuerdo con la cabecera siguiente, para
que encapsule los detalles de implementacin anteriores.
//includes necesarios
class Socket
{
public:
Socket();
virtual ~Socket();

// 0 en caso de exito y -1 en caso de error
int Connect(char ip[],int port); //para el cliente
int InitServer(char ip[],int port);//para el servidor

//devuelve un socket, el empleado realmente para la comunicacion
//el socket devuelto podria ser invalido si el accept falla
Socket Accept();//para el servidor
void Close();//para ambos

//-1 en caso de error,
// numero de bytes enviados o recibidos en caso de exito
int Send(char cad[],int length);
int Receive(char cad[],int length);

private:
int sock;
};
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 36
Universidad Politcnica de Madrid -UPM
El cdigo del servidor se ver simplificado a:
#include <iostream>
#include "Socket.h"

int main()
{
Socket servidor;
servidor.InitServer("127.0.0.1",12000);

Socket conn=servidor.Accept();

char cad[]="Mensaje";
int length=sizeof(cad);

conn.Send(cad,length);

conn.Close();
servidor.Close();

return 1;
}
Y el cdigo del cliente:
#include "Socket.h"
#include <iostream>

int main()
{
Socket client;
client.Connect("127.0.0.1",12000);

char cad[1000];
int length=1000;

int r=client.Receive(cad,length);
std::cout<<"Recibidos: "<<r<<" contenido: "<<cad<<std::endl;

client.Close();

return 1;
}


2.4.1 Envo de mltiples mensajes
Obviamente, la comunicacin no necesariamente se reduce al envo de un
mensaje. Supngase que el servidor lo que quiere enviar es un mensaje 10 veces.
Aunque el mensaje podra ser distinto cada vez, para realizar la prueba podemos
enviar 10 veces el mismo saludo, quedando el cdigo del servidor como sigue:

char cad[]="Hola Mundo";
int length=sizeof(cad);


Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 37
Universidad Politcnica de Madrid -UPM
for(int i=0;i<10;i++)
{
int err=conn.Send(cad,length);
if(err!=length)
{
std::cout<<"Send error"<<std::endl;
break;
}
}
En el lado del cliente podramos conocer que nos van a enviar 10 mensajes y
realizar un bucle similar:
char cad[1000];
int length=1000;
for(int i=0;i<10;i++)
{
int r=client.Receive(cad,length);
if(r<0)
{
std::cout<<Error en la recepcion<<std::endl;
break;
}
else
std::cout<<"Rec: "<<r<<" contenido: "<<cad<<std::endl;
}
No obstante al ejecutar estos programas podramos obtener una salida como la
siguiente en el cliente:
Rec: 11 Hola Mundo
Rec: 33 Hola Mundo
Rec: 66 Hola Mundo
Error en la recepcion
Esto se debe a que el servidor enva seguido y todo lo rpido que le permite el
bucle for los mensajes, que llegan al cliente. Si el cliente solicita recibir un mensaje de
una longitud mxima de 1000 caracteres puede leer efectivamente ms de un mensaje
enviado por el servidor. Al sacarlos por pantalla no aparece Hola Mundo Hola Mundo
porque hay un terminador de cadena \0 entre ambos.
En este punto caben dos alternativa como posibles soluciones a este problema:
1. El servidor enva datos mucho ms despacio de lo que recibe el cliente.
En este caso no se suele presentar ningn problema. Supngase que el
servidor espera 1 segundo antes de enviar el siguiente mensaje. El
cliente ir recibiendo los mensajes por separado sin problemas:
char cad[]="Hola Mundo";
int length=sizeof(cad);

for(int i=0;i<10;i++)
{
usleep(1000000);//espera 1 segundo
int err=conn.Send(cad,length);
if(err!=length)
{
std::cout<<"Send error"<<std::endl;
break;
}
}
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 38
Universidad Politcnica de Madrid -UPM
2. Existe un convenio entre el cliente y el servidor que especifica como son
los mensajes, para que el cliente sepa que es lo que va a recibir y como
lo tiene que interpretar. Este convenio puede consistir en especificar
una longitud fija para los mensajes, o en establecer un carcter
terminador de mensaje. En el caso anterior podramos haber recorrido
los mensajes buscando los caracteres nulos \0 que nos separaran cada
mensaje. Si consideramos los mensajes de longitud fija el cdigo del
servidor podra ser:
//definimos los mensajes de 100 bytes siempre
char cad[100]="Hola Mundo";
int length=sizeof(cad); //length=100

for(int i=0;i<10;i++)
{
int err=conn.Send(cad,length); //enviamos 100 bytes

if(err!=length)
{
std::cout<<"Send error"<<std::endl;
break;
}
}
Y el cdigo del cliente quedara:
char cad[100];
int length=100; //vamos a recibir mensajes de 100 bytes
for(int i=0;i<10;i++)
{
int r=client.Receive(cad,length);
if(r<0)
{
std::cout<<Error en la recepcion<<std::endl;
break;
}
else
std::cout<<"Rec: "<<r<<" contenido: "<<cad<<std::endl;
}
Ntese que aunque se necesitan solo unos pocos bytes para enviar Hola
Mundo, realmente se envan muchos ms. Es un enfoque bastante ineficiente, pero
muy simple. Se supone que se van a enviar distintos mensajes y que nunca sern ms
largos que 100 caracteres. La salida por pantalla es correcta porque se incluye el
carcter final de cadena \0, por lo que realmente no se imprimen los 100 caracteres
existentes en el buffer.
2.4.2 Conexiones mltiples.
Un servidor puede aceptar ms de una conexin, de tal forma que puede
permitir ejecutar varias veces seguidas el mismo cliente, o incluso a distintos clientes
desde distintas mquinas. Las conexiones pueden incluso ser simultneas, es decir se
puede permitir conectarse a un cliente y cuando termina de comunicar con el, permitir
la conexin de otro cliente, o se puede permitir la conexin y comunicacin simultnea
con varios clientes.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 39
Universidad Politcnica de Madrid -UPM
Un cliente solo puede comunicarse con un servidor.
2.4.2.1. Conexiones secuenciales
Una primera opcin es que el servidor atienda secuencialmente las conexiones
de los distintos clientes, esto es, se conecta un cliente, se comunica con el y vuelve a
esperar aceptando en el accept() a un nuevo cliente.

Figura 2-3. Servidor que permite mltiples conexiones secuenciales de clientes
El cliente permanecera inalterado, y el cdigo del servidor quedara como
sigue:
Servidor
Cliente
Se crea el socket
de conexin
Se le asigna una
direccin y un
puerto y se pone
a la escucha
El socket de
conexin se
queda bloqueado
a la espera
Aceptando una
conexin
Comunicacin
Cierre del socket
de comunicacion
Cierre del socket
de conexion
Seguir
aceptando
clientes?
SI
NO
Comunicacin
Conexin
Se crea el socket
de conexin y
comunicacin
Cierre
Socket de
conexion
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 40
Universidad Politcnica de Madrid -UPM
#include <iostream>
#include "Socket.h"

int main()
{
Socket servidor;
servidor.InitServer("127.0.0.1",12000);

while(1)
{
Socket conn=servidor.Accept();

//comunicacion, en este caso envio de 1 unico mensaje
char cad[]="Hola mundo";
int length=sizeof(cad);

conn.Send(cad,length);

conn.Close();
}
servidor.Close();

return 1;
}
La funcin listen() toma sentido en este contexto, ya que permite poner a
la cola peticiones de conexiones de varios clientes que intentan la conexin mientras el
servidor esta comunicando con el cliente actual. Cuando el servidor vuelve al
accept() se atienden dichas peticiones de conexin.
2.4.2.2. Conexiones simultneas.
Es posible que el servidor acepte la conexin de varios clientes y enve datos a
todos ellos, manteniendo la conexin activa con todos simultneamente.
Para ello y dado que aun no estamos utilizando programacin concurrente,
primero se realiza el accept() de tantos clientes como se vayan a conectar (el
servidor debe conocer dicho numero). Hay que recordar que el accept() bloquea
hasta que se conecta un cliente, por lo tanto hasta que no se conecten tantos clientes
como accept() se intenten, el programa no podr continuar. Como cada conexin
devuelve un socket diferente a travs del accept(), estos sockets se pueden
almacenar en un vector, y manejar todas las conexiones en el servidor a travs de
dicho vector.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 41
Universidad Politcnica de Madrid -UPM

Figura 2-4. Comunicacin simultanea con varios clientes
El cdigo resultante en el servidor podra ser:
#include <iostream>
#include "Socket.h"
int main()
{
Socket servidor;
servidor.InitServer("127.0.0.1",12000);
Socket conexiones[5];
for(i=0;i<5;i++)
conexiones[i]=servidor.Accept();

//comunicacion, en este caso envio de 1 unico mensaje
//se envia a los 5 clientes
char cad[]="Hola mundo";
int length=sizeof(cad);

for(i=0;i<5;i++)
conexiones[i].Send(cad,length);
for(i=0;i<5;i++)
conexiones[i].Close();
servidor.Close();
return 1;
}
ClienteN
Comunicacion
Conexion
Se crea el socket
de conexin y
comunicacin
Cierre
Servidor
Cliente1
Se crea el socket
de conexin
Se le asigna una
direccin y un
puerto y se pone
a la escucha
El socket de
conexin se
queda bloqueado
a la espera
Aceptando una
conexin
Comunicacin N
Cierre de los N sockets de
comunicacin y del de conexin
Seguir
aceptando
clientes?
SI
NO
Comunicacin
Conexin
Se crea el socket
de conexin y
comunicacin
Cierre
Comunicacin 1
Socket de
conexinN
Socket de
conexin 1
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 42
Universidad Politcnica de Madrid -UPM
2.5. ESTRUCTURA DE FICHEROS
Ahora que se ha visto como realizar el envo de informacin por la red, y se
dispone de una clase que encapsula la funcionalidad de los sockets se va a proceder a
comenzar el desarrollo de la aplicacin distribuida del juego del tenis. Debe quedar
claro que solo hay que desarrollar dos aplicaciones, la aplicacin servidor y la
aplicacin cliente. La aplicacin servidor se ejecutar una vez, pero la aplicacin cliente
(el mismo binario) puede ser ejecutado mltiples veces y en distintas mquinas. Se
parte de la aplicacin desarrollada en el tema anterior, que constituye el juego del
tenis (los dos jugadores en la misma mquina), cuyos ficheros se encuentran todos en
la misma carpeta y los cuales son:
Esfera.h y Esfera.cpp (la clase Esfera)
Plano.h y Plano.cpp (la clase Plano)
Raqueta.h y Raqueta.cpp (la clase Raqueta)
Vector2D.h y Vector2D.cpp (la clase Vector2D)
Mundo.h y Mundo.cpp (la clase Mundo)
Tenis.cpp (el fichero principal con el main() )
Makefile
La primera intencin podra ser duplicar esta carpeta para realizar las
modificaciones necesarias en cada una de ellas y transformarlas en el servidor y el
cliente. No obstante, esto implicara que habra mucho cdigo idntico duplicado en
dos sitios. Por ejemplo, la clase Plano ser exactamente igual en el cliente y en el
servidor, su parametrizacin es igual, se dibuja igual. Por tanto no es necesario (de
hecho es contraproducente) que el cdigo este repetido. Se pueden desarrollar ambos
programas, el cliente y el servidor compartiendo uno o varios archivos de cdigo
fuente.
Si se analiza la funcionalidad del servidor y del cliente se llega a la conclusin
que ambas aplicaciones son iguales, exceptuando:
El servidor atiende el teclado, cambiando la velocidad de las raquetas,
pero los clientes no, son solo espectadores. Esto se hace en la funcin
CMundo::OnKeyboardDown()
El servidor cambia las posiciones de los objetos (anima), realiza los
clculos de las colisiones. El cliente no tiene que mover los objetos
(podra moverlos de forma diferente al servidor), solo tiene que recibir
la informacin del servidor de donde estn los objetos en cada instante
de tiempo. El cambio de posicin de los objetos se hace en la funcin
CMundo::OnTimer().
Como se ve, la nica clase que va a tener diferencias entre el servidor y el
cliente es la clase CMundo. Por tanto, se propone nicamente duplicar este archivo
con dos nombres diferentes (aunque el nombre de la clase se puede mantener.)
Tambin es necesario duplicar el archivo en el que se encuentra el main(), ya que es
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 43
Universidad Politcnica de Madrid -UPM
el que instancia la clase CMundo, y en funcin de si es el servidor o el cliente,
necesitara hacer un #include a MundoServidor.h o a MundoCliente.h
Esfera.h y Esfera.cpp (la clase Esfera)
Plano.h y Plano.cpp (la clase Plano)
Raqueta.h y Raqueta.cpp (la clase Raqueta)
Vector2D.h y Vector2D.cpp (la clase Vector2D)
MundoServidor.h y MundoServidor.cpp (la clase Mundo para el
servidor)
MundoCliente.h y MundoCliente.cpp (la clase Mundo para el cliente)
servidor.cpp (el fichero principal con el main(), para el servidor )
cliente.cpp (el fichero principal con el main(), para el cliente )
Makefile
En el Makefile se especifican como se construyen las dos aplicaciones
diferentes:
CC=g++
CFLAGS= -g
LIBS= -lm -lglut
OBJS=Esfera.o Plano.o Raqueta.o Vector2D.o Socket.o
HEADERS=Esfera.h MundoCliente.h MundoServidor.h Plano.h Raqueta.h
Vector2D.h

all: servidor cliente

servidor: $(OBJS) MundoServidor.o servidor.o
$(CC) $(CFLAGS) -o servidor servidor.o MundoServidor.o $(OBJS)
$(LIBS)

cliente: $(OBJS) MundoCliente.o cliente.o
$(CC) $(CFLAGS) -o cliente cliente.o MundoCliente.o $(OBJS)
$(LIBS)

Socket.o: Socket.cpp $(HEADERS)
$(CC) $(CFLAGS) -c Socket.cpp
MundoCliente.o: MundoCliente.cpp $(HEADERS)
$(CC) $(CFLAGS) -c MundoCliente.cpp
MundoServidor.o: MundoServidor.cpp $(HEADERS)
$(CC) $(CFLAGS) -c MundoServidor.cpp
Esfera.o: Esfera.cpp $(HEADERS)
$(CC) $(CFLAGS) -c Esfera.cpp
Plano.o: Plano.cpp $(HEADERS)
$(CC) $(CFLAGS) -c Plano.cpp
Raqueta.o: Raqueta.cpp $(HEADERS)
$(CC) $(CFLAGS) -c Raqueta.cpp
Vector2D.o: Vector2D.cpp $(HEADERS)
$(CC) $(CFLAGS) -c Vector2D.cpp
servidor.o: servidor.cpp $(HEADERS)
$(CC) $(CFLAGS) -c servidor.cpp
cliente.o: cliente.cpp $(HEADERS)
$(CC) $(CFLAGS) -c cliente.cpp
clean:
rm -f *.o cliente servidor
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 44
Universidad Politcnica de Madrid -UPM
Con este Makefile la simple invocacin
make
construye tanto el servidor como el cliente
2.6. TRANSMITIENDO EL PARTIDO DE TENIS
Inicialmente vamos a realizar el envo de los datos necesarios del servidor a un
nico cliente. Para ello se deben de seguir los siguientes pasos:
2.6.1 Conexin
Aadir el Socket de conexin y el de comunicacin en la clase Mundo del
servidor:
Socket server;
Socket conn;
Aadir el Socket en la clase Mundo del cliente
Socket client;
En la funcin de inicializacin del juego en el servidor se establece la direccin
IP y el puerto del servidor y se espera la aceptacin de un cliente:
//en el fichero MundoServidor
void CMundo::Init()
{
//inicializacion de la pantalla, coordenadas, etc

server.InitServer("127.0.0.1",12000);
conn=server1.Accept();
}
Ntese en este punto que si se compila y ejecuta el servidor no se muestra
nada por pantalla. Sencillamente el programa esta bloqueado a la espera de la
conexin y ni siquiera ha creado aun la ventana grafica. No obstante el accept se
podra realizar ms tarde, despus de haber creado la ventana.
El cliente tambin realiza en su funcin init() la conexin del socket:
//del fichero MundoCliente
void CMundo::Init()
{
//inicializacion de la pantalla, coordenadas, etc

client.Connect("127.0.0.1",12000);
}

Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 45
Universidad Politcnica de Madrid -UPM
2.6.2 Envo de datos
Lo primero es necesario establecer cuales son los datos que es necesario que
enve el servidor al cliente. Dado que la pantalla es en su mayora esttica, las variables
que es necesario transmitir podran ser:
Coordenadas (x, y) de la pelota
Coordenadas (x1, y1, x2, y2) de la raqueta del jugador 1.
Coordenadas (x1, y1, x2, y2) de la raqueta del jugador 2.
Estos datos deben de ser enviados por el servidor cada vez que se produce un
cambio en los mismos, es decir, en cada temporizacin del timer. Cmo se envan
datos numricos? Aunque una solucin ms evolucionada se presentara en un tema
posterior, una primera solucin sencilla consiste en escribir (sprintf()) estos
valores numricos en una cadena de texto y enviar dicha cadena de texto.
EJERCICIO:
1. Realizar el envo de los datos por el socket de comunicacin en el servidor
(MundoServidor), en la funcin CMundo::OnTimer(), al final de la misma,
manteniendo el cdigo existente encargado de realizar la animacin y lgica del
juego.
2. Eliminar el cdigo de la funcin CMundo::OnTimer() de MundoCliente y sustituirlo
por la recepcin del mensaje del servidor y la extraccin de los valores numricos.

2.7. EJERCICIOS PROPUESTOS
Realizar la retransmisin del juego a un numero fijo de clientes, por
ejemplo 3
Implementar los conceptos desarrollados en este tema en un juego de
complejidad similar.

Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 47
Universidad Politcnica de Madrid -UPM
3
3
.
.

COMUNICACIONES Y CONCURRENCIA
3.1. INTRODUCCIN
En el captulo anterior hemos concluido con dos programas, un servidor y un
cliente, en el que el servidor enviaba los datos de la partida de tenis de forma continua
al cliente. De hecho, tambin podamos permitir que se conectaran varios clientes y
despus (una vez conectados todos los clientes, con lo que se tenia que conocer su
numero) enviar los datos a todos los clientes. Pero aun no podemos permitir que los
clientes espectadores se conecten y desconecten cuando quieran, o que los
jugadores puedan efectivamente jugar de forma remota.
Tal como esta planteado el programa, esto no es posible hacerlo con
programacin convencional (secuencial). Analizaremos en este captulo el porque y
veremos la solucin a dichos problemas. Comenzamos analizando un sencillo ejemplo.
Supngase que se esta diseando un controlador de una mquina, que se plasma
finalmente en un regulador que podra tener el siguiente aspecto (en pseudocdigo):
void main()
{
float referencia=3.0f;
float K=1.2f;
while(1)
{
float medida=GetValorSensor();
float error=referencia-medida;
float comando=K*error;//regulador proporcional
EnviaComando(comando);
}
}
Donde las funciones GetValorSensor() y EnviaComando() realizaran
la interfaz correspondiente con el hardware de la mquina. Obviamente el programa
se tiene que ejecutar de forma continua, recalculando en cada pasada el nuevo error y
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 48
Universidad Politcnica de Madrid -UPM
enviando un comando nuevo. El programa anterior utiliza una referencia (el punto al
que se quiere llevar el sistema) fija. Supngase que ahora se desea que el usuario sea
capaz de introducir por teclado dicha referencia tantas veces como quiera (para llevar
la mquina a distintos puntos) y que se programa de la siguiente forma:
void main()
{
float referencia=3.0f;
float K=1.2f;
while(1)
{
printf("Introduzca referencia: ");
scanf("%f",&referencia);
float medida=GetValorSensor();
float error=referencia-medida;
float comando=K*error;
EnviaComando(comando);
}
}
El efecto conseguido es que el programa se queda parado en el scanf()
esperando a la entrada del usuario. Cuando el usuario teclea un valor, se calcula y
enva un comando a la mquina y el programa se vuelve a quedar parado en el
scanf(). Si el usuario no teclea una nueva referencia, la mquina sigue funcionando
con el comando anterior de forma indefinida.
Obviamente, la solucin anterior no es valida. Tenemos dos tareas diferentes:
la ejecucin de forma continua del control y la interfaz con el usuario. Dichas tareas
tienen que ejecutarse de forma paralela a la vez. No podemos dejar de ejecutar el
control por el hecho de que el usuario este tecleando una referencia, ni podemos
inhabilitar al usuario de teclear una referencia por el hecho de que se este ejecutando
el control de forma continua.
La solucin es utilizar programacin concurrente. En el ejemplo anterior se
podra lanzar un hilo dedicado a la gestin de la entrada del usuario mientras que el
hilo principal ejecuta el control. El programa en pseudo cdigo podra quedar as:
float referencia=0.0f;//variable global
void hilo_usuario()
{
while(1)
{
printf("Introduzca referencia: ");
scanf("%f",&referencia);
}
}
void main()
{
float K=1.2f;
crear_hilo ( hilo_usuario );
while(1)
{
float medida=GetValorSensor();
float error=referencia-medida;
float comando=K*error;
EnviaComando(comando);
}
}
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 49
Universidad Politcnica de Madrid -UPM
Ntese como se ha puesto la variable referencia como global, para que
ambos hilos tengan acceso a la misma. Los hilos comunican informacin entre ellos a
travs de memoria global de la aplicacin.
3.2. REQUISITOS
Vamos a resumir las funcionalidades que nos quedan por implementar en
nuestro sistema distribuido:
Queremos permitir que los clientes se puedan conectar en el instante
que quieran. El servidor no debe quedar bloqueado por esperar a que
los clientes se conecten.
Queremos permitir cualquier nmero de clientes espectadores. De
dichos espectadores, nicamente los dos primeros podrn
efectivamente controlar las raquetas.
Los dos primeros clientes que se conecten podrn controlar las
raquetas, el primero de ellos con las teclas w y s y el segundo con las
teclas l y o.
El servidor debe de gestionar adecuadamente las desconexiones de los
clientes.
3.3. FUNCIONAMIENTO DE GLUT
El funcionamiento bsico de la librera GLUT se plasma en la funcin
glutMainLoop(), que es invocada desde el main():
//los callbacks
void OnDraw(void);
void OnTimer(int value);
void OnKeyboardDown(unsigned char key, int x, int y);

int main(int argc,char* argv[])
{
//Inicializar el gestor de ventanas GLUT
//y crear la ventana
glutInit(&argc, argv);
glutInitWindowSize(800,600);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
glutCreateWindow("ClienteTenis");

//Registrar los callbacks
glutDisplayFunc(OnDraw);
glutTimerFunc(25,OnTimer,0);
glutKeyboardFunc(OnKeyboardDown);

//pasarle el control a GLUT,que llamara a los callbacks
glutMainLoop();

return 0;
}
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 50
Universidad Politcnica de Madrid -UPM
Dicha funcin contiene en su interior un bucle continuo (en caso contrario
terminara la funcin main() y terminara el programa). Dicho bucle continuo se
podra representar a nivel conceptual como:
void glutMainLoop()
{
while(1)
{
if(pulsacion_teclado)
OnKeyBoardDown(tecla); //la funcion del usuario
if(hay_que_dibujar)
OnDraw(); //la funcion del usuario
if(tiempo_temporizador)
OnTimer();//la funcion del usuario
}
}
Por lo tanto, si se introduce alguna funcin que bloquee la secuencia continua
de ejecucin, la aplicacin se vera bloqueada por completo. Por ejemplo, supngase
que se ubica un scanf() en la funcin CMundo::OnTimer() para cambiar el
radio de la pelota:
void CMundo::OnTimer(int value)
{
printf("Introduzca el radio: ");
scanf("%f",&esfera.radio);

jugador1.Mueve(0.025f);
jugador2.Mueve(0.025f);
esfera.Mueve(0.025f);

El resultado final es la aplicacin bloqueada.
3.3.1 Lanzando un hilo
Podramos conseguir el anterior objetivo, mediante el uso de un hilo, de la
siguiente forma:
void* hilo_usuario(void* d)
{
CMundo* p=(CMundo*) d;
while(1)
{
printf("Introduzca el radio: ");
scanf("%f",&p->esfera.radio);
}
}
void CMundo::Init()
{
//inicializaciones varias


pthread_t thid;
pthread_create(&thid,NULL,hilo_usuario,this);
}
En este caso, la esfera esta contenida dentro de la clase CMundo, sin embargo,
el hilo es una funcin global, no es una funcin de la clase CMundo. Para conseguir el
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 51
Universidad Politcnica de Madrid -UPM
acceso del hilo al objeto mundo, lo que se puede hacer es pasarle un puntero al
mismo aprovechando el cuarto parmetro de la funcin pthread_create(). El hilo
se encargara a su vez de hacer el cast correspondiente para poder acceder a los
miembros de la clase CMundo.
3.4. ESTRUCTURA DEL SERVIDOR
Se ha visto en los requisitos que es necesario realizar distintas tareas, de forma
simultanea:
El hilo principal del servidor se encargara de realizar la animacin de la
escena (a travs de la funcin OnTimer), del dibujo y de enviar los datos
por los sockets a los clientes. Como el envo no es bloqueante, no es
necesario crear un hilo para esta tarea.
La aceptacin de nuevos clientes si que es bloqueante. Siempre se tiene
que estar ejecutando el accept() si queremos que los clientes
puedan conectarse y desconectarse cuando quieran. Por lo tanto es
necesario un hilo para esta tarea.
Para que los clientes remotos puedan efectivamente jugar de forma
distribuida, es necesario que enven informacin al servidor. Cada vez
que se pulse una tecla, enviaran dicha tecla al servidor. El servidor debe
de estar esperando a dicho mensaje. El problema es que la recepcin de
mensajes, en principio tambin es bloqueante, por lo que el programa
queda bloqueado hasta que se recibe dicho mensaje. La solucin es
implementar un hilo para cada uno de los dos jugadores que este a la
espera de dichos mensajes.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 52
Universidad Politcnica de Madrid -UPM

Figura 3-1. Estructura del servidor
Ntese adems que las frecuencias a las que funcionan los distintos hilos son
muy variables. El hilo principal ejecuta cada 25 milisegundos, aproximadamente. Sin
embargo el hilo de aceptacin de nuevos clientes ejecuta una iteracin del bucle cada
vez que se conecta un nuevo cliente, lo que puede tardar de forma variable desde
pocos milisegundos a infinito tiempo. Los hilos de recepcin de los comandos de los
jugadores funcionan a una frecuencia variable que coincide con las pulsaciones de
teclado de los jugadores.
3.5. MLTIPLES CONEXIONES SIMULTANEAS
Para permitir la conexin simultanea de mltiples clientes, es necesario
mantener un socket por cada uno de dichos clientes. Para tal efecto declaramos en la
clase CMundo (del fichero MundoServidor.h) un vector de la STL de objetos de la clase
Socket. Usamos un vector STL porque nos permite de forma cmoda aadir nuevos
objetos, quitar elementos y recorrerlo de forma sencilla. Tambin aadimos un
mtodo a CMundo denominado GestionaConexiones(), que se encargara de
realizar dicha gestin.
class CMundo
{
public:

Programa servidor
//hilo principal

OnTimer()
{
//tareas
//animacion

//envio
//datos
}
//hilo de
//aceptacion de
//nuevos clientes

while(1)
{
//accept()
}
//hilo de
//recepcion de
//comandos del
//jugador1

while(1)
{
//recv()
}
//hilo de
//recepcion de
//comandos del
//jugador2

while(1)
{
//recv()
}



Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 53
Universidad Politcnica de Madrid -UPM

Socket servidor;
std::vector<Socket> conexiones;
void GestionaConexiones();

};
A continuacin lanzamos un hilo denominado hilo_conexiones(), y de
forma similar a como hacamos anteriormente, pasamos un puntero al objeto actual
(this) a dicho hilo. Como es interesante manejarnos dentro de la clase mundo, la
nica tarea que tiene que hacer la funcin hilo_conexiones()es invocar al
mtodo GestionaConexiones(). Dicho mtodo entrara en un bucle infinito en
el que se repite un accept(). Cada vez que se conecte un cliente, se le aade al
vector de clientes conectados.
void* hilo_conexiones(void* d)
{
CMundo* p=(CMundo*) d;
p->GestionaConexiones();
}
void CMundo::GestionaConexiones()
{
while(1)
{
Socket s=servidor.Accept();
conexiones.push_back(s);
}
}
void CMundo::Init()
{
//inicializacion datos
servidor.InitServer("127.0.0.1",12000);
pthread_t thid_hilo_conexiones;
pthread_create(&thid_hilo_conexiones,NULL,hilo_conexiones,this);
}

3.6. MOSTRAR LOS CLIENTES CONECTADOS
Una ampliacin interesante al apartado anterior seria mostrar en la ventana los
clientes conectados y sus nombres, aparte de los puntos de los dos jugadores. Para ello
aadimos un nuevo vector a la clase CMundo del servidor. Tambin transformamos las
variables de los puntos de los jugadores en un vector:
class CMundo
{
public:


Socket servidor;
std::vector<Socket> conexiones;
std::vector<std::string> nombres;
void GestionaConexiones();

int puntos[2];
};
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 54
Universidad Politcnica de Madrid -UPM
Cada vez que se conecte un cliente nuevo nos deber enviar su nombre, para
aadirlo a nuestro vector. Por lo tanto segn se conecta un cliente, esperamos con un
Receive() dicho mensaje con el nombre.
void CMundo::GestionaConexiones()
{
while(1)
{
Socket s=servidor.Accept();
char cad[100];
s.Receive(cad,100);
nombres.push_back(cad);
conexiones.push_back(s);
}
}
Los nombres de los clientes pueden ser mostrados por pantalla:
void CMundo::OnDraw()
{


char cad[100];
sprintf(cad,"Servidor");
print(cad,300,10,1,0,1);
int i;
for(i=0;i<nombres.size();i++)
{
if(i<2)
{
sprintf(cad,"%s %d",nombres[i].data(),puntos[i]);
Print(cad,50,50+20*i,1,0,1);
}
else
{
sprintf(cad,"%s",nombres[i].data());
Print(cad,50,50+20*i,1,1,1);
}
}
}
Por supuesto el cliente nos debe enviar el nombre, lo que se puede preguntar
al usuario mediante un scanf() al comenzar el programa, y enviarlo
inmediatamente despus del Connect(). As el mtodo Init() de la clase
CMundo (del cliente) quedara as:
void CMundo::Init()
{
//inicializacion del mundo

char nombre[100];
printf("Introduzca su nombre: ");
scanf("%s",nombre);
cliente.Connect("127.0.0.1",12000);

cliente.Send(nombre,strlen(nombre)+1);
}
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 55
Universidad Politcnica de Madrid -UPM
3.7. RECEPCIN COMANDOS MOVIMIENTO
Cuando el programa cliente detecte una pulsacin de teclado, enviara dicha
pulsacin al servidor, para que el servidor la interprete como juzgue necesario. El envo
del cliente se realiza fcilmente en la funcin OnKeyboardDown():
void CMundo::OnKeyboardDown(unsigned char key, int x, int y)
{
char cad[100];
sprintf(cad,"%c",key);
cliente.Send(cad,strlen(cad)+1);
}
Ntese como este envo se realiza nicamente si el usuario pulsa una tecla. El
hilo implementado en el servidor tendr una forma similar al hilo anterior:
void* hilo_comandos1(void* d)
{
CMundo* p=(CMundo*) d;
p->RecibeComandosJugador1();
}
void CMundo::RecibeComandosJugador1()
{
while(1)
{
usleep(10);
if(conexiones.size()>=1)
{
char cad[100];
conexiones[0].Receive(cad,100);
unsigned char key;
sscanf(cad,%c,&key);
if(key=='s')jugador1.velocidad.y=-4;
if(key=='w')jugador1.velocidad.y=4;
}
}
std::cout<<"Terminando hilo comandos jugador1"<<std::endl;
}
void CMundo::Init()
{
//Inicializacion

server.InitServer("127.0.0.1",12000);

pthread_t thid_hilo_conexiones, thid_hilo_comandos1;
pthread_create(&thid_hilo_conexiones,NULL,hilo_conexiones,this);
pthread_create(&thid_hilo_comandos1,NULL,hilo_comandos1,this);
}
Ntese en este caso la comprobacin conexiones.size()>=1, para
asegurarnos de que efectivamente existe al menos 1 cliente conectado. Adems se ha
aadido un retardo usleep(10) para evitar que el bucle while(1) ejecute en
vacio si no hay clientes conectados, lo que supondra una sobrecarga de la CPU
innecesaria.
EJERCICIO: Compltese el programa, aadiendo un segundo hilo que gestione los
comandos del segundo jugador.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 56
Universidad Politcnica de Madrid -UPM
3.8. GESTIN DESCONEXIONES
En cualquier instante los clientes espectadores pueden desconectar. Qu pasa
entonces con el vector de sockets mantenido por el servidor? Las desconexiones
deben de ser analizadas y gestionadas adecuadamente.
La forma ms sencilla de detectar las desconexiones es en el envo realizado
dentro de la funcin CMundo::OnTimer() en el lado del servidor. El envo hay que
hacerlo a todos los clientes conectados. Podramos utilizar el retorno de Send() para
realizar la eliminacin del cliente del vector. No obstante hay que tener en cuenta los
efectos del borrado sobre el vector que se est recorriendo.
void CMundo::OnTimer(int value)
{

for(i=0;i<conexiones.size();i++) //MALA SOLUCION
{
if(-1==conexiones[i].Send(cad,strlen(cad)+1))
{
conexiones.erase(conexiones.begin()+i);
nombres.erase(nombres.begin()+i);
if(i<2)
puntos[0]=puntos[1]=0;
}
}
}
La solucin ms sencilla consiste en ir recorriendo el vector al revs, del final al
principio, con lo que las eliminaciones no afectan al bucle for.
void CMundo::OnTimer(int value)
{

for(i=conexiones.size()-1;i>=0;i--)
{
if(0>=conexiones[i].Send(cad,200))
{
conexiones.erase(conexiones.begin()+i);
nombres.erase(nombres.begin()+i);
if(i<2)
puntos[0]=puntos[1]=0;
}
}
}
Ntese que adems, si se ha desconectado uno de los dos primeros clientes (es
decir uno de los dos jugadores), entonces el primer espectador pasara a ocupar su
lugar y comenzara una nueva partida, poniendo los marcadores a cero.
3.9. FINALIZACIN DEL PROGRAMA
Hasta este punto, cuando se cierra el programa servidor, los hilos acaban de
forma forzada. Es conveniente en cualquier programa realizar un cierre ordenado de
todos los hilos en ejecucin.
Para ello se deben seguir los siguientes pasos (todos ellos en el servidor):
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 57
Universidad Politcnica de Madrid -UPM
Aadir una variable denominada acabar, que inicialmente vale 0 a la
clase CMundo.
Poner dicha variable a 1 en el destructor de la clase CMundo.
Utilizar la variable como condicin de repeticin en los bucles while()
de los hilos:
while(!acabar)
{

}
Poner los identificadores de todos los hilos como variables de la clase
CMundo, para que puedan ser utilizados en el pthread_join
Ejecutar el pthread_join() tantas veces como sea necesario en el
destructor de la clase CMundo, para esperar a que terminen los hilos.
En este punto se analiza el resultado cuando se cierra el programa servidor.
Realmente se est esperando a la finalizacin de los hilos? La respuesta es no. Los
hilos estn bloqueados en el accept() y en el recv() por lo que aunque
modifiquemos la bandera acabar esta no es tenida en cuenta hasta la siguiente
iteracin del bucle. Hay que conseguir que se desbloqueen el accept() y el recv()
de los hilos, lo que se puede hacer de forma sencilla cerrando el socket del servidor,
antes de los pthread_join()
3.10. EJERCICIO PROPUESTO

Realizar la misma tarea con otro juego de complejidad similar.
Analizar los posibles problemas de sincronizacin que pueden aparecer
en caso de conexiones y desconexiones de clientes.
Aumentar la informacin que se retransmite, para que los clientes
tengan tambin la informacin de quien esta conectado y quien esta
jugando, as como los puntos actuales de la partida.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 59
Universidad Politcnica de Madrid -UPM

4
4
.
.

COMUNICACIN Y SINCRONIZACIN
INTERPROCESO
4.1. INTRODUCCIN
Existen otros mecanismos para comunicar datos entre distintos procesos
diferentes a los sockets, cuando los procesos se ejecutan en una mquina con una
memoria principal comn y gestionada por un nico sistema operativo
(monocomputador). A diferencia de la comunicacin por sockets, que se suele
denominar programacin distribuida, estos mecanismos entran dentro de la
denominada comunicacin interproceso (Inter Process Comunication IPC). Entre estos
mecanismos destacan:
Las tuberas sin nombre (pipes) y con nombre (FIFOS)
La memoria compartida
El hecho de tener varios procesos (o hilos) accediendo a unos datos comunes
de forma concurrente puede originar problemas de sincronizacin en esos datos. Para
prevenir estos problemas hay tambin otros mecanismos como:
Los mutex y las variables condicionales
Las tuberas (usadas para sincronizar)
Los semforos
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 60
Universidad Politcnica de Madrid -UPM
4.2. EL PROBLEMA DE LA SINCRONIZACION
Cuando existen varios hilos accediendo de forma concurrente a unos datos, se
pueden presentar problemas de concurrencia. En nuestra aplicacin, tenemos varios
hilos accediendo de forma concurrente al vector de conexiones. En concreto el hilo
principal, a travs del timer:
void CMundo::OnTimer(int value)
{

for(i=conexiones.size()-1;i>=0;i--)
{
char cad[1000];
sprintf(cad,"%f %f %f %f %f %f %f %f %f %f",
esfera.centro.x,esfera.centro.y,
jugador1.x1,jugador1.y1,
jugador1.x2,jugador1.y2,
jugador2.x1,jugador2.y1,
jugador2.x2,jugador2.y2);
if(0>=conexiones[i].Send(cad,strlen(cad)+1))
{
conexiones.erase(conexiones.begin()+i);
nombres.erase(conectados.begin()+i);
puntos[0]=puntos[1]=0;
}
}
}
El hilo de gestin de las conexiones:
void CMundo::GestionaConexiones()
{
while(!acabar){
Socket s=server.Accept();
char cad[100];
s.Receive(cad,100);
nombres.push_back(cad);
conexiones.push_back(s);
}

}
Y los hilos de recepcin de mensajes de los jugadores:
void CMundo::RecibeComandosJugador1()
{
Socket s;
while(!acabar)
{
usleep(10);
if(conexiones.size()>0)
{
char cad[100];
conexiones[0].Receive(cad,100); //peligroso
printf("Llego la tecla %c\n",cad[0]);
unsigned char key=cad[0];
if(key=='s')jugador1.velocidad.y=-4;
if(key=='w')jugador1.velocidad.y=4;
}
}
}
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 61
Universidad Politcnica de Madrid -UPM
Ms concretamente: Es posible que mientras el hilo que recibe los mensajes del
jugador decide que hay un jugador conectado (conexiones.size()>0), el hilo
principal que enva los datos por el socket, se de cuenta que dicho cliente ha sido
desconectado y decida borrarlo del vector. Si el vector queda vacio, un acceso a
conexiones[0] genera un error fatal segmentation fault, y nuestro servidor
abortara de manera inesperada.
No obstante, en la prctica es bastante improbable que suceda esto, y
seguramente serian necesarias cientos de conexiones y desconexiones para que este
efecto fuera visible. Por lo tanto, no abordaremos de momento el problema de la
sincronizacin, pero hay que tener en cuenta que en una aplicacin real sera
totalmente obligatorio realizar esta sincronizacin, sino nuestro programa podra fallar
en un momento inesperado.
Sin embargo si hay un motivo por el que el servidor puede cerrar
inesperadamente. Es la recepcin de la seal SIGPIPE cuando se intenta enviar algo por
un socket que ha sido cerrado. Si no se gestiona esta seal, el comportamiento por
defecto termina el programa. La forma ms sencilla de obviar esta seal, es indicar a la
funcin send() en sus banderas, que no enve esta seal en caso de error, lo que se
hace de la siguiente forma:
int err=send(sock, cad, length,MSG_NOSIGNAL);

4.3. COMUNICACIN INTERPROCESO
En este tema se propone el siguiente esquema como ejemplo del uso de
distintos mecanismos de comunicacin interproceso:

Figura 4-1. Ejemplo de comunicacin interproceso con tuberas y memoria
compartida
RED
TCP/IP
Logger
FIFO
Bot
Memoria
compartida
Servidor
Cliente
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 62
Universidad Politcnica de Madrid -UPM
En el computador que corre el servidor, se desarrollara un programa que sirva
para mostrar eventos de una forma ordenada por pantalla, aunque tambin podra
decidir guardarlos a disco, a una base de datos, etc. Los eventos sern los puntos
marcados, y quien (el nombre del jugador) que ha marcado un tanto, y sern enviados
mediante cadenas de texto por una tubera con nombre o FIFO, al programa que
llamaremos logger.
En el lado del cliente se desarrollara un programa sencillo que pueda controlar
los movimientos de la raqueta correspondiente automticamente. A este programa le
llamaremos bot. El cliente y la aplicacin bot intercambiaran datos en una zona de
memoria compartida.
Ambas aplicaciones nuevas sern aplicaciones de tipo consola. El makefile de
las cuatro aplicaciones quedara como sigue:
CC=g++
CPPFLAGS=-g
LIBS= -lm -lglut -lpthread
OBJS=Esfera.o Plano.o Raqueta.o Vector2D.o Socket.o

all: servidor cliente bot logger

logger: logger.o
$(CC) $(CPPFLAGS) -o logger logger.o $(LIBS)
bot: bot.o
$(CC) $(CPPFLAGS) -o bot bot.o $(LIBS)
servidor: $(OBJS) MundoServidor.o servidor.o
$(CC) $(CPPFLAGS) -o servidor MundoServidor.o servidor.o $(OBJS)
$(LIBS)
cliente: $(OBJS) MundoCliente.o cliente.o
$(CC) $(CPPFLAGS) -o cliente MundoCliente.o cliente.o $(OBJS)
$(LIBS)
depend:
makedepend *.cpp -Y
clean:
rm -f *.o servidor cliente bot logger

#DEPENDENCIAS

4.4. TUBERAS CON NOMBRE
Las tuberas son un mecanismo tanto de comunicacin como de sincronizacin.
Las tuberas sin nombre o pipes se utilizan en procesos que han sido creados mediante
fork() y tienen relaciones padre-hijo, de tal forma que heredan dicha tubera.
Cuando se trata de procesos totalmente separados, la tubera tiene que ser con
nombre para que ambos procesos sean capaces de acceder a ella.
Las tuberas con nombre se direccionan como un archivo (un archivo especial)
en la estructura de directorios. En las tuberas con nombre tiene que existir un proceso
que se encargue de crear dicho pseudoarchivo, que adems tiene que ser el primer
proceso que comience a ejecutar. Dicho proceso podra tener un cdigo como el
siguiente, para enviar por el FIFO una frase a otro proceso que se conecte al mismo:
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 63
Universidad Politcnica de Madrid -UPM
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>


int main(int argc,char* argv[])
{
mkfifo("/ruta/MiFifo1",0777);

int pipe=open("/ruta/MiFifo1",O_WRONLY);

char cad[150]=Hola que tal;
int ret=write(pipe,cad,strlen(cad)+1);

close(pipe);
unlink("/ruta/MiFifo1");

return 0;
}
Donde:
mkfifo("/ruta/MiFifo1",0777);
crea un archivo con un icono especial en forma de tubera en la ruta indicada, y
con los permisos correspondientes (0777= permisos de lectura, escritura y ejecucin
para todo el mundo).
int pipe=open("/ruta/MiFifo1",O_WRONLY);
La funcin open() abre dicha tubera con el acceso especificado (O_WRONLY,
O_RDONLY, O_RDWR) y devuelve un descriptor de archivo (pipe) que es el utilizado
para enviar y recibir datos. Ntese que esta funcin bloquea hasta que se conecta
alguien en el otro extremo de la tubera.
A continuacin se hace un envo de datos:
int ret=write(pipe,cad,strlen(cad)+1);
Y finalmente se cierra la tubera y se elimina el pseudoarchivo
close(pipe);
unlink("/ruta/MiFifo1");
El otro proceso nicamente debe de abrir la tubera, usarla y cerrarla, pero no
crear el archivo ni borrarlo. Lgicamente, este segundo proceso debe de ser arrancado
despus del anterior, para que la tubera sea creada primero antes de intentar abrirla.
int main(void)
{
int pipe=open("/ruta/MiFifo1",O_RDONLY);

char cad[150];
read(pipe,cad,sizeof(cad));

printf("Cadena=%s\n",cad);

close(pipe);
return 1;
}
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 64
Universidad Politcnica de Madrid -UPM
Hay que recordar que la tubera es un mecanismo totalmente unidireccional, no
permite que el receptor enve datos por la misma tubera. Si se desea implementar
comunicacin bidireccional es necesario el uso de 2 tuberas.
Ejercicio: Implementar el programa Logger y los cambios necesarios en el Servidor,
para que este enve al Logger el nombre y nmero de puntos que lleva un jugador
solo en el momento de marcar un tanto. Seguir los siguientes pasos:
1. El programa Logger se ejecuta antes que el servidor, por lo tanto ser el
encargado de crear y destruir el archivo.
2. El logger entra en un bucle infinito de recepcin de datos.
3. Aadir el identificador del FIFO como atributo de la clase CMundo en el
servidor.
4. Abrir la tubera (antes de lanzar los hilos)
5. Enviar los datos cuando se produzcan puntos.
6. Cerrar la tubera adecuadamente
4.5. MEMORIA COMPARTIDA
La memoria compartida es un mecanismo exclusivamente de comunicacin que
permite tener en comn una zona de memoria, accesible desde varios procesos.
Dichos procesos, una vez inicializada y accedida, vern la zona de memoria compartida
como memoria propia del proceso. Esta forma de trabajar resulta muy interesante
especialmente si la cantidad de datos a compartir entre los distintos procesos es muy
elevada.
Hay distintas interfaces a la memoria compartida, como las funciones de BSD y
la memoria compartida POSIX. En este captulo se utiliza la memoria compartida
POSIX.
As un proceso que quisiera tener en comn una zona de memoria de 10 datos
de tipo entero, compartida con otros procesos, podra hacer algo de la forma:

#include <sys/types.h>
#include <stdio.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>

int main(void)
{
int datos[10]={0};

//memoria compartida
key_t mi_key=ftok("/bin/ls",12);
int shmid=shmget(mi_key,sizeof(datos),0x1ff|IPC_CREAT);
char* punt=(char*)shmat(shmid,0,0x1ff);

Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 65
Universidad Politcnica de Madrid -UPM
while(1)
{
int i,num;
printf("Numero de dato: ");
scanf("%d",&i);
printf("Dato: ");
scanf("%d",&num);
datos[i]=num;
memcpy(punt,datos,sizeof(datos));
}

shmdt(punt);
shmctl(shmid,IPC_RMID,NULL);

return 1;
}
Donde
key_t mi_key=ftok("/bin/ls",12);
obtiene una llave nica que sirve para identificar la zona de memoria
compartida. Los parmetros suministrados a esta funcin tienen que ser los mismos en
los diferentes procesos que utilicen la zona de memoria, y son un nombre de archivo
(uno cualquiera existente en el sistema de archivos) y un numero entero.
A continuacin se obtiene el descriptor mediante la funcin shmget(), a la
que se le indica el tamao en nmero de bytes de la misma, los permisos (0x1ff
significa acceso total a todos). En el caso que el proceso realmente quiera crear la zona
porque todava no existe, debe especificar la bandera IPC_CREAT.
int shmid=shmget(mi_key,sizeof(datos),0x1ff|IPC_CREAT);
La obtencin de un puntero, cuyo tipo se puede adaptar sencillamente con un
cast, se obtiene con la funcin shmat(), a la que se especifican otra vez los
permisos particulares de este acceso.
char* punt=(char*)shmat(shmid,0,0x1ff);
El acceso posterior a la zona de memoria se puede hacer con algn tipo de cast,
de indireccin por ndices de un vector o directamente copiando datos a esa zona de
memoria. Una vez terminada de utilizar, es necesario soltar el puntero asignado y
liberar la zona de memoria:
shmdt(punt);
shmctl(shmid,IPC_RMID,NULL);
Como en el caso anterior, el proceso que efectivamente crea la zona de
memoria debe de ser arrancado antes que los procesos que accedan a ella. Uno de
estos procesos, podra tener el aspecto siguiente:
#include <sys/types.h>
#include <stdio.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>

int main(void)
{
int datos[10];
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 66
Universidad Politcnica de Madrid -UPM
int i;
key_t mi_key=ftok("/bin/ls",12);
int shmid=shmget(mi_key,sizeof(datos),0x1ff);
char* punt=(char*)shmat(shmid,0,0x1ff);

while(1)
{
memcpy(datos,punt,sizeof(datos));
for(i=0;i<10;i++)
printf("%d ",datos[i]);
printf("\n");
}
shmdt(punt);
return 1;
}
Para utilizar cmodamente la memoria compartida en nuestra aplicacin,
vamos a crear (solo la declaracin es necesaria) una clase de conveniencia que agrupe
los distintos datos que se necesitaran compartir entre el cliente y el bot:
#include "Esfera.h"
#include "Raqueta.h"

class DatosMemCompartida
{
public:
Esfera esfera;
Raqueta raqueta1;
Raqueta raqueta2;
int jugador;//0 es raqueta1, 1 raqueta 2, otra cosa, espectador
int accion; //1 arriba, 0 nada, -1 abajo
};
La primera cosa que se observa es que el bot difcilmente podr realizar
ninguna decisin sino sabe que raqueta est controlando (o si est controlando
alguna). Esta informacin tampoco la tiene el cliente, ya que este se limita a transmitir
las teclas pulsadas, y el servidor le har caso o no. Para incluir esta informacin, debe
de ser el servidor el que enve a todos los clientes el nmero de cliente que son. As si
es el cliente 0, sabr que es el primer jugador con la raqueta1, y si es el cliente 1, sabr
que es el segundo jugador con la raqueta2.
for(i=conexiones.size()-1;i>=0;i--)
{
char cad[1000];
sprintf(cad,"%d %f %f %f %f %f %f %f %f %f %f",
i,esfera.centro.x,esfera.centro.y,
jugador1.x1,jugador1.y1,
jugador1.x2,jugador1.y2,
jugador2.x1,jugador2.y1,
jugador2.x2,jugador2.y2);
}
El cliente, tambin aadir una variable denominada num_cliente a la clase
CMundo, y la extraer convenientemente de la cadena recibida.
Es necesario aadir las variables siguientes a la clase CMundo del cliente, para
que acceda adecuadamente a la zona de memoria compartida:
#include "DatosMemCompartida.h"

Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 67
Universidad Politcnica de Madrid -UPM
class CMundo
{
public:


DatosMemCompartida* datos;
int shmid;


};
El cliente ser el encargado de crear la zona de memoria compartida, lo que se
puede hacer en la funcin Init():
void CMundo::Init()
{

key_t mi_key=ftok("/bin/ls",12);
int shmid =shmget(mi_key,sizeof(DatosMemCompartida),0x1ff|IPC_CREAT);
datos=(DatosMemCompartida*)shmat(shmid,0,0x1ff);
}
Cada vez que el cliente obtiene datos nuevos los pone en la zona de memoria
compartida, para que el bot tenga acceso a ellos:
void CMundo::OnTimer(int value)
{
char cad[1000];
client.Receive(cad,1000);
sscanf(cad,"%d %f %f %f %f %f %f %f %f %f %f",
&num_cliente,
&esfera.centro.x,&esfera.centro.y,
&jugador1.x1,&jugador1.y1,
&jugador1.x2,&jugador1.y2,
&jugador2.x1,&jugador2.y1,
&jugador2.x2,&jugador2.y2);

datos->jugador=num_cliente;
datos->esfera.centro=esfera.centro;
datos->raqueta1=jugador1;
datos->raqueta2=jugador2;
}
El bot a su vez, lee los datos de la memoria compartida y toma una decisin
acerca de la accin a realizar:
#include <stdio.h>
#include <sys/shm.h>
#include <string.h>
#include "DatosMemCompartida.h"

int main(int argc,char* argv[])
{
int i;
key_t mi_key=ftok("/bin/ls",12);
int shmid=shmget(mi_key,sizeof(DatosMemCompartida),0x1ff);
DatosMemCompartida* dat
=(DatosMemCompartida*)shmat(shmid,0,0x1ff);
while(1)
{
usleep(25000);
if(dat->jugador==0)
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 68
Universidad Politcnica de Madrid -UPM
{
dat->accion=0;//accion por defecto, ninguna
//completar


}
if(dat->jugador==1)
{
}
}

shmdt(dat);
return 0;
}

Ejercicio: Completar el bot para que tome una decisin del movimiento a realizar

El cliente, consultara la accin decidida por el bot y la llevar a cabo. Esta
tarea tambin ser llevada a cabo en el OnTimer:
void CMundo::OnTimer(int value)
{

if(num_cliente==0)
{
if(datos->accion==-1)
OnKeyboardDown('s',0,0);
if(datos->accion==1)
OnKeyboardDown('w',0,0);
datos->accion=0;
}
if(num_cliente==1)
{
if(datos->accion==-1)
OnKeyboardDown('l',0,0);
if(datos->accion==1)
OnKeyboardDown('o',0,0);
datos->accion=0;
}
}

Ejercicio: Asegurar que la zona de memoria compartida se libera cuando se cierra la
aplicacin cliente. Utilizar una bandera para indicar al bot que tambin debe de
cerrarse.

4.6. EJERCICIOS PROPUESTOS
Se proponen a continuacin algunas posibles mejoras a realizar en el juego:
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 69
Universidad Politcnica de Madrid -UPM
Hacer que el jugador pueda controlar mediante su cliente con el teclado
la raqueta. nicamente cuando el jugador deja de controlarla durante
10 segundos, entra automticamente el bot y coge el control de nuevo.
Realizar un tercer programa en el computador del servidor que
permitiera a un comentarista del partido ir tecleando comentarios que
fueran guardados en el fichero (mostrados por pantalla en nuestro
caso). Tener en cuenta posibles problemas de sincronizacin o gestin
de mensajes.
Aadir un hilo al programa bot que sirva de interfaz con el usuario y
pida al mismo que teclee algn valor que le permita cambiar el
comportamiento del bot, ms hbil, menos hbil, por ejemplo.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 70
Universidad Politcnica de Madrid -UPM















Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 71
Universidad Politcnica de Madrid -UPM







Parte II. Programacin
avanzada






Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 72
Universidad Politcnica de Madrid -UPM
















Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 73
Universidad Politcnica de Madrid -UPM

5
5
.
.

PROGRAMACIN DE CDIGO EFICIENTE
5.1. INTRODUCCIN
Se establece como pre-requisito en este libro que el lector conoce el lenguaje C
y que es capaz de programar en dicho lenguaje, sintetizando pequeos algoritmos y
soluciones. Tambin se asume conocimiento del lenguaje C++ y de conceptos de
programacin orientada a objetos. No obstante es bastante posible que el lector
todava no tenga en consideracin cuando programa que el cdigo que esta tecleando
puede funcionar ms o menos rpido cuando se ejecute en el computador. Hay que
tener en cuenta que el computador, PC o microprocesador va ejecutando
secuencialmente las instrucciones (ya compiladas en lenguaje mquina), y lo hace de
manera tan rpida que los pequeos programas realizados por un aprendiz se ejecutan
sin ningn problema.
Sin embargo, en el desarrollo de aplicaciones reales, ya sean de gestin,
ingeniera o cientficas o incluso ldicas como videojuegos, hay que tener en cuenta
que el volumen (cantidad de lneas de cdigo) de dichas aplicaciones es elevadsimo y
el microprocesador debe de ejecutar gran cantidad de cdigo. En muchos de estos
casos es importante tener en cuenta la eficiencia o cuanto de rpido ejecuta el cdigo
que estamos programando.
Veamos un ejemplo sencillo, en el que queremos programar una funcin que
calcule la exponencial de un nmero real, ya que necesitamos dicha funcin para
nuestros clculos ingenieriles. Una forma comn de calcular la exponencial en sistemas
informticos es utilizar su desarrollo de Taylor:

2 3
1 ...
1! 2! 3! !
n
x
x x x x
e
n

Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 74
Universidad Politcnica de Madrid -UPM
Parece razonable enfocar la solucin al problema mediante la siguiente
descomposicin en funciones, entre las que aparecen de forma lgica la potencia y el
factorial. La funcin exponencial recurre a ellas para calcular el trmino n-simo de la
serie de Taylor. La estructura del programa con este esquema seria:
#define PRECISION 100

double exponencial(double num);
double factorial(int num);
double potencia(double base,int expo);

void main(void)
{
double x,e_x;
int i;
printf("Numero: ");
scanf("%lf",&x);
e_x=exponencial(x);
printf("la exp.de %lf es %lf\n",x,e_x);
}
Y la implementacin de las funciones quedara:
double factorial(int valor)
{
int i;
double fact=1;
for (i=valor;i>0;i--)
fact*=(double)i;
return(fact);
}
double potencia(double base,int expo)
{
int i;
double pot=1;
for (i=1;i<=expo;i++)
pot*=base;
return(pot);
}
double exponencial(double num)
{
int i;
double resultado=1;
for (i=1;i<=PRECISION;i++)
resultado+=(potencia(num,i)/factorial(i));
return(resultado);
}
Aunque esta solucin es impecable desde el punto de vista estructural (la
subdivisin del problema en partes), tiene un importante fallo: una gran ineficiencia a
la hora de calcular la exponencial.
Considrese cada trmino de Taylor. Se puede apreciar que para calcular el
trmino n-esimo hay que realizar los siguientes clculos:
(n veces)
! ( 1) ( 2) (n terminos) 2 1
n
x x x x x
n n n n

Es decir, para calcular la potencia, hacen falta n multiplicaciones y para
calcular el factorial hacen falta otras n multiplicaciones. Por tanto el termino dcimo
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 75
Universidad Politcnica de Madrid -UPM
de la serie, requiere 10 veces (20 frente a 2) multiplicaciones que el termino de grado
2. Obviamente, el problema se agrava a medida que se incrementa el nmero de
trminos de la serie. Se dice que el coste computacional de calcular cada termino crece
linealmente con el ordinal del termino, lo que se representa habitualmente como O(n).
No obstante hay una solucin ha este problema, basndose en la recursividad
en el calculo del numerador y el denominador de cada termino. Resulta obvio que:
1
! ( 1)!
n n
x x x
n n n

Lo que implica que el numerador y denominador de cada trmino se pueden
calcular a partir del numerador y denominador del trmino anterior. Esta solucin es
obviamente mucho ms eficiente, ya que para calcular cada trmino hacen falta
nicamente 2 multiplicaciones, una para el numerador y otra para el denominador,
independientemente del ordinal del trmino en cuestin. Se dice que cada termino se
pude calcular (a partir del numerador y denominador del termino anterior) en tiempo
constante (independientemente del ordinal del termino) lo que se representa
comnmente como O(1).
La implementacin de esta solucin se realiza en una funcin denominada
exponencial2():
double exponencial2(double num)
{
int i;
double resultado=1;
double numerador=1.0,denominador=1.0f;
for (i=1;i<=PRECISION;i++)
{
numerador*=num;
denominador*=i;
resultado+=numerador/denominador;
}
return(resultado);
}
Es importante resaltar en este punto que la solucin numrica al problema es
exactamente la misma que en el caso anterior. No es una simplificacin, ni una
aproximacin, se calcula el mismo resultado pero de dos formas diferentes.
Para poner de relieve las diferencias entre ambas soluciones, las ejecutamos
cien mil veces cada una. Tngase en cuenta que los clculos que puede hacer una
aplicacin real pueden ser muy numerosos, y quizs el calculo de la exponencial puede
ser requerido miles de veces.
int main(int argc, char* argv[])
{
double num;
printf("Numero: ");
scanf("%lf",&num);

tiempo();

for(int i=0;i<100000;i++)
exponencial(num);

Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 76
Universidad Politcnica de Madrid -UPM
tiempo();
printf("la exp.de %lf es %lf\n",num,exponencial(num));


for(i=0;i<100000;i++)
exponencial2(num);

tiempo();
printf("la exp.de %lf es %lf\n",num,exponencial2(num));


return 0;
}
Para medir los tiempos de ejecucin hemos utilizado una funcin de
conveniencia denominada tiempo() que se encarga de sacar por pantalla el tiempo
transcurrido entre dos llamadas a la misma:
#include <stdlib.h>
#include <sys/timeb.h>

void tiempo()
{
static struct timeb t1={0};
struct timeb t2;
ftime(&t2);
float t=((t2.time-t1.time)*1000+
(t2.millitm-t1.millitm))/1000.0f;
if(t1.time!=0)
printf("Tiempo= %f\n",t);
t1=t2;
}
El resultado de ejecutar el programa anterior podra ser similar al siguiente, que
es el resultado de ejecutarlo en un Intel Core2 Duo a 3Ghz con WindowsXP y
compilando en Visual C++ 6.0. Estos resultados pueden variar lgicamente en funcin
de la mquina, el sistema operativo y el sistema de desarrollo.
Numero: 1
Tiempo= 3.500000
la exp.de 1.000000 es 2.718282
Tiempo= 0.141000
la exp.de 1.000000 es 2.718282
Como se puede apreciar, el tiempo necesario en el primer caso es de 3,5
segundos, frente a los 141 milisegundos que tarda en el segundo caso. Es decir, la
segunda solucin es unas 25 veces ms rpida que la primera. Al final, el resultado
puede ser una aplicacin que deja a la espera al usuario varios segundos antes de darle
un resultado, con la incomodidad que ello supone, si se utiliza el primer enfoque,
mientras que en el segundo caso la aplicacin responder mucho ms rpidamente y
por tanto la satisfaccin del usuario ser mayor y las probabilidades de xito del
software tambin sern incrementadas.
El desarrollo de cdigo eficiente y el anlisis de la ejecucin de cdigo es una
disciplina mucho ms all de lo que puede cubrir este libro. Este tema trata
nicamente de ilustrar algunas tcnicas y ejemplos que introduzcan al lector en este
problema, de tal forma que el programador novel empiece a tener en cuenta criterios
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 77
Universidad Politcnica de Madrid -UPM
de eficiencia cuando programa, y tenga un punto de partida practico y sencillo a dichas
disciplinas.
5.2. MODOS DE DESARROLLO
Es importante resaltar en este punto que los entornos de desarrollo y
compiladores permiten fundamentalmente dos modos de desarrollo:
La versin de desarrollo, depuracin o Debug es la versin que permite
depurar el cdigo en tiempo de ejecucin (con un debugger tpicamente
integrado en el entorno). El ejecutable generado no esta optimizado
para ejecutarse rpidamente ni para menor tamao.
La versin final o Release es para generar un ejecutable optimizado para
mayor velocidad, pero que no permite la depuracin del cdigo en
busca de errores.
En Visual Studio se puede seleccionar entre ellas en Menu-> Build -> Set Active
Configuration y seleccionar la que se desee. La configuracin por defecto es la Debug.
En linux y gcc se utiliza la bandera -g para indicar el modo de depuracin.
La optimizacin y medida de tiempos de ejecucin se realizan tpicamente en la
versin Release, que es la que ejecuta ms rpidamente.
5.3. TIPOS DE OPTIMIZACIONES
Existen cuatro tipos bsicos de optimizaciones que se pueden tratar en el
desarrollo de un software:
Memoria: intentar minimizar el uso de la memoria utilizada por nuestro
programa.
Tamao del ejecutable: que el tamao del ejecutable en disco sea lo
ms pequeo posible.
Eficiencia de ejecucin (procesamiento): que la aplicacin ejecute lo
ms rpido posible o utilizando la menor cantidad posible de CPU
Tamao datos: Ancho de banda, espacio en disco; que los datos que
utiliza, almacena o comunica a travs de cualquier canal sean lo ms
reducidos o compactos posibles.
Hay algunas optimizaciones que es capaz de realizar automticamente un buen
compilador, como detectar funciones inline o la tcnica de desenrollar bucles o
loop unrolling. Sin embargo, el compilador no puede suplir la labor del programador
en disear o usar un buen algoritmo, utilizar una estructura de datos eficiente o
seleccionar un formato adecuado.
Dadas las caractersticas habituales de los computadores actuales, en los que la
memoria y el almacenamiento en disco duro son muy abundantes, la optimizacin ms
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 78
Universidad Politcnica de Madrid -UPM
comn e importante es la de ejecucin de los programas, y es en la que ms se incide
en este tema.
5.4. VELOCIDAD DE EJECUCIN
La velocidad de ejecucin de un procesador se mide generalmente en Mflops o
Mflop/s, o millones de operaciones de coma flotante por segundo. Realmente es una
medida de procesamiento matemtico, que contabiliza el nmero de adiciones y
multiplicaciones de nmeros de coma flotante de precisin doble (64 bits) que realiza
un sistema cada segundo.
No obstante, una medida interesante y prctica en el desarrollo de aplicaciones
es el tiempo total de ejecucin que tarda un determinado algoritmo o cdigo en
ejecutar. Estas medidas son las que se realizan en este tema. Este enfoque contabiliza
el tiempo total del cdigo, no solo las operaciones aritmticas de coma flotante.
Recerdese que gran parte del cdigo se destina a estructuras de control, acceso a
memoria, direccionamiento de matrices y vectores, etc.
Existe un banco de pruebas (benchmark) denominado el test de Linpack que
consiste en la resolucin de un sistema de ecuaciones lineales denso (100, 1000
ecuaciones) mediante el mtodo del pivote parcial, midiendo la eficiencia real en
Mflops. Este test de Linpack es el que se utiliza tambin para clasificar los
computadores y establecer la lista de los ms rpidos (el Top 500 de los
supercomputadores), mediante una medida de eficiencia real en un calculo numrico
concreto. Ntese que muchos fabricantes proporcionan una medida de eficiencia de
pico o perfecta en base a su arquitectura, su velocidad de reloj, el tamao de su
memoria, etc. Pero esto no se ajusta a la eficiencia real, tal y como muestra la
siguiente tabla:
Tabla 7. Eficiencia en el test de Linpack (100 ecuaciones) para diversos procesadores

Los motivos por los que la eficiencia real no coincide con la de pico son muy
numerosos, ya que en la eficiencia de ejecucin influye el algoritmo, las optimizaciones
realizadas por el compilador y el programador, el tamao del problema, el sistema
operativo, etc. Tambin hay que insistir en que los resultados son una medida parcial
del rendimiento del sistema, ya que la eficiencia de un computador en la ejecucin de
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 79
Universidad Politcnica de Madrid -UPM
aplicaciones reales depende tambin de otros factores, como la velocidad de acceso a
disco, las capacidades de la tarjeta grafica, las comunicaciones con dispositivos, etc.


5.5. ALGUNAS TCNICAS

5.5.1 Casos frecuentes
Considrese la estructura if-else if que maneja distintos casos o
condiciones, las cuales puede suceder con distinta probabilidad:
if(condicion1)
{
//hacer algo1
}
else if(condicion2)
{
//hacer algo2
}
else if(condicion3)
{
//hacer algo3
}
Esta estructura va evaluando las condiciones hasta que encuentra una
condicin cierta. Si la condicin 1 es cierta solo es necesario hacer 1 comprobacin,
mientras que si es falsa, hacen falta al menos 2 clculos, el de la condicin 1 y el de la
2. Si las condiciones 1 y 2 son falsas, entonces son necesarias 3 operaciones. Se puede
decir que el nmero medio de evaluaciones de condicin necesarias es:
m
N =Pr(condicion1)*1+Pr(condicion2)*2+Pr(condicion3)*3
Pr(condicion)=probabilidad de que sea la primera cierta

As, si la probabilidad de que la primera condicin sea cierta es muy pequea,
por ejemplo del 1%, al igual que la segunda condicin, mientras que la de la tercera es
del 98%, el nmero medio de comprobaciones seria:
m
N =0,98*3+0,01*2+0,01*1=2,97
Si por el contrario, la probabilidad de la primera condicin fuera del 98%,
mientras que la de las otras dos fuera del 1%, el nmero medio de comprobaciones
seria:
m
N =0,01*3+0,01*2+0,98*1=1,03
Una implementacin de prueba se puede realizar obteniendo nmeros
aleatorios en el rango 0-100, y expresando las condiciones respecto a esos nmeros
aleatorios:
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 80
Universidad Politcnica de Madrid -UPM
void casos1()
{
float valor=100*rand()/(float)RAND_MAX;
if(valor>=99.0f)
{
}
else if(valor>=98)
{
}
else if(valor<98.0f)
{
}
}
void casos2()
{
float valor=100*rand()/(float)RAND_MAX;
if(valor<98.0f)
{
}
else if(valor<99.0f)
{
}
else if(valor>=99.0f)
{
}
}

int main(int argc, char* argv[])
{
tiempo();
for(int i=0;i<10000000;i++)
casos1();
tiempo();
for(i=0;i<10000000;i++)
casos2();
tiempo();
return 0;
}
El resultado de ejecutar este programa (en la mquina citada anteriormente)
seria el siguiente, donde se aprecia la ganancia en tiempo de cmputo:
Tiempo= 0.125000
Tiempo= 0.047000
Hay que resaltar, que al igual que antes el programa realiza exactamente la
misma funcin. Esta tcnica se puede resumir como: poner los casos frecuentes
primero
5.5.2 Bucles
5.5.2.1. Desenrollado de bucles
El desenrollado de bucles o loop unrolling es una tcnica que consiste en
repetir el cdigo interno de un bucle varias veces para evitar precisamente la iteracin
de dicho bucle. Ntese que en la mayora de los casos, los bucles solo sirven para
evitar la repeticin de cdigo al programador. Pero dicho bucle incurre en un coste
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 81
Universidad Politcnica de Madrid -UPM
computacional al realizar las operaciones necesarias. Los compiladores modernos son
generalmente capaces de detectar situaciones en las que el loop unrolling es posible
y lo realizan automticamente. Cuando el compilador es capaz de detectar que el
numero de iteraciones del bucle es fijo (constante), generalmente produce
internamente dicho desenrollado, mejorando la eficiencia de ejecucin del cdigo.
Solo en pocos casos en los que la eficiencia puede ser critica y el compilador no puede
detectarlo, se recurre al loop unrolling manual, en el que el programador lo realiza
directamente en cdigo. En aplicaciones criticas (avinica, por ejemplo), esto puede
llegar a ser una practica comn.
Imagnese un programa que tiene que inicializar un vector de 1000 elementos,
cada uno con un valor igual a su ordinal. Tal como ilustra el programa siguiente, eso se
puede realizar de la forma tradicional, o realizando un unrolling en este caso de 10
en 10, aunque este tamao puede variar. En el caso de optimizaciones automticas
realizadas por el compilador, este decidir el tamao del unrolling.
main(int argc, char* argv[])
{
int size=1000;
float vector[1000];
//Metodo 1
tiempo();
for(int i=0;i<100000;i++)
for(int j=0;j<size;j++)
vector[j]=j;
//Metodo 2
tiempo();
for(i=0;i<100000;i++)
for(int j=0;j<size;j+=10)
{
vector[j]=j++;
vector[j]=j++;
vector[j]=j++;
vector[j]=j++;
vector[j]=j++;
vector[j]=j++;
vector[j]=j++;
vector[j]=j++;
vector[j]=j++;
vector[j]=j++;
}
tiempo();
return 0;
}
Los tiempos de ejecucin muestran la ventaja de este procedimiento:
Tiempo= 0.266000
Tiempo= 0.093000
5.5.2.2. Invariantes en bucles
Al ser los bucles tareas repetitivas, en muchos casos un gran nmero de veces,
conviene prestar atencin a elementos repetitivos o invariantes dentro de los bucles.
El siguiente fragmento de cdigo tiene como objetivo rellenar el vector de 10000
componentes con unos valores que dependen del ordinal del elemento, as como de
dos variables a y b, que en este caso se obtienen aleatoriamente (aunque podran
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 82
Universidad Politcnica de Madrid -UPM
venir de otra parte de la aplicacin). El valor de dichas variables influyen no solo en el
clculo, sino que la relacin entre ambas establece que calculo se debe realizar, tal y
como se aprecia en el cdigo siguiente:
void bucle1()
{
double a=rand();
double b=rand();
double result[10000];
for(int j=0;j<10000;j++)
{
if(a<b)
result[j]=a*j/b;
else
result[j]=b*j/a;
}
}
Sin embargo, un anlisis de este cdigo nos muestra que la comparacin del
if(a<b) se est realizando 10000 veces de forma innecesaria, ya que los valores de
a y b no se modifican dentro del bucle. Por lo tanto, resulta ms eficiente sacar la
comparacin de dentro del bucle, y repetir el cdigo del bucle para cada uno de los
dos casos resultantes:
void bucle2()
{
double a=rand();
double b=rand();
double result[10000];
if(a<b)
for(int j=0;j<10000;j++)
result[j]=a*j/b;
else
for(int j=0;j<10000;j++)
result[j]=b*j/a;
}
En este caso, el compilador no ha sido capaz de detectar esta posibilidad, pero
si por ejemplo las variables a y b tuvieran valores constantes, el compilador quizas
si seria capaz de optimizar.
De hecho se puede ir ms lejos y detectar que no solo la comparacin es
invariante dentro del bucle, sino que parte de la operacin aritmtica realizada en la
asignacin de valores al vector tambin lo es. Por lo tanto podemos tambin extraer
dicho clculo y realizarlo una nica vez antes de comenzar los bucles:
void bucle3()
{
double a=rand();
double b=rand();
double result[10000];
double c=a/b;
if(a<b)
for(int j=0;j<10000;j++)
result[j]=j*c;
else
for(int j=0;j<10000;j++)
result[j]=j/c;
}
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 83
Universidad Politcnica de Madrid -UPM
Si ejecutamos las tres soluciones miles de veces:
main(int argc, char* argv[])
{
tiempo();
for(int i=0;i<10000;i++)
bucle1();
tiempo();
for(i=0;i<10000;i++)
bucle2();
tiempo();
for(i=0;i<10000;i++)
bucle3();
tiempo();
return 0;
}
Se obtienen los siguientes tiempos:
Tiempo= 2.516000
Tiempo= 1.562000
Tiempo= 0.891000
Una vez ms, la ganancia computacional es visible. Se han presentado en este
caso invariantes aritmticos y estructurales, pero tambin es posible extraer la
declaracin de objetos dentro de un bucle fuera del mismo para evitar la repetida
reserva de memoria.
De cualquier forma, tal y como se recomienda en las conclusiones,
generalmente no es necesario hacer un anlisis exhaustivo buscando estas
posibilidades dentro de los bucles mientras se programa. En general se programan los
bucles, si es necesario se analiza el rendimiento y si se aprecia alguna posible mejora
significativa y necesaria dentro del bucle, se implementa.
5.5.3 Gestin de memoria
El uso de memoria, la reserva y liberacin de memoria, ya sea utilizando
memoria dinmica o utilizando memoria esttica y dejando al compilador realizar la
tarea, lleva asociado un coste computacional. Generalmente este coste es muy
pequeo, ya que el manejo de memoria suele estar muy optimizado, pero puede ser
relevante en aplicaciones que manejen gran cantidad de datos o lo hagan de forma
muy repetitiva.
Si por ejemplo se desea copiar la informacin de una matriz tridimensional a
otra, o asignar todas sus componentes a cero, se podra implementar de la siguiente
forma:
int a[3][3][3];
int b[3][3][3];
for(i=0;i<3;i++)
for(j=0;j<3;j++)
for(k=0;k<3;k++)
b[i][j][k] = a[i][j][k];
for(i=0;i<3;i++)
for(j=0;j<3;j++)
for(k=0;k<3;k++)
a[i][j][k] = 0;
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 84
Universidad Politcnica de Madrid -UPM
No obstante, usando la contigidad en la reserva de memoria y los mecanismos
de asignacin por defecto entre objetos, se puede implementar exactamente lo mismo
de una forma mucho ms compacta y ms eficiente en su ejecucin, ya que se evitan
los bucles y se realiza directamente una copia de un bloque de memoria en otro:
typedef struct
{
int element[3][3][3];
} Three3DType;

Three3DType a,b;
...
b = a;
memset(a,0,sizeof(a));
Otra estrategia, cuando se requiere utilizar continuamente datos de dimensin
variable es reaprovechar la memoria ya reservada en caso de que sea posible. Una
solucin simple consistira en reservar memoria para cada nuevo conjunto de datos
(de dimensin n variable), utilizarlos, y a continuacin liberar la memoria utilizada.
while(continuar)
{
//obtener n

int* p=new int[n];

//hacer lo que sea

delete [] p;
}
Aunque este enfoque es eficiente desde el punto de vista del uso de memoria
(siempre se utiliza la mnima cantidad de memoria necesaria), la memoria suele ser
muy abundante. Sin embargo el coste computacional de la reserva y liberacin puede
ser relevante. En ese caso seria ms conveniente el siguiente enfoque, en el que solo
se libera memoria en caso de que no haya suficiente para almacenar los datos, para
reservar a continuacin el tamao necesario. Si se tiene un tamao reservado y se
necesita menos tamao, no se libera la memoria, sino que directamente se utiliza
(desaprovechando una parte). De esta forma, el tamao reservado se estabiliza en el
mximo necesario:
int max=0;
int* p;
while(continuar)
{
//obtener n
if(n>max)
{
delete [] p;
p=new int[n];
}
//hacer lo que sea
}
delete [] p;
Algo similar puede ocurrir por ejemplo usando la Standard Template Library
(STL). Si necesitamos crear una cadena de gran tamao, aadiendo uno a uno nuevos
caracteres, podramos hacer:
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 85
Universidad Politcnica de Madrid -UPM
std::string cadena;
for(i = 0; i < 1000; i++)
encoded.append(1, letra);
Sin embargo es mucho ms eficiente hacer que la cadena reserve
automticamente espacio para 1000 caracteres. De estar forma la adicin (append) de
caracteres funciona mucho ms rpido porque no tiene que redimensionar la memoria
interna dinmicamente:
std::string cadena;
cadena.reserve(1000);
for(i = 0; i < 1000; i++)
encoded.append(1, letra);

5.5.4 Tipos de datos
Cuando se decide utilizar un tipo de dato u otro, hay que tener en cuenta que
esto puede tener consecuencias en el coste computacional. Los procesadores actuales
tienen hardware dedicado para realizar operaciones con datos tanto de tipo real como
entero. Es posible por tanto, que el procesamiento de nmeros reales de precisin
simple (float) se realice ms rpidamente que el de los datos de precisin doble
(double). Curiosamente tambin es posible que ciertas operaciones con nmeros
enteros se realicen ms rpidamente con enteros de 4 bytes (int) que con enteros de
menor tamao (short o char), en ciertos procesadores de 32 bits, ya que su
arquitectura esta diseada para este tamao de datos.
Por otra parte, la seleccin de un tipo de datos u otro tambin puede tener una
seria repercusin en la memoria utilizada, en caso de estructuras de informacin muy
grandes. De igual forma, si esos datos se deciden guardar en el disco duro, un tamao
muy grande se traducir en un archivo que ocupe mucho espacio, adems del
consiguiente tiempo necesario para escribir en el disco, que en general es una
operacin relativamente lenta.
Tmese como ejemplo las imgenes, como las tomadas por una cmara o un
escner. Si una imagen normal tiene millones de pxeles, por ejemplo 1024x768, y cada
pxel necesita tpicamente representar la informacin de color (3 componentes),
entonces hacen falta aproximadamente 2.4 millones de datos. Las componentes de
color admiten una representacin comn como enteros en el rango 0-255. Si optamos
por utilizar variables de tamao 1 byte (unsigned char), entonces necesitaremos
2.4Mb de memoria para almacenar dicha imagen en memoria. Si por el contrario
utilizramos variables de tamao 4 bytes (int), entonces multiplicaramos
obviamente por 4, requiriendo aproximadamente 9.4Mb. El gasto de memoria es pues
considerable.
Si es el programador el que crea nuevos tipos de datos, es conveniente que
tenga en cuenta estos criterios. As por ejemplo, en la creacin de un nuevo tipo de
datos para representar matrices, puede ser muy interesante que el tipo de datos
contemple la posibilidad de codificar explcitamente distintas posibles
representaciones especiales de matrices como matrices diagonales, matrices
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 86
Universidad Politcnica de Madrid -UPM
simtricas, matrices triangulares o matrices dispersas, aprovechando estas
caractersticas especiales para conseguir una mayor eficiencia. La mayora de libreras
matemticas existentes que manejan matrices implementan esta funcionalidad.
5.5.5 Tcnicas en C++
El lenguaje de programacin C++ tiene algunas caractersticas que tienen que
ser tenidas en cuenta cuando se programa.
Por ejemplo, cuando se pasa un objeto a un mtodo por valor, de tal forma que
el mtodo no pueda modificar dicho objeto, se realiza una copia del objeto. Si el objeto
tiene un tamao en memoria importante se incurre en un coste computacional. Este
coste computacional puede ser evitado con el uso de referencias constantes:
void metodo(ClaseA a); //se realiza una copia de a
void metodo(const ClaseA& a);//no se realiza copia de a
El polimorfismo es una potente utilidad que puede ser utilizada para realizar
una buena ingeniera del software y un buen diseo utilizando patrones. No obstante,
hay que tener en cuenta que el polimorfismo (a travs de la virtualidad de mtodos),
tiene tambin un coste computacional asociado, ya que la decisin de a que funcin se
llama tiene que realizarse en tiempo de ejecucin. Esto no quiere decir que no haya
que utilizar el polimorfismo, simplemente que hay que tenerlo en consideracin como
posible factor en aplicaciones de uso de CPU muy intensivo, sobre todo si el
polimorfismo se encuentra en el ncleo computacional de la aplicacin.
La encapsulacin de datos dentro de clases utiliza tpicamente mtodos de
acceso a dichos datos. Una vez ms hay que tener en cuenta que la llamada de
mtodos tiene un coste computacional asociado. Si se tienen problemas de eficiencia
quizs puede ser necesario dejar los datos de una clase como public para poder
acceder a ellos directamente.
El uso de funciones inline puede mitigar este efecto, ya que el compilador
sustituye las llamadas a la funcin por el cdigo que est dentro, en vez de enlazar con
ella, tantas veces como sea necesario. As el tamao del ejecutable es algo mayor, pero
se evita la sobrecarga de invocacin de funciones. Aunque no se especifique un
mtodo como inline, el compilador tiene capacidad para detectar, decidir y
compilar como inline dicho mtodo, si con ello calcula que conseguir ms
eficiencia. El uso de funciones inline suele recomendarse con funciones de hasta un
mximo de 3 lneas.
El uso de constructores y destructores es tambin una interesante capacidad de
C++, pero tampoco hay que olvidar que los mismos tienen un coste computacional
asociado. Si el nmero de construcciones es elevado conveniente tener en cuenta que
la inicializacin en la construccin es ms eficiente que la asignacin. As si tenemos la
siguiente clase:
ClaseA{
ClaseA(ClaseB b);
protected:
ClaseB B;
};
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 87
Universidad Politcnica de Madrid -UPM
Una implementacin del constructor podra hacer:
ClaseA::ClaseA(ClaseB b)
{
B=b;
}
Sin embargo es ms eficiente:
ClaseA::ClaseA(ClaseB b):B(b)
{
}

5.6. CASOS PRCTICOS
Se presentan en esta seccin algunos ejemplos concretos que permiten
profundizar algo ms en algunos conceptos, a la vez que proporcionan una idea de
escenarios ms reales de aplicacin.
5.6.1 Algortmica vs. Matemticas
Ahora se quiere realizar un programa que necesita calcular la suma de los n
primeros nmeros naturales:
n
i=1
i
La forma que viene inmediatamente a la cabeza del programador es la
utilizacin de un bucle para realizar dicho sumatorio, resultando en:
int Suma1(int n)
{
int i, sum = 0;
for (i = 1; i <= n; i++)
sum += i;
return sum;
}
No obstante, existe una solucin cerrada o analtica a este sumatorio:
n
i=1
( 1)
i
2
n n

La implementacin de esta solucin es inmediata:
int Suma2(int n)
{
int sum = (n * (n+1)) / 2;
return sum;
}

Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 88
Universidad Politcnica de Madrid -UPM
Para contabilizar algn tiempo distinto de cero (para la segunda solucin), es
necesaria la repeticin del clculo 10 millones de veces:
int main(int argc, char* argv[])
{
tiempo();
for(int i=0;i<10000000;i++)
Suma1(1000);
tiempo();
for(i=0;i<10000000;i++)
Suma2(1000);
tiempo();
return 0;
}
La salida por pantalla en la mquina anteriormente descrita es la siguiente:
Tiempo= 8.437000
Tiempo= 0.032000
En este caso queda de relevancia una abismal diferencia entre una solucin u
otra. Adems tambin es importante destacar que esta optimizacin de ninguna
manera podr ser nunca incluida por el compilador. Se puede concluir que nada
sustituye el razonamiento y el conocimiento de un buen ingeniero software.
Resaltamos que no es suficiente con ser un buen programador y conocer el lenguaje.
Un buen ingeniero software tiene que tener slidas bases de matemticas, fsica, etc.
5.6.2 Generacin de nmeros primos
En el apartado anterior se ha convertido una solucin algortmica en una
solucin cerrada o analtica (matemtica). Esto no siempre es posible, y muchas veces
una solucin algortmica es totalmente necesaria. No obstante, la importancia de
elegir un algoritmo u otro es bien conocida. En problemas tpicos, como la ordenacin
de un vector segn algn criterio, se conoce bien que algoritmos funcionan ms
rpidamente que otros y se puede elegir entre un conjunto el ms conveniente para
nuestra aplicacin. De hecho muchas libreras implementan las distintas opciones para
eleccin del usuario.
Sin embargo, en numerosas ocasiones el programador tendr que desarrollar
su propio algoritmo. Imagnese que se necesita programar una aplicacin que
clasifique los primeros N nmeros enteros en primos y no primos. Por motivos de
implementacin se decide utilizar un vector de enteros con significado booleano, en el
que el ndice u ordinal del elemento corresponde al numero, y el valor del vector (0:
falso, no primo, 1: verdadero, primo) corresponde a la clasificacin realizada. Dicho
vector es pasado como parmetro a una funcin que es la encargada de rellenar dicho
vector con los valores adecuados.
En una primera aproximacin, resulta lgico recorrer los N primeros nmeros
enteros y para cada uno de ellos estudiar si es primo o no lo es. La forma de hacerlo es
comprobar si es divisible por los nmeros enteros menores que el. A priori se supone
que el numero es primo (es_primo[i]=1;). Si al realizar una divisin, se
comprueba que no lo es, se marca como no primo y se termina la comprobacin. Un
sencillo anlisis matemtico revela que no es necesario probar la divisibilidad por
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 89
Universidad Politcnica de Madrid -UPM
todos los nmeros inferiores al considerado, sino nicamente por los nmeros hasta la
raz cuadrada del nmero considerado.
void Metodo1(int es_primo[],int n)
{
for(int i=0;i<n;i++)
{
es_primo[i]=1;
for(int j=2;j<=sqrt(i);j++)
{
if(i%j==0)
{
es_primo[i]=0;
break;
}
}
}
}
Sin embargo en este caso existe un enfoque mucho ms eficiente, consistente
en una solucin inversa, en la que en vez de ir analizando cada numero si es o no
primo mediante divisiones, vamos a ir eliminando nmeros que sabemos que no son
primos. La solucin anterior se puede considerar una solucin hacia atrs, mientras
que la propuesta ahora es una solucin hacia delante. Es decir, si cogemos el nmero
2, podemos realizar una especie de tabla de multiplicar y concluir rpidamente que los
nmeros 4, 6, 8, etc. no son primos. A continuacin podemos repetir el razonamiento
con el numero 3, concluyendo que los nmeros 6, 9, 12, etc. tampoco son primos.
Podramos proceder as con todos los nmeros, pero ms eficiente aun es hacerlo solo
sobre los primos. Si el numero 4 ha sido ya marcado como no primo, entonces lo
omitimos del proceso, ya que sus mltiplos (8, 12, 16, etc.) tambin habrn sido ya
marcados como no primos, y por lo tanto seria redundante e innecesario.
La implementacin de este mtodo quedara como sigue:
void Metodo2(int es_primo[],int n)
{
for(int i=0;i<n;i++)
es_primo[i]=1;

i=2;
while(i<n)
{
for(int j=2;i*j<n;j++) //marcar no primos
{
es_primo[i*j]=0;
}
do //buscar siguiente primo
{
i++;
}
while(i<n && !es_primo[i]);
}
}
La utilizacin de estos dos mtodos, incluyendo vectores de dimensin
dinmica, y la comprobacin de que ambos resultados son idnticos seria:

Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 90
Universidad Politcnica de Madrid -UPM
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <sys/timeb.h>
#include <memory.h>

int main(int argc, char* argv[])
{
printf("Introduce n=");
int n=0;
scanf("%d",&n);
int* es_primo=new int[n];
int* es_primo2=new int[n];

//METODO 1
tiempo();
Metodo1(es_primo,n);
tiempo();

//METODO 2
Metodo2(es_primo2,n);
tiempo();

if(0==memcmp(es_primo,es_primo2,n*sizeof(int)))
printf("Iguales\n");
else
printf("Error, diferentes\n");

delete [] es_primo;
delete [] es_primo2;
return 0;
}
El resultado de ejecutar este cdigo en la mquina anteriormente descrita es el
siguiente:
Introduce n=1000000
Tiempo= 0.953000
Tiempo= 0.047000
Como anteriormente, se pone de relieve una gran ganancia en tiempo de
cmputo, ya que el segundo mtodo es unas 20 veces ms rpido, gracias al nuevo
mtodo.
5.6.3 Pre-computacin de datos
El sensor LMS200 de SICK es un sensor lser que proporciona 181 medidas de
distancia (rango) en un intervalo de 180 grados, es decir una medida cada grado. Este
sensor se utiliza en numerosas aplicaciones industriales, seguridad, robtica, etc.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 91
Universidad Politcnica de Madrid -UPM

Figura 5-1. Sensor lser LMS200 de SICK
Este sensor proporciona de manera continua dichos datos de rango-distancia,
que deben de ser procesados por el computador si se desea obtener en coordenadas
cartesianas el perfil del objeto escaneado, o las coordenadas de los posibles obstculos
u objetos. Una solucin al problema, implementada mediante una funcin podra ser:
void Cartesianas1(double rango[],double x[],double y[])
{
for(int i=0;i<=180;i++)
{
x[i]=rango[i]*cos(i);
y[i]=rango[i]*sin(i);
}
}
Las operaciones ms costosas (o lentas) en el cdigo anterior son las funciones
trigonomtricas de seno y coseno. A primera vista parece que no se puede evitar dicho
clculo, lo que es cierto. Pero tambin es cierto que entre diferentes llamadas a la
funcin, los ngulos de los que se calcula el seno y el coseno son siempre los mismos,
de 0 a 180, con intervalos de 1 grado. Por tanto, se puede evitar tener que recalcular
dichos valores en cada llamada a la funcin.
Para ello podemos optar por pre-calcular unos vectores declaramos como
variables globales por simplicidad. Tngase en cuenta que una solucin real utilizara
algn otro mecanismo mejor desde el punto de vista de la ingeniera del software
como variables estticas, variables miembro de un clase, etc. El calculo de los valores
lo realizamos en una funcin que solo necesitar ser llamada una nica vez. La funcin
de clculo de coordenadas cartesianas utilizar ahora los valores precomputados en
lugar de recurrir a las funciones matemticas originales.
double sin_alfa[181],cos_alfa[181];
void PrecomputaDatos()
{
for(int i=0;i<=180;i++)
{
cos_alfa[i]=cos(i);
sin_alfa[i]=sin(i);
}
}
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 92
Universidad Politcnica de Madrid -UPM
void Cartesianas2(double rango[],double x[],double y[])
{
for(int i=0;i<=180;i++)
{
x[i]=rango[i]*cos_alfa[i];
y[i]=rango[i]*sin_alfa[i];
}
}
Como en otros casos anteriores, cabe resaltar que no estamos haciendo aqu
ninguna aproximacin numrica ni simplificacin del problema. El resultado numrico
ser idntico para ambas soluciones.
Ejecutamos ambos mtodos miles de veces. Tngase en cuenta que esto no
difiere mucho de la realidad, ya que en la prctica el sensor esta proporcionando datos
de forma continua al computador.
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <sys/timeb.h>

int main(int argc, char* argv[])
{
double x[181],y[181],rango[181];
for(int i=0;i<=180;i++)
rango[i]=rand()/(float)RAND_MAX;//simular medidas
//Metodo 1
tiempo();

for(int j=0;j<10000;j++)
Cartesianas1(rango,x,y);

//Metodo 2
tiempo();
PrecomputaDatos();
tiempo();
for(j=0;j<10000;j++)
Cartesianas2(rango,x,y);

tiempo();

return 0;
}
En la ejecucin se aprecia que el clculo de las funciones trigonometricas
efectivamente tiene un elevado coste asociado. La precomputacion de los valores es
prcticamente despreciable, pero supone un gran ahorro de tiempo (unas 20 veces
ms rpido)
Tiempo= 0.328000
Tiempo= 0.000000 (precomputo)
Tiempo= 0.015000
En otros casos puede resultar que los senos y los cosenos no sean siempre los
de los mismos ngulos. Aun as en esos casos se puede implementar una solucin
interpolada, en la que se precomputan unas tablas con valores distribuidos siguiendo
unos determinados intervalos. Dichas tablas se utilizan mediante interpolacin para
cualquier valor intermedio. La solucin as programada es una aproximacin numrica
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 93
Universidad Politcnica de Madrid -UPM
la solucin real, con un cierto error. No obstante, la ganancia en velocidad puede
suponer una ventaja frente a la precisin numrica. Algunas aplicaciones de grficos
interactivos, videojuegos y tarjetas graficas utilizan tcnicas basadas en este concepto.
5.7. OBTENIENDO PERFILES (PROFILING) DEL CDIGO
Aunque se puede analizar un programa midiendo tiempos de la forma que lo
hemos hecho, es poco practico. Para analizar la ejecucin de cdigo, existen
herramientas (denominadas Profilers) que permiten ejecutar el cdigo, contabilizando
las llamadas a las funciones, el tiempo que emplea cada lnea del programa, etc.
mostrando informes como resultado de dicho anlisis.
En general, cuando se realiza un programa real, en el que el coste
computacional es importante, es necesario utilizar un profiler para analizar donde se
utilizan los recursos. Es importante resaltar que generalmente mejorando un 20% del
cdigo se puede conseguir un 80% de las optimizaciones posibles. Por tanto no merece
la pena disear absolutamente todo el cdigo condicionado a la eficiencia.
Simplemente analizando los cuellos de botella y mejorando ciertos aspectos se puede
conseguir un buen resultado con una carga de trabajo razonable.
El Visual Studio tiene incorporado un profiler que permite analizar algunos
tiempos de ejecucin de nuestro programa. Para activarlo es necesario ir a Project
Settings->Link->Enable profiling. A continuacin se reconstruye el proyecto (Rebuild
all). Para ejecutar el profiler, iremos a Menu->Build->Profile.
En el programa anterior, se obtiene el siguiente resultado:
Profile: Function timing, sorted by time
Date: Thu Jan 15 17:07:04 2009



Program Statistics
------------------
Command line at 2009 Jan 15 17:07: "F:\...........\Precomputo"
Total time: 251,998 millisecond
Time outside of functions: 6,875 millisecond
Call depth: 2
Total functions: 7
Total hits: 20006
Function coverage: 71,4%
Overhead Calculated 7
Overhead Average 7


Module Statistics for precomputo.exe
------------------------------------
Time in module: 245,123 millisecond
Percent of time in module: 100,0%
Functions in module: 7
Hits in module: 20006
Module function coverage: 71,4%


Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 94
Universidad Politcnica de Madrid -UPM
Func Func+Child Hit
Time % Time % Count Function
---------------------------------------------------------
228,48 93,2 228,48 93,2 10000 Cartesianas1(..) (precomputo.obj)
16,001 6,5 16,001 6,5 10000 Cartesianas2() (precomputo.obj)
0,401 0,2 245,12 100,0 1 _main (precomputo.obj)
0,213 0,1 0,213 0,1 4 tiempo(void) (precomputo.obj)
0,023 0,0 0,023 0,0 1 PrecomputaDatos(void) (precomputo.obj)
Otros entornos como Matlab, tienen Profilers ms avanzados, que permiten un
anlisis ms en profundidad y emiten informes ms completos, incluyendo grficos.
Como ejemplo, podemos analizar el siguiente cdigo Matlab:
function pruebaProfile

A=randn(10,10);
b=randn(10,1);

for i=1:20000

x1=inv(A)*b;
x2=A\b;
if(x1~=x2)
disp "error";
display x1;
display x2;
end

B=A*A;
C=A+randn(10,10);
D=A+C*C;

end
Para activar el Profiler y analizar este cdigo, se realiza en la lnea de
comandos:
>> profile on;
>> pruebaProfile
>> profile off
>> profile report
El resultado final es el siguiente. Aunque Matlab tambin tiene un visor
especifico para el Profiler, que tambin dispone de ms funcionalidad.

Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 95
Universidad Politcnica de Madrid -UPM

Figura 5-2. Profiling de una funcin programada en Matlab
La figura anterior muestra el tiempo utilizado en cada lnea, tanto en tiempo
total como en porcentaje. Se aprecia que la solucin del sistema Ax=b es lo que ms
tiempo utiliza. Esta solucin se puede hacer de dos formas posibles, mediante la
inversa (x1=Inv(A)*b), o mediante eliminacin de Gauss (x2=A\b). El profiler
demuestra como la segunda opcin es ms eficiente, lo que refuerza la importancia de
la utilizacin de un algoritmo adecuado para la solucin de un problema.
5.8. CONCLUSIONES
Aunque en este tema se han presentado varias tcnicas de optimizacin para
un cdigo ms eficiente, esto no quiere decir que el programador deba perder tiempo
en implementar todo su cdigo teniendo en cuenta dicha eficiencia. En este apartado
nos gustara pues resumir algunas ideas importantes:
No ofuscarse en la eficiencia del cdigo. Segn Donald Knuth
premature optimization is the root of all evil. Centrarse en el diseo,
la correccin y la ingeniera del software y dejar el problema de la
eficiencia para el final, con el uso de un profiler.
No hay que asumir que algunas operaciones son ms rpidas que otras.
Benchmark everything. Medir tiempos. Utilizar siempre un profiler.
Reducir cdigo no implica siempre eficiencia. Recurdese el loop
unrolling
Si se pueden tener en cuenta algunas optimizaciones tpicas y sencillas
sobre la marcha, como es el paso de parmetros por referencia
constante, que es una practica habitual en buenos programadores C++.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 96
Universidad Politcnica de Madrid -UPM
Muchas veces hay que realizar un balance. El uso de optimizaciones
para un cdigo ms eficiente a veces es contrapuesto a la buena
ingeniera, a la legibilidad y compresin del cdigo, a la encapsulacin, a
la modularidad. Otras veces, la velocidad puede requerir mucha
memoria, y hay que tomar una decisin de compromiso entre eficiencia
de ejecucin y uso de otros recursos.
Se recomienda el uso de componentes desarrollados y probados, ya que
generalmente estos ya han tenido en cuenta criterios de eficiencia.
Estudiar y seleccionar los algoritmos y estructuras de datos ms
eficientes para nuestro problema.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 97
Universidad Politcnica de Madrid -UPM


6
6
.
.

SERIALIZACIN DE DATOS
6.1. INTRODUCCIN
La serializacin de datos (marshalling en ingles), es el proceso de codificar un
conjunto de informacin o datos (objetos en programacin Orientada a Objetos), en
una estructura de informacin lineal o serie de bytes. Este proceso es necesario para
almacenar datos en un dispositivo de almacenamiento, enviar datos por mecanismos
de comunicacin serie (puertos serie, USB, por red TCP/IP). La serie de bytes puede ser
utilizada posteriormente para recuperar la informacin, y volver a generar la
estructura de informacin original.
La serializacin es pues un mecanismo muy utilizado para transportar objetos
por la red, hacer persistente objetos en ficheros o bases de datos, etc. Es por tanto una
tcnica necesaria en sistemas distribuidos, pero no se restringe a ellos.
Aunque muchos lenguajes de programacin incluyen soporte nativo para
serializacin de datos, este soporte puede no ser suficiente en casos de estructuras de
informacin dinmicas creadas por el usuario, o en el caso en que el usuario deba
decidir que informacin es la relevante para ser transmitida o almacenada y cual no.
Siguiendo el planteamiento practico de este libro, se propone un ejemplo como
gua de este captulo. Igualmente, este captulo no pretende ser un anlisis riguroso ni
una solucin completa al problema de la serializacin de datos, sino simplemente dar
al lector una perspectiva del problema y algunas ideas para abordarlo. No obstante, las
metodologas presentadas en el captulo pueden ser mas que suficientes para abordar
programas relativamente simples como la aplicacin distribuida propuesta en la
segunda parte.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 98
Universidad Politcnica de Madrid -UPM
En el ejemplo que se propone se tiene una escena (que podra pertenecer a un
juego de ordenador, una simulacin, un salva pantallas) consistente en un bosque.
Para el dibujo se ha utilizado OpenGL y para la gestin de las ventanas se utiliza la
librera GLUT.
Dicho bosque esta formado por una serie de rboles en distintas posiciones,
con distintas alturas, colores y tamaos de copa. El cdigo correspondiente a la escena
de la figura se puede encontrar en el cdigo adjunto a este libro.

Figura 6-1. Representacin grafica de la escena cuyos datos se van a serializar
La declaracin de la clase Bosque es la siguiente. Como se aprecia, todos los
atributos de las clases son pblicos. En un buen diseo, esto no debera ser as, pero
para nuestro caso se prefiere por simplicidad didctica.
#include "Arbol.h"
#define MAX_ARBOLES 100

class Bosque
{
public:
Bosque();
void Aleatorio(int num_arboles);
void Dibuja();
void PideDatos();
void Imprime();

int numero;
Arbol arbol[MAX_ARBOLES];
};
La clase Arbol, aparte de la posicin, esta fundamentalmente compuesta por
un cilindro (tronco) y una esfera (copa):

Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 99
Universidad Politcnica de Madrid -UPM
#include "Cilindro.h"
#include "Esfera.h"

class Arbol
{
public:
void Dibuja();
void PideDatos();
void Imprime();

float x;
float y;
Cilindro tronco;
Esfera copa;
};
Y por ltimo, las clases Esfera y Cilindro son parametrizaciones
sencillas de las primitivas correspondientes:
class Esfera
{
public:
void Dibuja();
void PideDatos();
void Imprime();

float radio;
unsigned short verde,rojo,azul;
};
class Cilindro
{
public:
void Dibuja();
void Imprime();
void PideDatos();
Cilindro();
virtual ~Cilindro();
float radio;
float altura;
};
El diagrama de clases de diseo que representa estas clases es el siguiente:
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 100
Universidad Politcnica de Madrid -UPM

Figura 6-2. Diagrama de Clases de Diseo
No obstante, es destacable la estructuracin de los objetos. El siguiente
diagrama muestra la disposicin en rbol de la informacin. El bosque esta compuesto
por una serie de rboles, y cada uno de ellos tiene su propia copa y su propio tronco.
Tambin es de relevancia la distribucin de responsabilidades, y el flujo
recursivo de invocaciones. En el diagrama se han mostrado algunos mensajes
correspondientes a la responsabilidad de dibujar el entorno. Cuando el gestor de
ventanas GLUT decide redibujar, acaba llamando al mtodo Bosque::Dibuja().
Este mtodo a su vez delega, llamando al mtodo Arbol::Dibuja(), para cada
uno de los rboles que lo componen. A su vez, cada rbol se dibuja a si mismo,
diciendo a sus componentes (el tronco y la copa) que se dibujen. Se puede decir que es
una aplicacin del patrn Experto en Informacin, ya que cada objeto es responsable
de pintarse a si mismo, dado que el tiene la informacin necesaria para pintarse. As,
se va procediendo, avanzando primero en profundidad y luego en anchura en el rbol
de informacin representado en la figura.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 101
Universidad Politcnica de Madrid -UPM

Figura 6-3. Estructura de objetos de la aplicacin
As, los mtodos correspondientes realizan invocaciones a los mtodos de los
objetos que los componen, tal y como se muestra (como ejemplo) para la funcin de
dibujo del bosque.
void Bosque::Dibuja()
{
int i;
for(i=0;i<numero;i++)
{
arbol[i].Dibuja();
}
}
Asimismo, tambin existen en las clases funciones que permiten solicitar los
datos de un nuevo bosque al usuario para que los teclee por pantalla, mostrar
(imprimir) por pantalla los datos de un bosque y generar un bosque aleatorio de un
determinado numero de rboles.
Supngase en este punto que es necesario almacenar toda la informacin de
este bosque en un archivo en el disco duro, para luego poder recuperarlo. O que como
la escena forma parte de un juego distribuido, y todos los jugadores se deben mover
en la misma escena, es necesario empaquetar en un vector de bytes la escena para
enviarla por la red, de tal forma que pueda ser recuperado en un computador remoto.
Se plantean a continuacin distintas alternativas.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 102
Universidad Politcnica de Madrid -UPM
6.2. REPRESENTACIN OBJETOS EN MEMORIA
Un objeto almacena sus variables de forma contigua en memoria. Supngase
que se declara un objeto de la clase Arbol, y se le solicitan los datos al usuario.
Arbol arbol;
a.PideDatos();
Al realizar dicha declaracin se reserva un espacio en memoria como el que se
ilustra en la figura siguiente, en el que se van reservando recursivamente espacio para
las variables y los objetos de los que se compone, con un ordenamiento que sigue el
establecido en la declaracin (.h) de la clase.

Figura 6-4. Almacenamiento en memoria de un objeto tipo rbol
Esta propiedad puede ser utilizada para una fcil serializacin de los datos.
Supngase que se quiere almacenar los datos de dicho rbol en un fichero, para su
posterior recuperacin. Si se abre el archivo en modo binario y se realiza una escritura
sin formato mediante fwrite(), se puede volcar una copia completa de los datos
del rbol al archivo.
FILE* f=fopen("Arbol.txt","wb");
fwrite(&arbol,sizeof(Arbol),1,f);
Como el archivo es binario, si se intenta abrir con un editor de texto, no se
encontrar ninguna informacin inteligible por el humano. Pero si posteriormente se
desea recuperar la informacin de dicho archivo, sobre un objeto de la clase Arbol
(que no necesita inicializar ni pedir sus datos, ya que sern asignados en la lectura),
basta con realizar los siguientes pasos:
Arbol a;
FILE* f=fopen("Arbol.txt","rb");
fread(&a,sizeof(Arbol),1,f);
El mismo razonamiento puede aplicar a todo el bosque, de tal forma que podra
ser almacenado en un fichero mediante:
FILE* f=fopen("Bosque.txt","wb");
fwrite(&bosque,sizeof(bosque),1,f);
Y posteriormente recuperado:
FILE* f=fopen("Bosque.txt","rb");


x
y
radio
altura
radio
rojo
verde
azul
copa
tronco
arbol
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 103
Universidad Politcnica de Madrid -UPM
fread(&bosque,sizeof(bosque),1,f);
De igual forma, si hubiramos deseado enviar los datos del bosque por la red,
podramos haber escrito en un vector de bytes la informacin, para posteriormente
enviar ese vector de bytes por el socket correspondiente (aunque realmente este es
un paso que se puede obviar en este caso)
char* buffer=new char[sizeof(bosque)];
memcpy(buffer,&bosque,sizeof(Bosque));
Y de la misma forma podramos recuperar la informacin del vector mediante:
memcpy(&bosque,buffer,sizeof(Bosque));
Aunque a primera vista podra parecer que ya hemos resuelto el problema de la
serializacin, esto no es cierto. En este caso funciona ya que toda la memoria es
esttica, incluido el vector de tamao variable del bosque:
int numero;
Arbol arbol[MAX_ARBOLES];
No obstante, ya hay un problema de eficiencia importante. Siempre se estn
serializando el nmero mximo posible de rboles, aunque nuestro bosque tenga
muchos menos. Esto implica un mayor tamao de archivo o un mayor tamao del
buffer para enviar por la red, con el consiguiente despilfarro de recursos del sistema.
La implementacin ha sido realizada as por simplicidad y evitar la memoria
dinmica. Pero en realidad, la capacidad del vector de rboles debera ser gestionada
dinmicamente. Que pasara si esto fuera as? Que la copia de memoria no seria
valida, ya que se estara copiando nicamente un puntero, que posteriormente no ser
valido.
Otro motivo por el que este esquema de serializacin puede ser no valido es el
hecho de que no se requiera serializar todos los datos de un objeto, sino solo algunos
de ellos. Esto es algo muy comn, ya que muchas clases contienen como variables
miembro variables auxiliares o temporales que se requieren para el funcionamiento
interno de la clase, pero que no tienen mas alcance. Siguiendo el mtodo anterior se
serializan el 100% de las variables miembro de la clase, sean relevantes o necesarias o
no. En este ultimo caso, tambin se esta incurriendo en un gasto innecesario de los
recursos del sistema.
Para solucionar estos problemas, el programador puede desarrollar su propia
estrategia de serializacin, que le permita gestionar que variables se serializan y cuales
no, as como gestionar adecuadamente la memoria dinmica de los objetos. Se
presentan a continuacin algunos enfoques tpicos.

6.3. SERIALIZACIN EN C
Aunque la estructura bsica de la aplicacin es Orientada a Objetos, la
serializacin tambin tiene que ser realizada en aplicaciones en C. Se presentan en
esta seccin algunas tcnicas para realizar esta tarea recurriendo nicamente a
funciones de C.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 104
Universidad Politcnica de Madrid -UPM
Generalmente se puede clasificar la serializacin en:
Con formato (texto). La informacin se almacena de tal forma que un
humano puede leerla, interpretarla e incluso modificarla fcilmente.
Para ello se almacena como cadena de caracteres (ASCII), en la que
incluso se pueden almacenar caracteres especiales para facilitar la
lectura como tabulaciones y retornos de carro.
Sin formato (binaria). La informacin se almacena como un vector de
bytes en el que se almacenan byte a byte (en codificacin binaria) todos
los datos, sin necesidad de separarlos por caracteres especiales. El
resultado es ininteligible por un humano.
Cuando se utiliza el formato, la representacin de un dato del mismo tipo
puede tener distinta longitud. Por ejemplo para representar el entero 12 hacen falta
solo 2 bytes (2 caracteres, uno para el 1 y otro para el 2), mientras que para el
123456 haran falta 6 bytes. Lo mismo sucede con nmeros de coma flotante como el
0.1 o el 3.1415. Sin embargo en formato binario, los datos ocupan siempre
exactamente el mismo tamao. Por ejemplo un entero puede ocupar siempre 4 bytes,
al igual que un float. La conclusin es que en general, el formato binario es mas
eficiente (necesita menos espacio), ya que adems no necesita separadores, y tiene la
ventaja aadida de no tener ninguna perdida de precisin numrica, por redondeos o
formatos. Por el contrario presenta la desventaja de no poder ser analizada fcilmente
por un humano.
6.3.1 Con formato (texto)
La serializacin con formato en C es bastante tediosa. Por una parte hay que
desarrollar cdigo segn se desee serializar a un archivo o a una cadena de texto para
su envo por red. Tambin se requiere un uso intensivo de las funciones de manejo de
cadenas sprintf(), sscanf(), strcat(), strcpy(), etc., ya que escribir
con formato en una cadena no es una tarea obvia.
Esta variante no ser desarrollada en este captulo. Se deja al lector como
ejercicio, que poda desarrollar fcilmente una vez ledas y comprendidas las secciones
siguientes.
6.3.2 Sin formato (binaria)
La serializacin binaria se apoya sobre una serie de macros write que van
insertando en un vector de bytes los datos correspondientes, que pueden ser de tipo
char (carcter o entero de 1 byte), short (entero de 2 bytes), long (entero de 4
bytes), float (real de 4 bytes) o double (real de 8 bytes). Por cada una de ellas
existe la contraria read, que sirve para extraer del vector la variable.
#define writeChar(x,y,z){x[y++] = z;}
#define writeShort(x,y,z){*((unsigned short*)((char*)&x[y]))=z; y+=2;}
#define writeLong(x,y,z){*((unsigned long *)((char*)&x[y]))=z; y+=4;}
#define writeFloat(x,y,z){*((float *)((char *)&x[y])) = z; y += 4;}
#define writeDouble(x,y,z){*((double *)((char *)&x[y])) = z; y += 8;}
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 105
Universidad Politcnica de Madrid -UPM

#define readChar(x, y, z) {z = x[y++];}
#define readShort(x,y,z){z=*(unsigned short*)((char *)&x[y]); y += 2;}
#define readLong(x,y,z){z=(*(unsigned long*)((char *)&x[y])); y += 4;}
#define readFloat(x, y, z) {z = (*(float *)((char *)&x[y])); y += 4;}
#define readDouble(x, y, z){z = (*(double *)((char *)&x[y])); y += 8;}
Los tres argumentos de la macro son el buffer o vector de bytes, la posicin o
ndice del vector de bytes y la variable. Se puede considerar todos ellos como pasados
por referencia, ya que la macro puede modificar (y modifica) sus valores. Cabe
destacar el aumento automtico del ndice segn el tamao de la variable, de tal forma
que el usuario de las macros puede despreocuparse de esta cuenta.
Para implementar la funcionalidad de serializacin y deserializacion, seguimos
con la estructura establecida para el dibujo y siguiendo el patrn del Experto en
Informacin, y aadimos a cada una de las clases (Bosque, Arbol, Cilindro,
Esfera) los siguientes mtodos:
void Read(char cad[],int& cont);
void Write(char cad[],int& cont);
Ntese que el paso del contador cont a las funciones se hace por referencia,
de tal forma que la funcin pueda incrementar dicho contador. La implementacin de
estos mtodos para el bosque seria:
void Bosque::Write(char cad[], int& cont)
{
writeChar(cad,cont,numero);
int i;
for(i=0;i<numero;i++)
arbol[i].Write(cad,cont);
}

void Bosque::Read(char cad[], int& cont)
{
readChar(cad,cont,numero);
int i;
for(i=0;i<numero;i++)
arbol[i].Read(cad,cont);
}
Ntese como lo primero que hacen las funciones es gestionar el nmero de
rboles que componen el bosque. Aunque tambin se pueden plantear otras
soluciones que no requieren el almacenamiento explicito de este tamao, su
utilizacin simplifica mucho la solucin. Tambin es importante recordar que con estas
funciones, ya no importa si el vector de rboles ha sido creado esttica o
dinmicamente.

La clase Arbol a su vez procede de forma similar:
void Arbol::Write(char cad[], int& cont)
{
writeFloat(cad,cont,x);
writeFloat(cad,cont,y);
tronco.Write(cad,cont);
copa.Write(cad,cont);
}

Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 106
Universidad Politcnica de Madrid -UPM
void Arbol::Read(char cad[], int& cont)
{
readFloat(cad,cont,x);
readFloat(cad,cont,y);
tronco.Read(cad,cont);
copa.Read(cad,cont);
}
En este caso es el programador el que decide el que se serializa, con que
formato y en que orden. Es importante por tanto que se respeten estos criterios en el
desempaquetamiento de los datos, ya que de no hacerlo el resultado ser incorrecto.
No obstante, la implementacin de la de-serializacin correspondiente siguiendo el
diseo realizado y el patrn Experto en Informacin, se ubica en el mismo lugar,
siendo fcil la comprobacin de la necesaria simetra.
La serializacin se completa con las funciones correspondientes en Cilindro
y Esfera:
void Cilindro::Write(char cad[], int& cont)
{
writeFloat(cad,cont,radio);
writeFloat(cad,cont,altura);
}
void Cilindro::Read(char cad[], int& cont)
{
readFloat(cad,cont,radio);
readFloat(cad,cont,altura);
}
void Esfera::Write(char cad[], int &cont)
{
writeFloat(cad,cont,radio);
writeChar(cad,cont,rojo);
writeChar(cad,cont,verde);
writeChar(cad,cont,azul);
}
void Esfera::Read(char cad[], int& cont)
{
readFloat(cad,cont,radio);
readChar(cad,cont,rojo);
readChar(cad,cont,verde);
readChar(cad,cont,azul);
}
Una vez realizada esta implementacin, podemos realizar la serializacin de un
bosque de la siguiente forma:
bosque.Aleatorio(50);

char buffer[3000];
int cont=0;
bosque.Write(buffer,cont);
Ntese que en esta implementacin se supone que el buffer tiene capacidad
suficiente para almacenar dicha informacin, y no se realiza ninguna comprobacin al
respecto. Esto, obviamente, no es una solucin ni valida ni completa, ya que el buffer
podra ser pequeo y producirse un desbordamiento, con el consiguiente error en
tiempo de ejecucin. En una solucin real se debe al menos comprobar que el tamao
del buffer (que puede ser pasado en otro parmetro) es suficiente, aunque tambin
seria adecuada la posibilidad de consultar primero el espacio necesario, o utilizar
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 107
Universidad Politcnica de Madrid -UPM
memoria dinmica redimensionando el buffer cuando sea necesario. Aun as, la
solucin expuesta describe adecuadamente la naturaleza del diseo adoptado, que
puede ser fcilmente extensible a dicha comprobacin.
La extraccin de la informacin se realizara pues de la siguiente forma:
cont=0;
bosque.Read(buffer,cont);

Figura 6-5. Propagacin de mensajes Write entre los objetos
6.4. SERIALIZACIN EN C++
La serializacin utilizando un lenguaje de mas alto nivel como es C++ es mas
sencilla, no solo por el lenguaje en si mismo, sino por las libreras de soporte del
mismo.
De especial importancia en este caso es la existencia de streams (flujos,
aunque los seguiremos llamando streams) en la librera estndar de C++ Standard
Template Library (STL). Recurdese la peculiaridad de que para incluir las cabeceras de
esta librera no se incluye el .h
#include <iostream> //Include de la STL, OK
#include <iostream.h> //Include de librera IO de C++, NO
Entre las clases pertenecientes a la IOStream Library, destacamos las
siguientes, que van a ser las utilizadas en nuestro cdigo:
istream Stream de entrada
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 108
Universidad Politcnica de Madrid -UPM
ostream Stream de salida
ifstream Stream de entrada de fichero. Derivada de istream.
ofstream Stream de salida a fichero. Derivada de ostream.
istringstream Stream de entrada de cadena (string). Derivada de
istream.
ostringstream Stream de salida a cadena (string). Derivada de
ostream.
Tambin hay que recordar que a esta librera pertenecen los objetos globales
cin, cout, cerr y clog (dentro del espacio de nombres std). La potencia
de C++ (como el polimorfismo), as como esta librera hacen que programar la
serializacin con y sin formato sea bastante ms sencillo.
6.4.1 Con formato (texto)
La serializacin con formato se realiza fcilmente con los operadores de
insercin (<<) y extraccin (>>) que ya se encuentra implementado para los tipos
bsicos (int, float, etc.), y que se puede sobrecargar fcilmente para tipos de
datos (clases) programadas por el usuario.
Como dichos operadores no son mtodos de la clase, se declaran como amigos
(friend), para que tengan acceso a los posibles atributos protegidos o privados.
Aunque en este caso no sea necesario ya que todos los atributos son pblicos,
mantenemos la amistad para conseguir una implementacin tpica.
Ntese que tanto la insercin como la extraccin admiten un primer parmetro
de las clases base istream y ostream, aunque luego se pueden utilizar las clases
derivadas segn se desee utilizar un fichero o una cadena.
#include "Arbol.h"
#define MAX_ARBOLES 100

#include <iostream>
using namespace std;

class Bosque
{
friend istream& operator>>(istream& s, Bosque& b);
friend ostream& operator<<(ostream& s, const Bosque& b);

Gracias al using namespace std se evita el tener que anteponer el
prefijo std a todas las clases: std::istream, std::ofstream, etc.
El segundo parmetro es una referencia en el caso de la extraccin, ya que el
operador deber modificar el objeto correspondiente. En el caso de la insercin, el
objeto no debe de ser modificada, y por tanto se utiliza una referencia constante.
#include "Cilindro.h"
#include "Esfera.h"
#include <iostream>
using namespace std;
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 109
Universidad Politcnica de Madrid -UPM

class Arbol
{
friend istream& operator>>(istream& s, Arbol& a);
friend ostream& operator<<(ostream& s, const Arbol& a);

Ambos operadores devuelven una referencia a istream u ostream, para
poder concatenar operaciones:
stream>>a>>b; //stream es un objeto de tipo istream (p. ej. ifstream)
stream<<a<<b; //stream es un objeto de tipo ostream (p. ej. ofstream)
La declaracin de los operadores para las clases Esfera y Cilindro es
totalmente anloga.
La implementacin de los operadores sigue la filosofa anteriormente expuesta,
manejando ahora el operador sobrecargado correspondiente:
istream& operator>>(istream& s, Bosque& b)
{
s>>b.numero;
int i;
for(i=0;i<b.numero;i++)
s>>b.arbol[i];
return s;
}
La lectura o extraccin no supone ningn problema, porque en la misma ya se
procesan los separadores (recurdese que es una serializacin con formato) como los
espacios o retornos de carro. Sin embargo, en la escritura o serializacin es el
programador el encargado de establecer dichos separadores. Con el objeto endl se
consigue un final de lnea.
ostream& operator<<(ostream& s, const Bosque& b)
{
s<<b.numero<< endl;
int i;
for(i=0;i<b.numero;i++)
s<<b.arbol[i]<< endl;
return s;
}
Si queremos escribir dos variables en la misma lnea, entonces tenemos que
separarlas por espacios o tabulaciones.
istream& operator>>(istream& s, Arbol& a)
{
s>>a.x>>a.y;
s>>a.tronco;
s>>a.copa;
return s;
}
ostream& operator<<(ostream& s, const Arbol& a)
{
s<<a.x<<" "<<a.y<<std::endl;
s<<a.tronco;
s<<a.copa;
return s;
}
La serializacin de la Esfera y el Cilindro quedaran como sigue:
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 110
Universidad Politcnica de Madrid -UPM
istream& operator>>(istream& s, Cilindro& c)
{
s>>c.radio>>c.altura;
return s;
}
ostream& operator<<(ostream& s, const Cilindro& c)
{
s<<c.radio<<" "<<c.altura<<std::endl;
return s;
}
istream& operator>>(istream& s, Esfera& e)
{
s>>e.radio>>e.rojo>>e.verde>>e.azul;
return s;
}
ostream& operator<<(ostream& s, const Esfera& e)
{
s<<e.radio<<" "<<e.rojo<<" "<<e.verde<<" "<<e.azul<<endl;
return s;
}
Una vez realizada esta implementacin podemos serializar los datos
cmodamente desde un fichero, sacarlos por la consola, a una cadena-stream, etc.:
cout<<bosque; //a consola
ofstream file("Bosque.txt");
file<<bosque; //a un fichero
ostringstream str; //a una cadena-stream
str<<bosque;
string cadena=str.str();//Como obtener la cadena (para enviar
//por un socket, por ejemplo)
El resultado de ejecutar la primera lnea de cdigo seria similar a lo siguiente,
que por otra parte debera coincidir con el contenido del fichero de texto Bosque.txt.
Se aprecian claramente los valores de los atributos respectivos, valores que se podran
modificar fcilmente.
50
-9.97497 1.27171
0.2 4.38661
1.80874 117 174 0

-2.99417 7.91925
0.2 5.64568
1.7466 34 233 0

4.21003 0.270699
0.2 4.60799
1.01498 18 156 0

La deserializacin seria igualmente sencilla, sin importar si los datos vienen de
un fichero o de una cadena-stream (recibida por un socket, por ejemplo).
ifstream file("Bosque.txt"); //desde un fichero
file>>bosque;
istringstream str;
//la cadena coge algun valor
str>>bosque; //Desde una stringstream
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 111
Universidad Politcnica de Madrid -UPM
6.4.2 Sin formato (binaria)
En el apartado anterior, si deseamos hacer una serializacin binaria por
eficiencia, o simplemente porque no queremos que los datos sean fcilmente visibles,
podramos intentar abrir el fichero en modo binario:
ofstream file("Bosque.txt",ios::binary);
Pero esto no es suficiente, ya que los operadores insercin y extraccin
trabajan sobre los tipos bsicos siempre con formato (en modo texto), y por tanto se
serializan de ese modo, aunque el fichero sea abierto en modo binario. Si se desea que
la serializacin sea completamente binaria, hay que recurrir a las funciones especificas
de la IOStream library que hacen estas tareas. Estas funciones se llaman tpicamente
read y write. Aadimos a todas nuestras clases unos mtodos que se llamen de
forma similar, y que admitan una referencia a stream. Gracias a esta referencia,
podremos utilizar el polimorfismo, y nuestros mtodos funcionarn igual para las
clases derivadas correspondientes (fstreams y stringstreams).
Los siguientes mtodos sern entonces aadidos a las clases Bosque,
Arbol, Esfera y Cilindro:
void Read(std::istream& str);
void Write(std::ostream& str);
La filosofa coincide completamente con la desarrollada anteriormente en
lenguaje C, a excepcin que ahora se utilizan las funciones de lectura y escritura sin
formato (read y write) en un stream:
void Bosque::Read(std::istream& str)
{
str.read((char*)&numero,sizeof(numero));
int i;
for(i=0;i<numero;i++)
arbol[i].Read(str);
}
void Bosque::Write(std::ostream& str)
{
str.write((char*)&numero,sizeof(numero));
int i;
for(i=0;i<numero;i++)
arbol[i].Write(str);
}
Como anteriormente, preservar el orden es totalmente necesario:
void Arbol::Read(std::istream& str)
{
str.read((char*)&x,sizeof(float));
str.read((char*)&y,sizeof(float));
tronco.Read(str);
copa.Read(str);
}
void Arbol::Write(std::ostream& str)
{
str.write((char*)&x,sizeof(float));
str.write((char*)&y,sizeof(float));
tronco.Write(str);
copa.Write(str);
}
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 112
Universidad Politcnica de Madrid -UPM
El resto del cdigo se puede encontrar en las carpetas adjuntas.
Si ahora se desea guardar los datos del bosque en un archivo binario, bastara
con realizar:
ofstream file("Bosque.txt",ios::binary);
bosque.Write(file);
Abriendo el archivo con un editor de textos se puede apreciar que el contenido
es totalmente ininteligible. Si posteriormente se desea recuperar los datos del bosque
desde dicho archivo se podra hacer:
ifstream file("Bosque.txt",ios::binary);
bosque.Read(file);

6.5. CONCLUSIONES
Se ha presentado en este captulo la problemtica de la serializacion de datos y
sus aplicaciones en persistencia (ficheros de datos) o comunicaciones. Asimismo se
han introducido algunos ejemplos de tcnicas y estrategias que permiten realizar esta
tarea de forma ordenada, con el correspondiente cdigo en los lenguajes C y C++.
El ejemplo explicado es una aplicacin grafica, pero el uso de la serializacion es
mucho mas extenso, tanto que los diseadores de sistemas de desarrollo, libreras y
lenguajes ya la tienen en cuenta desde el comienzo, proporcionando dichos servicios
de una u otra forma. Aunque en este tema se han explicado tcnicas que permiten al
usuario realizar la tarea, se aconseja estudiar en detalle el sistema de desarrollo
utilizado y libreras de terceros en el caso de proyectos software reales.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 113
Universidad Politcnica de Madrid -UPM


7
7
.
.

BSQUEDAS EN UN ESPACIO DE
ESTADOS MEDIANTE RECURSIVIDAD
7.1. INTRODUCCIN
La bsqueda y la representacin del conocimiento son dos de los problemas
fundamentales de la Inteligencia Artificial (IA). La bsqueda puede formalizarse
mediante un espacio de estados que, a su vez, puede verse como un grafo donde los
nodos representan estados de dicho espacio y los arcos dirigidos las reglas
(operadores, transiciones etc.) que permiten el paso entre estados. La formalizacin de
un problema de modo que se pueda resolver mediante algn tipo de bsqueda se
denomina representacin del conocimiento.
Un espacio de estados para un problema de bsqueda puede formalizarse
como una cuadrupla <S, A, I, O> donde S representa el conjunto de estados (o
configuraciones) posibles que pueden darse, A las acciones (reglas, operadores etc.)
que permiten el paso entre estados, I la configuracin (o estado) inicial y O la
configuracin (estado) objetivo a alcanzar. En el caso general, los conjuntos I y O
pueden contener ms de un estado.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 114
Universidad Politcnica de Madrid -UPM
3 6
4 5 8
7 2 1
3 6
4 5 8
7 2 1
4 3 6
5 8
7 2 1
3 5 6
4 8
7 2 1
3 6
4 5 8
7 2 1
4 3 6
5 8
7 2 1
4 3 6
7 5 8
2 1
3 6
4 5 8
7 2 1
4 3 6
7 5 8
2 1
Inicio Objetivo a
b c
d e f g
3 6
4 5 8
7 2 1
3 6
4 5 8
7 2 1
4 3 6
5 8
7 2 1
3 5 6
4 8
7 2 1
3 6
4 5 8
7 2 1
4 3 6
5 8
7 2 1
4 3 6
5 8
7 2 1
4 3 6
7 5 8
2 1
3 6
4 5 8
7 2 1
4 3 6
7 5 8
2 1
Inicio Objetivo
3 6
4 5 8
7 2 1
4 3 6
7 5 8
2 1
Inicio Objetivo a
b c
d e f g

Figura 1. Ejemplo de un problema de bsqueda: el puzzle-8. En trazo grueso se ha
representado el camino solucin.
En la Figura 1 se muestra un ejemplo de problema de bsqueda extrado del
mundo de los juegos. En el puzzle-8, 8 piezas numeradas del 1 al 8 y un hueco
comparten una cuadrcula 3x3. El objetivo del juego es obtener una configuracin
objetivo a partir de una configuracin de piezas y hueco dada. Las piezas solo pueden
moverse en horizontal y vertical ocupando el hueco (casilla sombreada). La figura
muestra un posible rbol de bsqueda generado para encontrar la solucin. Los nodos
de dicho rbol son las configuraciones intermedias que se atraviesan durante la
bsqueda y los arcos (o ramas) los posibles movimientos legales (en el ejemplo dos por
cada estado, por lo que el rbol se denomina binario). En la figura se ha regruesado el
camino solucin.
Un aspecto fundamental de cualquier procedimiento de bsqueda es cmo
evadir la explosin combinatoria de estados que pueden aparecer. Por ejemplo, en el
puzzle-8 una solucin tiene de promedio unos 20 pasos. El factor de ramificacin (el
nmero de estados descendientes posibles para un nodo cualquiera del rbol de
bsqueda) tiene una media ligeramente menor que 3, con lo que el tamao del
espacio de bsqueda est en torno a
20 9
3 10 , un nmero muy considerable teniendo
en cuenta la aparente simplicidad del problema.
En torno a 10
9
estados seran, pues, los recorridos por un procedimiento de
bsqueda sistemtica exhaustivo que, mediante ensayo y error, generara todos los
posibles estados intermedios entre el nodo raz y el objetivo. Este es el procedimiento
de control de la bsqueda ms sencillo conocido como fuerza bruta. Existen una
cantidad importante de algoritmos de control de propsito general para realizar
bsquedas exhaustivas, conocidos como tcnicas de bsquedas desinformada (o
tambin, bsqueda a ciegas) que conforman un marco genrico para cualquier
problema planteado como una bsqueda en un grafo. Esta seccin se centra en la
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 115
Universidad Politcnica de Madrid -UPM
implementacin de uno de ellos: la bsqueda primero en profundidad (Depth-First-
Search o simplemente DFS)
De manera informal, la bsqueda DFS consiste en elegir en todo momento para
continuar la bsqueda al candidato que se encuentre a mayor profundidad. En el otro
extremo se encuentra la bsqueda primero-en-anchura que se decanta por el
candidato situado a menor profundidad de entre los posibles. Tomando como ejemplo
nuevamente el problema planteado en la Figura 1, y suponiendo que en caso de
empates se elije siempre el candidato ms a la izquierda, la seleccin de nodos sera {a,
b, d, e, c, f , g} para la bsqueda DFS y {a, b, c, d, e, f, g} para la bsqueda primero en
anchura.
7.2. BSQUEDA PRIMERO EN PROFUNDIDAD
Muchos de los algoritmos que recorren grafos se describen con facilidad pero
rara es la vez que no presentan dificultades a nivel de detalle. En el caso de bsquedas
en grafos los algoritmos de control deben tener especial cuidado con la aparicin de
estados repetidos y ciclos. Sin deteccin de ciclos es posible que la bsqueda quede
atrapada en un bucle infinito (ver figura 2).

a
b
c
e
d
a
b
c
e
d

Figura 2: Un grafo de bsqueda que presenta un ciclo. Sin un control de repeticin de
estados la bsqueda podra quedar atrapada indefinidamente en {a, d, e, c}.
Existen problemas donde la aparicin de los temidos ciclos simplemente no es
posible. En estos casos el algoritmo de control se simplifica considerablemente y es
ms rpido. A continuacin se describe el algoritmo primero-en-profundidad escrito en
pseudocdigo:
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 116
Universidad Politcnica de Madrid -UPM
Procedimiento PRIMERO EN PROFUNDIDAD (INICIO, OBJETIVO)
Inicializacin: ABIERTOS:={INICIO}, CERRADOS :={}
REPETIR hasta alcanzar OBJETIVO o ABIERTOS est vaco
1. Quitar de ABIERTOS el elemento ms a la izquierda y llamarlo X
2. Generar los hijos de X
3. Aadir X a CERRADOS
4. Eliminar aquellos hijos de X que estn en ABIERTOS o en CERRADOS
5. Aadir los hijos de X a ABIERTOS por la izquierda

Como puede apreciarse, el algoritmo es completamente independiente del
dominio. La bsqueda parte de un estado inicial INICIO y termina cuando se genera un
sucesor que resulta ser el estado OBJETIVO. El bucle de control general lleva implcita
dicha comprobacin, que se entiende puede realizarse en tiempo polinomial.
De forma intuitiva, el procedimiento elige un candidato de los posibles, genera
los sucesores y los guarda como nuevos candidatos a expandir.
7.2.1 Terminologa
El trmino hijo en el pseudocdigo hace referencia a un sucesor directo,
empleando la analoga entre un rbol de bsqueda y un rbol genealgico. As, es
frecuente utilizar relaciones de parentesco para indicar la profundidad de la relacin
(abuelo, bisabuelo, nieto etc.) Un nodo raz del que cuelga un subgrafo ser antecesor
de todos los nodos de dicho subgrafo. Anlogamente, dichos nodos sern
descendientes de aqul. La relacin de parentesco resulta inadecuada cuando existen
ciclos en el grafo (como en el caso de la figura 2). Se denominan hojas a aquellos nodos
del rbol de bsqueda que no tienen sucesores. La bsqueda no puede continuar por
un nodo hoja teniendo que retroceder en el rbol a algn nodo antecesor, lo que se
conoce como vuelta-atrs.
7.2.2 Estructuras de datos
A pesar de la aparente sencillez del pseudocdigo se requieren, en el caso
general, las siguientes estructuras de datos:
Una lista de nodos ABIERTOS: Esta lista puede verse como una cola LIFO
(Last In First Out) donde el ltimo elemento que entra es el primer
elemento ledo. Si se visualiza la cola como una estructura horizontal
donde los datos pueden entrar y salir por ambos extremos izquierda y
derecha, una cola LIFO se consigue introduciendo y leyendo datos por el
mismo lugar.
Unas lista de nodos CERRADOS
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 117
Universidad Politcnica de Madrid -UPM
Para cada nodo del rbol hay que almacenar la informacin del camino
recorrido. Esto permite recuperar la trayectoria desde el nodo raz una
vez alcanzado un estado OBJETIVO. En el algoritmo propuesto basta con
almacenar para cada nodo examinado quin es su padre.
La lista ABIERTOS almacena el conjunto de estados generados en cualquier
momento de la bsqueda, pero todava no analizados (es decir, se desconocen sus
posibles sucesores). Puede perfectamente producirse la paradoja de que un estado
OBJETIVO se encuentre en ABIERTOS pero no sea seleccionado para continuar la
bsqueda con lo que el procedimiento todava podra tardar un tiempo exponencial en
darse cuenta que ya ha encontrado lo que buscaba. El conjunto de nodos por los que la
bsqueda puede continuar en cualquier momento se denomina frontera y coincide
con la lista de nodos en ABIERTOS para el algoritmo primero en profundidad.
La lista CERRADOS corresponde con el conjunto de estados ya examinados (es
decir, cuyos sucesores ya han sido generados y se encuentran en ABIERTOS). Esta lista
es necesaria para controlar la aparicin de estados repetidos y ciclos durante la
bsqueda).
Dependiendo del problema particular, es posible que algunas de las estructuras
y operaciones indicadas para el algoritmo no sean necesarias. Para ello es necesario
realizar un anlisis previo del tipo de rbol de bsqueda que puede generarse. Como
ejemplo, en problema de las 3 en raya no pueden producirse estados repetidos (en
cada turno aparece una nueva pieza en el tablero). Un algoritmo primero-en-
profundidad para decidir la mejor jugada en este caso no necesita comprobar si cada
estado nuevo ya ha sido generado con anterioridad con lo que la lista CERRADOS es
innecesaria
El caso del puzzle-8 (figura 1) es el caso opuesto. En cada turno es posible
realizar un movimiento que genera el nodo padre (en el ejemplo de la figura, el
movimiento de la pieza 3 a la izquierda en el estado b, genera el nodo inmediatamente
antecesor a. Ntese que en la figura se ha dibujado el grafo de bsqueda sin estados
repetidos (es decir, un verdadero rbol). Operadores de transicin entre estados
bidireccionales (muy frecuentes en problemas de enrutamiento) generan tambin
ciclos durante la bsqueda. Una vez que se detecte esta posibilidad es necesario
almacenar todos los estados de la bsqueda recorridos de manera dinmica, si se
quiere garantizar que el procedimiento sea completo (es decir, que encuentre una
solucin si la hubiere).
7.2.3 Anlisis
El pseudocdigo no presenta grandes dificultades. En cada iteracin de elije un
nodo frontera en ABIERTOS (lnea 1), se calculan sus sucesores directos (lnea 2) y se
aade dicho nodo a CERRADOS (lnea 3), puesto que ya ha sido analizado. La lnea 4 es
necesaria para gestionar sucesores repetidos y ciclos. Un sucesor nuevo repetido
puede estar en ABIERTOS (en cuyo caso se ha generado con anterioridad pero an no
se han examinado) o en CERRADOS, en cuyo caso se expandi con anterioridad en el
grafo. Todos los sucesores recin generados se eliminan si estn bien en ABIERTOS,
bien en CERRADOS y solo los que quedan se aaden a la cola (lnea 5). Para conseguir
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 118
Universidad Politcnica de Madrid -UPM
que la cola sea LIFO, condicin imprescindible para que la bsqueda sea primero en
profundidad, se incluye la direccin de carga y descarga, en este caso por el mismo
lado. La figura 3 ejemplifica la generacin de un rbol de bsqueda primero en
profundidad para un espacio de estados dado.
a
b c
f
a
b
c
e d
a
b
c
d e
a
b
e d
f
a
b
e d
f
a
b
e d
f
a
c
c
c
a
b c
f
a
b
c
e d
a
b
c
d e
a
b
e d
f
a
b
e d
f
a
b
e d
f
a
b
e d
f
a
b
e d
f
a
c
c
c

Figura 3. Ejemplo de bsqueda primero-en-profundidad para el espacio de estados
recuadrado en la figura. A medida que la bsqueda avanza el rbol evoluciona de
izquierda a derecha y de arriba abajo.
En computacin se dice que un algoritmo es correcto si para cualquier solucin
candidata que genera, dicha solucin satisface las especificaciones del problema. Ms
fuerte es el requisito de completitud. Un algoritmo se dice que es completo cuando si
existe una solucin la encuentra. Es interesante destacar que, de forma un tanto
sorprendente, la bsqueda primero en profundidad (DFS) no garantiza la completitud
en el caso general. Esto es as porque cabe la posibilidad de que el algoritmo se pierda
en ramales de profundidad infinita y nunca llegue a examinar el camino o caminos que
llevan al estado OBJETIVO. Imagine el lector que quiere saber si es un descendiente
directo de Abraham Lincoln y dispone del conocimiento necesario para ello. Si decide
emplear una bsqueda DFS en sentido inverso (es decir, analizando padres, abuelos,
bisabuelos etc. con la esperanza de encontrar a Lincoln) una bsqueda DFS podra
retrotraerse hasta la prehistoria an en el caso altamente improbable de que s fuera
descendiente directo. Dicho en otros trminos, si el algoritmo DFS se ejecuta
indefinidamente, no es posible concluir ni a favor ni en contra de la premisa de partida.
Por el contrario, el requerimiento en memoria es muy modesto. Como puede
verse en la figura, DFS solo necesita almacenar un nico camino desde el nodo raz al
nodo actual junto con todos los nodos sucesores generados por ese camino. As pues,
el problema de DFS reside en el tiempo de cmputo pero no en la cantidad de
memoria que necesita para su ejecucin.
La figura 4 muestra una traza completa del algoritmo DFS para un problema de
enrutamiento. Como puede apreciarse, la lista ABIERTOS coincide en todo momento
con la frontera de la bsqueda.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 119
Universidad Politcnica de Madrid -UPM
a
b c
d e
{e, d, b, a} {c} 4
{c ,e, d, b, a} {} 5
{d, b, a} {e, c} 3
{b, a} {d, e, c} 2
{a} {b, c} 1
{} {a} 0
CERRADOS ABIERTOS N it.
a
b c
d e
a
b c
d e
{e, d, b, a} {c} 4
{c ,e, d, b, a} {} 5
{d, b, a} {e, c} 3
{b, a} {d, e, c} 2
{a} {b, c} 1
{} {a} 0
CERRADOS ABIERTOS N it.
{e, d, b, a} {c} 4
{c ,e, d, b, a} {} 5
{d, b, a} {e, c} 3
{b, a} {d, e, c} 2
{a} {b, c} 1
{} {a} 0
CERRADOS ABIERTOS N it.

Figura 4. Traza de una bsqueda primero en profundidad sobre el espacio de estados
que aparece a la izquierda.
7.3. BSQUEDA PRIMERO EN ANCHURA
Aunque esta seccin est dedicada a una implementacin prctica de la
bsqueda DFS, resulta interesante compararla con otra tcnica de bsqueda
sistemtica desinformada denominada primero-en-anchura. En este caso, la estrategia
de seleccin de nodos consiste en elegir aquel candidato en ABIERTOS que se
encuentre a menor profundidad. Intuitivamente, el rbol se genera horizontalmente o
en anchura lo que da el nombre a esta tcnica.
La implementacin primero-en-anchura (BFS, del ingls Breadth-First-Search) es
esencialmente idntica a la bsqueda DFS solo que, en este caso, los nodos que entran
deben extraerse de la lista en primer lugar, es decir, la lista ABIERTOS es, en este caso,
un cola FIFO. Si se modifica la lnea 5 del pseudocdigo DFS para que los sucesores de
X se almacenen en ABIERTOS por la derecha entonces se transforma en una
bsqueda primero-en-anchura. La figura 5 muestra la nueva traza para el mismo
espacio de estados empleado en la figura 4.
a
b c
d e
{d, c, b, a} {e} 4
{e, d, c, b, a} {} 5
{c, b, a} {d, e} 3
{b, a} {c, d, e} 2
{a} {b, c} 1
{} {a} 0
CERRADOS ABIERTOS N it.
a
b c
d e
a
b c
d e
{d, c, b, a} {e} 4
{e, d, c, b, a} {} 5
{c, b, a} {d, e} 3
{b, a} {c, d, e} 2
{a} {b, c} 1
{} {a} 0
CERRADOS ABIERTOS N it.
{d, c, b, a} {e} 4
{e, d, c, b, a} {} 5
{c, b, a} {d, e} 3
{b, a} {c, d, e} 2
{a} {b, c} 1
{} {a} 0
CERRADOS ABIERTOS N it.

Figura 5. Traza de una bsqueda primero en anchura sobre el espacio de estados
que aparece a la izquierda. Los nodos recin generados se incorporan a ABIERTOS
por la derecha.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 120
Universidad Politcnica de Madrid -UPM
Al recorrer un espacio de estados primero-en-profundidad, la frontera de la
bsqueda pasa a ser el conjunto de nodos en un mismo nivel del rbol y sus posibles
sucesores (a diferencia de la bsqueda DFS que almacenada solamente el camino
desde la raz). Este hecho hace que la memoria requerida por BFS sea
exponencialmente mayor que la bsqueda DFS. Un ejemplo: el nmero de nodos hoja
de un rbol uniforme con factor de ramificacin 5 y profundidad 6 es 5
6
=15625, el
orden de magnitud de los nodos que el procedimiento BFS debera mantener en
memoria. Un algoritmo DFS equivalente necesitara aproximadamente 5x6=30, es
decir 5 nodos por cada nivel.
Sin embargo BFS, a diferencia de DFS, es completo. Esto quiere decir que
garantiza encontrar una solucin al problema si sta existe. La demostracin es trivial.
Si existe una solucin al problema, sta debe encontrarse en una profundidad finita del
rbol d. Como BFS expande primero en anchura, completar el nivel d del rbol antes
de pasar a niveles superiores, con lo que encontrar la solucin en un tiempo finito.
Como se explic en la seccin 7.2, el algoritmo primero en profundidad puede
perderse en una rama de profundidad infinita y nunca llegar a encontrar la solucin
(en una profundidad d pero en un camino distinto). En teora de computacin es muy
frecuente la dicotoma espacio-tiempo. Un procedimiento que consume mucha
memoria es, en la mayora de los casos, ms eficiente que un procedimiento
equivalente que consume menos. La dicotoma es perfectamente aplicable a las
tcnicas BFS y DFS.
7.4. METODOLOGA GENERAL DE RESOLUCIN DE UN PROBLEMA DE
BSQUEDA MEDIANTE COMPUTACIN
Antes de abordar el problema de la implementacin es importante destacar
que existen un conjunto de consideraciones previas y tareas a realizar para la
implementacin de un procedimiento eficiente que resuelva un problema de
bsqueda genrico. Dicho problema se presupone bien formado. Entre las tareas a
realizar destacan:
Definicin del problema de una manera formal: Por ejemplo, para un
problema de enrutamiento definir con precisin las reglas de
movimiento entre ciudades, para el puzzle-8 el movimiento de las piezas
en horizontal y vertical etc.
Anlisis: En esta fase se estudia minuciosamente el problema ya
formalizado para determinar aquellas caractersticas que puedan tener
influencia en las tcnicas de bsqueda que se van a emplear en su
resolucin. Por ejemplo, si existe una explosin combinatoria en el
nmero de estados, y no se tiene conocimiento especfico del dominio
para guiar la bsqueda, entonces BFS no es recomendable por que
consume excesiva memoria.
Aislamiento y representacin adecuada del conocimiento necesario:
Entre otras tareas, resulta extremadamente relevante para la eficiencia
global del procedimiento de bsqueda una representacin adecuada de
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 121
Universidad Politcnica de Madrid -UPM
la nocin de estado y de los operadores que permiten la transicin
entre estados. En la figura 6 se muestran dos representaciones
alternativas de los operadores de movimiento para el puzzle-8. Se deja
al criterio del lector el decidir cul de las dos sera ms adecuada con
vistas a la implementacin de un algoritmo de bsqueda para ese
problema.
Eleccin adecuada de la mejor tcnica de bsqueda: A partir de la
informacin adquirida en las etapas anteriores se decide cul de las
numerosas tcnicas de bsqueda de propsito general es ms adecuada
para el problema particular. Si la eleccin se restringe a una bsqueda
DFS o BFS, un tamao de espacio de estados grande apunta hacia la
tcnica DFS, a expensas de perder completitud en el caso peor. Para
tamaos razonables de espacio de estados se aconseja la bsqueda
primero en anchura que es completa.
5 3 6
4 8
7 2 1
5 3 6
4 8
7 2 1
5 3 6
4 8
7 2 1
5 3 6
4 8
7 2 1
5 3 6
4 8
7 2 1

Figura 6. Ejemplo de dos representaciones de los operadores de movimiento en el
puzzle-8: las piezas hacia el cuadro vaco (derecha) o el cuadro vaco hacia las piezas
(izquierda).
7.5. IMPLEMENTACIN DE UNA BSQUEDA DFS MEDIANTE
RECURRENCIA
La recurrencia es una tcnica de programacin que consiste en especificar la
ejecucin de un proceso mediante su propia definicin. Un algoritmo recursivo es, por
tanto, aqul que plantea la solucin a un problema en trminos de una llamada a s
mismo, lo que se conoce como llamada recurrente (o recursiva). Existen ejemplos de
recurrencia en todas las reas de las ciencias. En matemticas una funcin recursiva
f(x) es ( ) 3 ( 3) f x f x . En informtica el ejemplo tpico para ilustrar recurrencia es un
algoritmo para computar el factorial de un nmero como el que se muestra a
continuacin:
//Procedimiento Factorial(n)
int factorial(int n)
{
if(n<2)
return 1;

return n*factorial(n-1);
}
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 122
Universidad Politcnica de Madrid -UPM
Como puede apreciarse en el ejemplo, lo que realmente est ocurriendo es que
en cada nivel de recursin el problema se va descomponiendo en problemas iguales
pero de menor tamao (el problema original tiene tamao n, tras la primera llamada
recursiva pasa a ser de tamao n-1 etc.). El problema ms pequeo es el factorial de 1
que se resuelve directamente en la segunda lnea de la funcin.
Es importante destacar que la lnea de cdigo
if(n<2) return 1;
no solamente resuelve el factorial de 1 sino que permite salir de la recursin.
Sin una sentencia de control de este tipo, el procedimiento quedara indefinidamente
atrapado en un bucle infinito, debido a la circularidad inherente a la tcnica. Es
fundamental garantizar que la condicin de salida (en este caso n<2) se cumple en un
nivel de recurrencia finito. En el ejemplo y puesto que la llamada a Factorial se realiza
con un valor una unidad menos que en el nivel anterior, resulta evidente que la
condicin de salida se va a cumplir siempre en el nivel de recursin n-2 y, por tanto, el
procedimiento tiene que terminar.
La sencillez del procedimiento Factorial permite analizar fcilmente el flujo
de ejecucin. En cada nueva llamada a Factorial el flujo entra por la primera lnea
de la funcin (justo despus de {) y puede salir debido a la instruccin
return 1;
o bien por la instruccin
return n*factorial(n-1);
En ambos casos, el flujo contina en la funcin del nivel de recurrencia anterior
justo donde se realiz la llamada; es decir, se devuelve el resultado de la operacin y el
flujo completa le ejecucin de la ltima lnea. Esto se puede ver con ms claridad
aadiendo una variable intermedia al cdigo de la siguiente manera:
//Procedimiento Factorial(n)
int factorial(int n)
{
int resultado;
if(n<2)
return 1;
resultado = factorial(n-1);
return n*resultado;
}
Como resumen, al emplear recurrencia hay que tener en cuenta siempre que el
flujo de ejecucin cumple con las especificaciones del problema, prestando
especialmente atencin a la condicin de salida.
7.5.1 La pila de llamadas
Al ejecutar cualquier proceso, los sistemas operativos le asignan un espacio en
memoria para cubrir sus necesidades, espacio que no puede ser utilizado por el resto
de procesos en ejecucin. Este espacio reservado se conoce como rea de memoria
del proceso.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 123
Universidad Politcnica de Madrid -UPM
La pila de llamadas es una pila de datos LIFO en el rea de memoria de un
proceso. La funcin principal es almacenar el punto donde devolver el control del flujo
de ejecucin una vez terminada la funcin (o subrutina) activa en ese momento. De
esta manera se pueden invocar funciones dentro de otras funciones sin perder el hilo
de ejecucin. Cada nueva llamada introduce la direccin de retorno a la funcin
invocante en la pila y empuja al resto de direcciones. Al terminar dicha funcin se lee
la primera direccin de la pila como punto de retorno.
Una de las ventajas adicionales de la pila de llamadas es que soporta
recurrencia. Para la pila, el hecho de que una funcin A llame a una funcin B o se
llame a s misma es irrelevante; basta almacenar en la pila la direccin de la instruccin
siguiente a ejecutar una vez termine. En el ejemplo del Factorial, la direccin
correspondera a la lnea
resultado = factorial(n-1);
Adicionalmente la pila de llamadas puede emplearse, entre otras cosas, para
almacenar de forma eficiente las variables locales pertenecientes a la funcin activa.
Estas variables pierden su valor una vez que termina la funcin. La pila puede realizar
esta reserva de forma muy eficiente, reubicando el puntero de pila. Como desventaja,
hay que decir que el rea de memoria reservada para la pila es bastante limitada.
Cuando se sobrepasa aparece el tpico error en tiempo de ejecucin de
desbordamiento de pila (o stack overflow), bien conocido por los programadores.
Como ejemplo compile y ejecute este cdigo escrito en C++:
#include <iostream.h>
#define MAX_SIZE 100
#define MAX_DEPTH 100

void ProcRecursivo(int k)
{
int vector[MAX_SIZE][MAX_SIZE];

if(k>=MAX_DEPTH) return; //Salida
for(int i=0; i<MAX_SIZE; i++)
for(int j=0; j<MAX_SIZE; j++)
vector[i][j]=0;
cout<<"Nivel: "<<k<<endl;
ProcRecursivo(k+1);
}

void main()
{
cout<<"Comienzo de recursion"<<endl;
try{
ProcRecursivo(0);
}
catch(...){
cout<<"Stack Overflow"<<endl;
}
cout<<"Fin de recursion"<<endl;
}
Este cdigo dispone de una funcin ProcRecursivo que se llama de forma
recursiva y que tiene como nica misin inicializar una matriz de enteros a cero en
cada nivel de recurrencia. Obsrvese que, al estar cada matriz declarada localmente, el
compilador, por defecto, reservar espacio en memoria en la pila de llamadas.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 124
Universidad Politcnica de Madrid -UPM
ProcRecursivo se llama a s mismo incrementando previamente en una unidad el
valor que controla la salida de la recursin. Cuando la recurrencia alcanza el nivel
MAX_DEPTH se produce la primera vuelta atrs. A partir de este momento se va
liberando de forma secuencial la pila de llamadas hasta retornar al nivel de recurrencia
0 de partida.
La funcin main llama a ProcRecursivo. Las instrucciones try y catch
en C++ (y en muchos otros lenguajes de alto nivel) sirven para gestionar la aparicin de
excepciones en tiempo de ejecucin. try encapsula entre llaves aquellas instrucciones
susceptibles de producir algn tipo de excepcin y catch encapsula entre llaves las
tareas a realizar si se producen (los manipuladores de las excepciones). Finalizado el
bloque catch, el flujo de ejecucin contina con la siguiente instruccin despus del
bloque. La sintaxis
catch(...)
indica que el bloque contiene los manipuladores para cualquier tipo de
excepcin (incluyendo excepciones especficas de C). En el ejemplo, la excepcin slo
puede producirse por desbordamiento de pila. En tal caso aparecera el mensaje Stack
Overflow en pantalla para despus continuar con la ejecucin de la instruccin que
muestra la cadena Fin de recursin en pantalla.
La ejecucin del cdigo anterior para valores de MAX_DEPTH = 100 y
MAX_SIZE=100 (Pentium D@3GHz, 1GB RAM sobre Windows XP) ya produce
desbordamiento de pila utilizando el compilador Visual Studio 6.0. Por defecto el
compilador otorga 1 MB de memoria a la pila de llamadas, y en este caso, la memoria
ocupada es 100 100( ) 100( ) 2(int) 2 x matriz x nivel x MB producindose el
desbordamiento. Si se modifican las opciones del compilador y se reserva 10MB de
memoria virtual para la pila (opcin /stack:0x10000000) ya no se produce la excepcin.
Es interesante comentar que el desbordamiento de pila tampoco se va a producir si se
compila con la opcin de mxima velocidad (/O2) debido a que una de las
optimizaciones que realiza el compilador es reservar memoria para la matriz
bidimensional fuera de la pila. Con esta opcin de compilacin activada, el tamao de
la matriz vector deja de constituir un problema para valores de MAX_SIZE muy
superiores a 100.
7.5.2 Bsqueda DFS como recursin
El algoritmo para un procedimiento genrico de bsqueda primero en
profundidad descrito en la seccin 7.2 estaba formulado de manera iterativa sobre una
estructura de datos LIFO. En cada iteracin se va modificando la lista ABIERTOS hasta
que, o bien se elige un estado OBJETIVO de dicha lista o bien la lista queda vaca, en
cuyo caso el procedimiento termina sin encontrar una solucin. Para simplificar, se
asume en este apartado que el grafo de bsqueda no tiene ciclos ni estados repetidos
con lo que se puede prescindir de la estructura CERRADOS.
Una manera alternativa de entender el procedimiento DFS es la de una
recursin donde la tarea que se repite en diferentes niveles est formada por las
subtareas siguientes:
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 125
Universidad Politcnica de Madrid -UPM
Extraccin de un nodo de ABIERTOS (y eliminar)
Generacin de sus sucesores
Aadir dichos sucesores a la lista de ABIERTOS
A su vez, la salida de la recursin (vuelta atrs de la funcin activa en ese
momento) se produce cuando:
Se ha encontrado un estado OBJETIVO
No se han encontrado sucesores para el nodo actual: lo que implica que
es un nodo hoja y hay que volver atrs para continuar por otro camino
Si se estima que la dimensin del espacio de estados es relativamente pequea,
se puede emplear la propia pila para almacenar los estados de ABIERTOS en el nivel del
rbol que se corresponde con el nivel de recursin. Intuitivamente la lista ABIERTOS se
divide por niveles y los nodos de cada nivel se declaran como variables locales en la
pila de llamadas. El algoritmo recursivo DFS modificado para permitir que la pila de
llamadas gestione la frontera de la bsqueda se muestra en la figura 7:

Procedimiento DFS_RECURSIVO (ACTUAL, OBJETIVO)
Valor inicial: ACTUAL = Estado inicial
1. Si ACTUAL = OBJETIVO finalizar
2. Generar los hijos de ACTUAL y almacenar en L (variable local)
3. REPETIR hasta que L est vaco
a. Seleccionar un nodo de L y llamarlo X
b. DFS_RECURSIVO (X, OBJETIVO)
c. Borrar X de L

Figura 7. Procedimiento recursivo primero en profundidad que permite emplear la
pila de llamadas para almacenar la frontera de la bsqueda.
Tomando como ejemplo el espacio de estados de la figura 4, la primera
llamada a la funcin recursiva de bsqueda almacenara localmente los nodos {b,c},
hijos del nodo raz. Una nueva llamada pasando como parmetro el nodo b
almacenara en la nueva lista local los nodos {d, e} descendientes directos el nodo
actual. Al ser d un nodo hoja, la expansin de dicho nodo provoca que la funcin
termine tras detectarse en la lnea 3 que no hay descendencia. Tras la vuelta-atrs, la
ejecucin continua por la lnea 3.c y se selecciona e el ltimo nodo abierto en este
nivel de recurrencia (nivel 3 del rbol de bsqueda). Tras sucesivas vueltas atrs se
expande el nodo c y finaliza la bsqueda.
La ventaja del algoritmo de la figura 7 es que aprovecha la forma en que el
Sistema Operativo gestiona la ejecucin de procesos en memoria para implementar la
lista ABIERTOS en una bsqueda DFS. La frontera de la bsqueda se divide por niveles
en el rbol y los nodos en cada nivel se almacenan por separado y de manera local a la
correspondiente funcin. La pila de llamadas se encarga de borrar la estructura de
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 126
Universidad Politcnica de Madrid -UPM
datos cuando se produce la vuelta atrs, una vez que se han analizado todos los nodos
en el nivel de recurrencia actual.
El procedimiento recursivo descrito para computar el factorial de un nmero
puede verse tambin como una bsqueda en un grafo. Desde esta perspectiva, el
espacio de estados tiene forma de rbol con una nica rama donde el estado inicial es
el factorial del nmero buscado y el estado objetivo tiene el valor unidad (factorial de
1). El procedimiento recorre el rbol hacia delante hasta alcanzar dicho estado (nivel
de recurrencia mxima). La solucin al problema se encuentra en el propio camino, y
se genera durante las sucesivas vueltas atrs.
7.5.2.1. Generacin de una clave
Como ejemplo sencillo de todo lo expuesto se propone como problema a
resolver el encontrar la clave de un nmero de 6 dgitos entre 0 y 9 que controla el
acceso a una cuenta de usuario en un servidor remoto. El procedimiento a realizar
tiene que generar todas las combinaciones posibles de la clave (10
6
) y bombardear al
servidor. Se considera aqu solamente la rutina generadora de claves posibles.
Este problema puede abordarse de forma trivial mediante un procedimiento
iterativo empleando bucles anidados.; cada bucle genera un nmero de la clave y el
bucle ms interior (en este caso el sexto) es el que genera la clave completa. La
solucin en C sera la siguiente:
#include <iostream.h>
#define TAM_NUMEROS 10
#define TAM_CLAVE 6

void main()
{
int clave[TAM_CLAVE];

for(int i=0; i< TAM_NUMEROS; i++)
for(int j=0; j< TAM_NUMEROS; j++)
for(int k=0; k< TAM_NUMEROS; k++)
for(int l=0; l< TAM_NUMEROS; l++)
for(int m=0; m< TAM_NUMEROS; m++)
for(int n=0; n< TAM_NUMEROS; n++){
//Generando clave
clave[0]=i;
clave[1]=j;
clave[2]=k;
clave[3]=l;
clave[4]=m;
clave[5]=n;
cout<<i<<j<<k<<l<<m<<n<<endl;
}
}
Este problema puede tambin enfocarse como un problema de bsqueda y
resolverse mediante la exploracin de un espacio de estados mediante la tcnica de
primero en profundidad. Un posible cdigo para la implementacin recursiva que
genera todas las posibles claves es:
#include <iostream.h>
#define TAM_CLAVE 6
#define TAM_NUMEROS 10
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 127
Universidad Politcnica de Madrid -UPM

int clave[TAM_CLAVE];

void FuncRec(int depth)
{
if(depth == TAM_CLAVE){ //Salida de recursin
for(int i=0; i<TAM_CLAVE; i++)
cout<<clave[i];
cout<<endl;
return; //Vuelta atrs
}

//Generacin de sucesores y llamada recursiva
clave[depth]=-1;
for(int j=0; j<TAM_NUMEROS; j++){
clave[depth]+=1;
FuncRec(depth+1);
}
}

void main()
{
FuncRec(0);
}
Como en ejemplos anteriores existe una funcin (FuncRec) que de forma
recursiva atraviesa el espacio de bsqueda de claves generando las 10
6
combinaciones.
La configuracin del estado se almacena, en este ejemplo, en un vector global clave
que es el que se va modificando en cada transicin. La verdadera clave es el valor de
esta estructura de datos en un nodo hoja del rbol de bsqueda.
La semntica detrs de cada nodo del rbol para una profundidad k es el
conjunto de claves que tienen como valores en ndices 0, 1, 2,, k-1 predeterminados
por el camino desde el nodo raz hasta el nodo actual. El subgrafo que cuelga de dicho
nodo conforma el espacio del resto de posibles claves con valores
, 1, , 1 k k tamaodeclave . Cuando la profundidad es exactamente 6 la
construccin de la clave est completa. Entonces se presenta en pantalla y se produce
la vuelta atrs:
if(depth == TAM_CLAVE) //Salida de recursin
{
for(int i=0; i<TAM_CLAVE; i++)
cout<<clave[i];
cout<<endl;
return; //Vuelta atrs
}
La generacin de sucesores se lleva a cabo en las lneas de cdigo:
clave[depth]=-1;
for(int j=0; j<TAM_NUMEROS; j++)
clave[depth]+=1;
La primera instruccin inicializa la configuracin del estado en el nivel de
profundidad siguiente (en el nivel de recursin k se generan los sucesores con valores
de 0 a 9 en la posicin k-sima de la clave). En este caso no se almacenan todos los
sucesores localmente en cada nivel sino que segn se van generando se llama a la
funcin de siguiente nivel. El cdigo completo de generacin y llamada es:
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 128
Universidad Politcnica de Madrid -UPM
clave[depth]=-1;
for(int j=0; j<TAM_NUMEROS; j++)
{
clave[depth]+=1;
FuncRec(depth+1);
}
El procedimiento es completo y para ello basta con analizar la forma del rbol
de bsqueda. El rbol tiene 6 niveles de profundidad y en cada nivel, todos los nodos
tienen exactamente 10 hijos (lo que se conoce como factor de ramificacin del rbol),
por lo que en el ltimo nivel hay exactamente 10
6
nodos hoja que son el nmero de
claves posibles a generar.
7.5.2.1.1 Comparativa entre ambos algoritmos
Desde la perspectiva de la complejidad computacional, el algoritmo iterativo es
ms eficiente en tiempo ya que el algoritmo recursivo tiene que generar no solamente
los nodos hoja sino el resto del rbol. El nmero de nodos totales N de un rbol
uniforme con factor de ramificacin b y profundidad d es:

2 3 1
1
d d
N b b b b b
En el ejemplo b=10 y d = 6 con 10
6
hojas y 1+10+100+1000+10000+100000
=111.111 nodos adicionales hasta completar la totalidad del rbol (aproximadamente
un 11%). Adems los compiladores modernos consiguen buenas optimizaciones de
iteraciones pero no de recurrencias.
En la parte positiva del cdigo recursivo cabe destacar:
Es ms compacto: El nmero de sentencias que necesita es claramente
ms corto y adems no depende del tamao de la clave.
Lamentablemente no se puede decir lo mismo de la legibilidad.
Es parametrizable completamente: El algoritmo iterativo permite
definir un parmetro TAM_NUMEROS configurable pero no permite
definir el parmetro TAM_CLAVE. Esto quiere decir que habr que
aadir tantas sentencias for como nmeros tenga la clave, lo que no
ocurre en la versin recursiva.
En cuanto a los requisitos en espacio, ambas implementaciones presentan un
buen comportamiento. En el caso de la versin recursiva solamente se emplea la pila
de llamadas para pasar el parmetro profundidad que es la nica informacin que se
requiere para construir los nodos sucesores. Los diferentes estados se generan al
vuelo actualizando una nica variable global clave.
En este ejemplo la versin iterativa es ms intuitiva porque el problema de
desciframiento de claves se presta a ello. Sin embargo, existen muchos otros
problemas donde no es fcil, ni mucho menos intuitivo, implementar el control de las
iteraciones para conseguir la solucin. Para estos problemas y debido al buen
comportamiento de la bsqueda primero en profundidad en cuanto al consumo de
memoria, el uso de recursin es preferible. Los algoritmos ms eficientes para muchos
problemas NP-Duros (como por ejemplo el problema del Mximo Clique) se
implementan mediante esta tcnica.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 129
Universidad Politcnica de Madrid -UPM
7.5.2.2. Permutaciones
Un ejemplo ligeramente ms complicado es la generacin de permutaciones de
N nmeros mediante una bsqueda recursiva primero en profundidad. En este caso el
estado se almacena en un vector de N nmeros denominado permuta. En el nodo
raz permuta tiene todos sus elementos a cero y contiene el valor de la permutacin
en los nodos hoja. Como en el ejemplo anterior, los nodos intermedios del rbol sirven
para ir rellenando la estructura de datos de forma adecuada. Para permutaciones de N
nmeros, el rbol de bsqueda tiene profundidad N+1, donde el nivel 0 corresponde al
nodo raz y el nivel N al de las N! hojas solucin.
La funcin recursiva propuesta bien escribe el valor cero en el vector permuta
o bien escribe el valor del nivel en el rbol del nodo actual. Un valor cero en permuta
indica al generador de sucesores que esa posicin debe ser rellenada en niveles
superiores y un valor distinto de cero determina el valor de la permutacin en esa
posicin para cualquier nodo sucesor. De manera intuitiva, la funcin recursiva genera
tantos sucesores como valores a cero (o huecos) tiene permuta en el momento de la
invocacin. Inicialmente, permuta tiene todos los valores a cero con lo que tendr N
sucesores en el nivel 1, lo que se corresponde con las diferentes posiciones del 1 en las
N! permutaciones. En la llamada recursiva del nivel 2, permuta ya tiene puesto el 1
en alguna posicin con lo que el nmero de sucesores ser N-1, las diferentes
posiciones que puede ocupar el 2 en el conjunto de permutaciones posibles fijado ya el
1. La bsqueda contina expandiendo nodos hasta alcanzar las hojas en el nivel N, en
cuyo caso permuta est completa (carece de huecos) y se produce la vuelta atrs. La
figura 8 muestra el rbol de estados completo para el procedimiento propuesto con N
= 3.
0 0 0
0 0 1
0 1 0 1 0 0
0 2 1 2 0 1 0 1 2 2 1 0 1 0 2 1 2 0
3 2 1 2 3 1 3 1 2 2 1 3 1 3 2 1 2 3
0
1
2
3
0 0 0
0 0 1
0 1 0 1 0 0
0 2 1 2 0 1 0 1 2 2 1 0 1 0 2 1 2 0
3 2 1 2 3 1 3 1 2 2 1 3 1 3 2 1 2 3
0
1
2
3

Figura 8. rbol de bsqueda para generar permutaciones de 3 nmeros mediante la
tcnica de primero en profundidad.
Como en el ejemplo anterior, ocurre que el nmero de estados generados es
superior al nmero de permutaciones solucin. Para el rbol de la figura se puede
demostrar que el nmero de nodos computados (llamadas a la funcin recursiva) ser
ms del doble y menos del triple de las permutaciones posibles (por ejemplo, para N =
4 los nodos visitados son 65 y existen 24 permutaciones posibles).
Una posible implementacin de la funcin recursiva que recorre el rbol de la
figura 8 primero en profundidad es:

Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 130
Universidad Politcnica de Madrid -UPM
int nivel = -1;
void FuncRec(int k)
{
nivel++;
permuta[k] = nivel;
if(nivel == N){ //Nodo hoja: Permutacin generada
Mostrar();
}
else{
for (int pos = 0; pos < N; pos++){
if (permuta[pos] == 0)
FuncRec(pos);
}
}
nivel--;
permuta[k] = 0;
}
La funcin emplea la variable global nivel para llevar la cuenta del nivel de
profundidad del rbol generado y permuta para almacenar las permutaciones y guiar
la bsqueda. La informacin pasada en cada llamada es la posicin en permuta
donde se va a aadir el valor correspondiente al siguiente nivel de profundidad. Nada
ms entra en la funcin se determina el primer estado sucesor:
nivel++;
permuta[k] = nivel;
para despus comprobar si se est hoja, en cuyo caso se muestra la
permutacin completa en pantalla:
if(nivel == N) Mostrar();
En caso de que el nodo actual no sea un nodo hoja se generan el resto de
estados sucesores que, como se explic anteriormente, correspondern a valores
nulos de permuta. Esta condicin se verifica justo antes de la expansin:
for (int pos = 0; pos < N; pos++)
if (permuta[pos] == 0) FuncRec(pos);
Finalmente, tanto si es un nodo hoja como si no, se borra en el estado del nivel
anterior la ltima modificacin de permuta para conseguir que el generador de
sucesores en dicho nivel funcione correctamente. En el nivel 1 del rbol de bsqueda
en la figura 8, esto equivale a borrar el 1 de permuta[0] justo antes de la vuelta
atrs al nodo raz, para que el nuevo nodo sucesor sea en efecto permuta={0,1,0}
y no permuta={1,1,0}. En este segundo caso, los sucesores que se generaran no
seran correctos. El cdigo que realiza el borrado es:
nivel--;
permuta[k] = 0;
Inicialmente nivel se inicializa a -1 para que la primera llamada a FuncRec
corresponda con el nivel 0 que sirve como ndice de la primera modificacin de
permuta. Permuta arranca con todo ceros. El cdigo completo que muestra todas
las permutaciones de 4 nmeros en pantalla es el siguiente:
#include <iostream.h>
#define N 4

int nivel=-1;
int permuta[N];
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 131
Universidad Politcnica de Madrid -UPM
void Mostrar(){
for (int i = 0; i < N; i++)
cout<<permuta[i];
cout<<endl;
}

void FuncRec(int k){
nivel++;
permuta[k] = nivel;
if(nivel == N){ //Nodo hoja: Permutacin generada
Mostrar();
}
else{
for (int pos = 0; pos < N; pos++){
if (permuta[pos] == 0)
FuncRec(pos);
}
}
nivel--;
permuta[k] = 0;
}

void main(){
for (int i = 0; i < N; i++)
permuta[i] = 0;
FuncRec(0);
}
Cabe destacar que tanto en el generador de permutaciones como en el
generador de claves no se ha seguido estrictamente en la implementacin el
pseudocdigo descrito en la figura 7. En particular, no se ha empleado la pila de
llamadas para almacenar toda la informacin de los estados frontera en cada nivel del
rbol por dos razones:
Era posible mantener una nica estructura de datos global y modificarla
localmente para conseguir representar todos los estados del rbol de
bsqueda y
En ambos ejemplos se ha generado la informacin relativa a las
transiciones de forma secuencial con las llamadas recursivas de forma
que resultaba innecesario almacenar todos los estados nuevos de golpe.
En la prctica ambas condiciones no son demasiado frecuentes y es ms
habitual encontrar implementaciones que siguen exactamente el pseudocdigo
descrito en la figura 7, con la siguiente salvedad: si el tamao del espacio de estados es
muy grande o si se busca mxima eficiencia, la reserva de espacio en memoria
reservado para variables locales a la funcin recursiva resulta excesivamente lenta ya
que se debe asignar y liberar en cada llamada. En estos casos, la solucin habitual pasa
por reservar a priori el espacio en memoria para todos los estados del rbol (siempre
que sea posible) antes de lanzar el procedimiento de bsqueda recursivo. En resumen,
los identificadores que reservan espacio en memoria para la informacin de los
estados del rbol deben ser globales a la funcin recursiva si se busca una mxima
eficiencia.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 133
Universidad Politcnica de Madrid -UPM

8
8
.
.

EJECUCIN DISTRIBUIDA DE TAREAS
8.1. INTRODUCCIN
La problemtica de la ejecucin distribuida de tareas est hoy en da
plenamente vigente despus del gran desarrollo que ha tenido Internet. En la prctica
existen innumerables problemas computacionales donde se produce una fuerte
explosin combinatoria que no son abordables adecuadamente por una nica unidad
de proceso. Para estos casos, el rpido desarrollo de Internet est llevando, cada vez
ms, al empleo de los tiempos muertos de la ingente capacidad de procesamiento
conectada a la red para realizar, lo que podra denominarse, supercomputacin
distribuida. Entre los numerosos ejemplos de este tipo de procesamiento cabe
destacar el cmputo del genoma humano.
El problema de la computacin distribuida o descentralizada est
estrechamente ligado con el de la computacin paralela. En este caso, los avances
tecnolgicos han permitido la aparicin de nuevos procesadores formados por
mltiples ncleos (unidades de procesamiento) que ya se comercializan a gran escala.
Por ejemplo, los procesadores Cell, desarrollados conjuntamente por Sony, IBM y
Toshiba en el 2001, aceleran notablemente aplicaciones de procesado de vectores y
multimedia. La videoconsola PlayStation3 de Sony fue su primera gran aplicacin. Otro
ejemplo interesante es el gran avance que han tenido la arquitectura de las tarjetas
grficas modernas, hasta el punto de que muchos clculos pueden llevarse a cabo
ahora ms rpidamente por su unidad de procesamiento (conocida como GPU), en
comparacin con las CPUs tradicionales.
Tanto la computacin distribuida como la computacin en paralelo se basan en
la descomposicin del procedimiento a realizar en subtareas, lo ms independientes
posibles, de tal modo que la solucin final se pueda generar con cierta facilidad a partir
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 134
Universidad Politcnica de Madrid -UPM
de las soluciones de cada una de las partes. Existen problemas fcilmente
paralelizables (como por ejemplo la generacin de claves o el problema de las N-
Reinas) y otros mucho menos aptos para ello (e.g. muchos problemas no triviales de
optimizacin o el algoritmo de bsqueda mini-max).
Las dificultades de trocear un problema en partes adecuadas para su
computacin por separado se pueden clasificar en 3 grandes grupos. Estos son:
Necesidad de comunicacin entre las unidades de procesamiento, con
el consiguiente incremento en el tiempo de cmputo total.
Particiones no independientes: En la mayora de problemas
importantes es casi imposible un fraccionamiento en partes totalmente
independientes. En el caso general, aparecen problemas de
sincronizacin derivados de que unas unidades de procesamiento
necesitan esperar la finalizacin de otras para continuar.
Repeticin de tareas: En muchos casos no se puede evitar
fraccionamientos con solapamiento. Esto hace que se pueda estar
ejecutando a la vez la misma tarea en diferentes unidades de
procesamiento.
En este captulo se muestra detalladamente un ejemplo de computacin
distribuida para un problema clsico del mundo de los juegos: el problema de las N-
Reinas.
8.2. EL PROBLEMA DE LAS N-REINAS

8.2.1 Historia
El problema de las 8-Reinas consiste en colocar en un tablero de ajedrez de
dimensiones 8x8, ocho reinas tal que ninguna se ataque entre s de acuerdo con las
reglas del ajedrez. La generalizacin del problema a un tablero de dimensiones N x N
se conoce como el problema de las N-Reinas.
Este problema fue publicado por primera vez de forma annima en la revista
alemana Schach en el ao 1848; posteriormente se le atribuy a un ajedrecista del
momento, Max Bezzel, del que poco ms se conoce. Ya en aquel tiempo atrajo la
atencin de la lite matemtica, entre los que se inclua el gran Carl Friedrich Gauss,
que intent enumerar todas las distintas soluciones al problema. Gauss slo pudo
encontrar 72 configuraciones distintas, lo que da una idea de la dificultad de este
problema aparentemente sencillo. Solo unos aos ms tarde, en 1850, Nauck public
las 92 soluciones del problema. En 1901, Netto por primera vez generaliz el problema
a encontrar N reinas en un tablero N x N, aunque otras fuentes atribuyen al propio
Nauck ese honor.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 135
Universidad Politcnica de Madrid -UPM
8.2.2 Caractersticas
El problema de las N-Reinas es un problema terico que se enmarca dentro
del rea de juegos. Ha sido un problema ampliamente estudiado desde la segunda
mitad del siglo XIX y para el que se han descubierto algunas soluciones analticas
cerradas; stas describen un procedimiento para obtener una o algunas pocas
configuraciones objetivo para todo valor de N (N>3) (obviamente para N=1 la solucin
existe y es trivial). Un ejemplo de solucin cerrada se enuncia a continuacin:

Se anima a lector a emplear este procedimiento para encontrar una
configuracin objetivo para valores de N bajos (por ejemplo N=10). Este y otros
mtodos analticos permiten afirmar los dos siguientes postulados:

Sin embargo, estos mtodos puramente analticos no son capaces de
responder a ninguna pregunta acerca de la forma del espacio de estados del problema,
ni tan siquiera proporcionar un conjunto de soluciones representativo de cada
instancia.
En el campo de la Inteligencia Artificial, el problema de las N-reinas se emplea
como demostrador de prcticamente todas las tcnicas de bsqueda heurstica
conocidas, dada la sencillez del enunciado y la tremenda explosin combinatoria que
genera. Empleando tcnicas de mejora iterativa basadas principalmente en
minimizacin de conflictos, Sosic y Gu a principios de los aos 90 pudieron ubicar ms
de 3.000.000 de reinas en un tablero vaco; esta lnea de investigacin contina abierta
en la actualidad.
stas y otras tcnicas de bsqueda local, sin embargo, no permiten encontrar
todas las soluciones del problema. Este ltimo es el escenario ms difcil ya que la
explosin combinatoria que se produce empleando una bsqueda exhaustiva
desinformada (e.g. un procedimiento primero en profundidad) es
2 2
2
!
!( )!
N N
N N N N
, lo que para valores de N mayores de 30 resulta muy difcilmente
1. Sea R la parte entera del resto de N/12
2. Sea L el conjunto de todos los nmeros pares de 2 (incluido) a N
en orden creciente.
3. Si R es 3 o 9 coloque el 2 al final de la lista
4. Aada a L (empezando por el final) el conjunto de nmeros impares
de 1 a N de acuerdo a las siguientes reglas:
a. Si R es 8 intercambie parejas (por ejemplo
3,1,7,5,11,9,15,13...)
b. Si R es 2 intercambie las posiciones del 1 y el 3 y coloque
el 5 al final de L
c. Si R es 3 9 coloque 1 y 3 al final de la lista
manteniendo el orden
5. Coloque la primera reina en la casilla de la primera fila que
indica el primer nmero de L; la segunda reina en la casilla de la
segunda fila indicada por el segundo nmero de L y as
sucesivamente.
1. El problema tiene solucin para N =1 y para todo N mayor 3
2. Se sabe como construir al menos una solucin cuando sta existe
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 136
Universidad Politcnica de Madrid -UPM
computable en la prctica. Esta cifra puede mejorarse mucho teniendo en cuenta que
slo puede colocarse una reina por fila y por columna. An con todo, la bsqueda de
todas las posibles configuraciones necesita de heursticas para atravesar el desierto
formado por el gigantesco espacio de bsqueda y encontrar los oasis de soluciones.
Una caracterstica singular de las N-reinas es que, si bien la dimensin del
espacio de estados es claramente exponencial en el nmero de reinas a colocar, el
nmero de soluciones tambin crece exponencialmente con N (ver Tabla 1).
Tabla 1. Nmero de soluciones distintas del problema de las N-Reinas para diferentes
valores de N.
4 5 6 7 8 9 10 11 12 13 14 15
2 10 4 40 92 352 724 2.680 14.200 73.712 365.596 2.279.184


Esta distribucin no es homognea en el espacio de estados sino ms bien
existen enormes zonas vacas salpicadas de grandes concentraciones de soluciones.
Intuitivamente esto quiere decir que, a mayor N, no es en absoluto evidente que el
problema sea exponencialmente ms difcil (es ms, todo apunta a que esta afirmacin
es falsa).
A principios de los 90, Kal encontr una heurstica que permita computar las
primeras 100 soluciones para cualquier N entre 4 y 1000 (ambos inclusive) en un
tiempo casi lineal en N, por lo que conjetur que la densidad del espacio de soluciones
podra ser uniforme. Recientemente, en una investigacin llevada a cabo por los
propios autores se ha encontrado una nueva heurstica que corrobora esa afirmacin y
extiende el cmputo a valores de N hasta 5000.
Cuando se aborda el problema de las N-Reinas desde la perspectiva de la
completitud se emplean fundamentalmente dos enfoques distintos:
Conocer el nmero exacto de soluciones que existen para cualquier
valor de N: En este enfoque interesa slo el nmero exacto de
soluciones y no necesariamente su enumeracin explcita ni, desde
luego, su almacenamiento (lo que sera, por otro lado, imposible dada la
explosin combinatoria del nmero de soluciones).
Enumerar las primeras K soluciones para cualquier valor de N: En este
caso se exige el cmputo explcito. El valor de K no suele ser muy grande
(por ejemplo 100), pero el suficiente para que el procedimiento de
resolucin no pueda emplear mtodos analticos cerrados.
Fuera del mbito de los juegos, es interesante mencionar la aplicacin del
problema de las N-Reinas en el campo de la optimizacin, donde constituye un
importante modelo terico en problemas de planificacin (scheduling) y de asignacin
de tareas (task assignment problems).
8.2.3 Estructuras de datos
La implementacin tpica del problema divide el tablero por filas y codifica una
solucin cualquiera como N nmeros entre 1 y N que representan las casillas ocupadas
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 137
Universidad Politcnica de Madrid -UPM
en cada fila, para un total de N! posibles configuraciones (las permutaciones de los N
nmeros (ver figura 2)).

4 3 2 1 4 3 2 1
F
1
F
2
F
3
F
4
F
1
F
2
F
3
F
4

Figura 2. Solucin del problema de las 4-Reinas. Dicha solucin puede codificarse
como la cuadrupla {2, 4, 1, 3} que corresponde a la columna de la casilla ocupada en
cada fila.
En consecuencia, es suficiente un vector de N nmeros para codificar cualquier
estado del espacio de soluciones y un procedimiento de bsqueda sistemtico (vlido)
es cualquier algoritmo que genere permutaciones.
Si se pretende abordar la generacin explcita de las primeras k soluciones,
prcticamente la nica alternativa razonable es realizar una bsqueda primero en
profundidad guiada por una heurstica adeudada. En este caso, la bsqueda no se debe
desarrollar en un espacio de soluciones (como en el clculo de permutaciones) sino
que cada estado del rbol se corresponde con una fila del tablero (o, alternativamente
una columna), que se va rellenando hasta completar una solucin en los nodos situado
a una profundidad N. De manera intuitiva en cada nivel del rbol se aade una reina al
tablero hasta alcanzar una solucin. Si en un estado concreto no existen casillas libres
en la fila o columna correspondiente se produce una vuelta-atrs y la ltima reina
colocada se elimina del tablero.
Segn se expuso en la seccin 7.3, el control de la bsqueda slo requiere
almacenar tanto el camino actual como todos los nodos sucesores directos de dicho
camino. Si tomamos como factor de ramificacin medio del rbol (b) el valor de N/2, el
espacio mximo requerido durante la bsqueda, teniendo en cuenta que la
profundidad del rbol (d) no puede exceder de N, ser:

2
2 2 2
N N N
Espaciomximo d N
lo que no supone mayor problema para los computadores actuales.
Respecto a la generacin de los nodos sucesores a partir del padre, el mayor
coste computacional reside en el clculo de las casillas atacadas tras colocar una nueva
reina fruto de los rayos diagonales (las interacciones entre filas y columnas se pueden
computar de manera sencilla actualizando una estructura de filas y columnas
ocupadas).
Para el cmputo eficiente de las casillas libres al vuelo es necesario aadir
nuevas estructuras de datos como por ejemplo registros que llevan la cuenta del
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 138
Universidad Politcnica de Madrid -UPM
nmero total de las casillas no atacadas en filas, columnas y diagonales etc. stas y
otras estructuras bien elegidas permiten que la determinacin de las casillas libres se
realice en tiempo constante pero requieren tiempo adicional para su cmputo.
Es interesante mencionar que existen codificaciones ms o menos ingeniosas
que asocian bits con caractersticas del dominio de manera que una operacin de
enmascaramiento permite ejecutar varias operaciones en paralelo con significado en el
problema. Estas estructuras pueden ser auxiliares (como por ejemplo emplear un
vector de bits por cada diagonal del tablero, donde cada bit representa una casilla) o
pueden estar en el corazn mismo del control de la bsqueda. El algoritmo elegido
para implementar las N-Reinas que se describe en esta seccin emplea este tipo de
codificacin.
8.3. IMPLEMENTACIN CENTRALIZADA
Se presenta en esta seccin un procedimiento que permite obtener, al menos
en teora, todas las soluciones distintas del problema de las N-Reinas para valores de
N hasta 32 (en la prctica esto no va a ser posible debido al crecimiento fuertemente
exponencial de las soluciones con N segn muestra la tabla 1). El algoritmo genera
explcitamente todas las posibles soluciones y lleva la cuenta del total. La restriccin en
el valor de N se debe a que, para la codificacin de las casillas libres en una fila se
emplea un nico entero de 32 bits, un bit por cada casilla de la fila. El control de la
bsqueda se realiza mediante la tcnica primero en profundidad implementada de
forma recursiva (ver seccin 7.5).
Ms concretamente, el tablero se rellena por filas y la colocacin de una nueva
reina en una fila provoca un cambio de estado; se puede decir, por tanto, que la
bsqueda se realiza en un espacio de filas donde cada estado-fila queda determinado
por el nmero de casillas libres que dispone (aqullas casillas no atacadas por reinas ya
presentes en el tablero). Los estados-fila sucesores se generan emplazando una nueva
reina en cualquiera de las casillas libres del estado-fila actual, con la particularidad
que, debido a las estructuras de datos empleadas, las filas siempre se completan en
direccin descendente empezando por la parte superior del tablero.
Para aclarar estos conceptos, la figura 3 muestra una posible traza del rbol de
bsqueda para el problema de las 4-Reinas. Todos los nodos en un mismo nivel del
rbol se corresponden con la misma fila del tablero, pero con diferentes distribuciones
de casillas libres; el nodo raz del rbol, por tanto, corresponde al estado-fila extremo
superior del tablero que inicialmente est vaco. Los nodos hoja del rbol estn
marcados con una cruz, con la excepcin del nodo hoja solucin que se encuentra en el
4 nivel. Los nodos hoja en niveles del rbol inferiores a N capturan el hecho de que una
fila no tiene casillas libres, lo que supone un error en la ubicacin de una reina en
niveles superiores y provoca una vuelta atrs. Se observa como la realizacin de la
bsqueda en un espacio de filas en lugar de un espacio de soluciones permite podar la
bsqueda reduciendo el tamao de rbol generado.


Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 139
Universidad Politcnica de Madrid -UPM
nSol++ nSol++

Figura 3. Traza del rbol de bsqueda del problema de las 4-reinas. El nodo raz
representa la fila superior del tablero. Estados marcados con una cruz son nodos hoja
no solucin. Al encontrara una configuracin solucin se incrementa el contador nSol
y contina la bsqueda.
En el ejemplo, la bsqueda yerra al comenzar colocando una reina en la esquina
superior derecha del tablero. Tras producirse la ltima poda en el nivel 3 (para la
configuracin de reinas en estados superiores del camino no existen casillas libres en la
fila actual) se produce una vuelta atrs. Posteriormente, tras encontrar una
configuracin solucin (estado marcado con el parmetro nsol) se incrementa en una
unidad la cuenta de soluciones y la bsqueda contina hasta que no existen sucesores
que explorar o bien, en el caso general, el contador llega a un valor K.
8.3.1 Descripcin
En la implementacin propuesta, el control de la bsqueda obedece
ntegramente al pseudocdigo propuesto para bsquedas primero en profundidad en
el captulo 7. Las reinas se colocan por filas; para cada nuevo estado alcanzado se
realizan las siguientes tareas en orden:
1. Comprobacin si el nuevo estado es solucin: Para ello basta analizar si
el nivel de profundidad del rbol es N. En este caso se suma uno al
contador de soluciones y se realiza una vuelta-atrs para continuar por
un nuevo camino.
2. Seleccin de la siguiente fila no ocupada: Las filas se rellenan de arriba
abajo, empezando por la fila superior y terminando por la base del
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 140
Universidad Politcnica de Madrid -UPM
tablero. En cada nivel del rbol se coloca una reina en alguna de las
casillas libres de la fila correspondiente.
3. Generacin de los nodos sucesores: Para ello se calcula el nuevo
conjunto de casillas no atacadas (libres) para la fila del siguiente nivel.
Este es el proceso ms costoso en tiempo de cualquier implementacin
y las estructuras de datos se eligen para minimizar dicho cmputo. Si no
existen casillas libres en la fila elegida (y la profundidad del rbol es
menor que N) entonces es que se ha producido un error en la
colocacin de alguna de las reinas anteriores. Se efecta entonces una
vuelta-atrs al nodo padre para retomar la bsqueda.
4. Seleccin de un nodo sucesor NS de entre los generados en el paso 3.
5. Convertir NS en el estado actual e ir al paso 1: Este paso se implementa
como llamada recursiva a la propia funcin encargada del
procedimiento de bsqueda.
8.3.2 Estructuras de datos
Para optimizar el cmputo de las casillas libres en cada fila se ha empleado una
codificacin mediante vectores de bits. Este tipo de codificaciones se utilizan con
mucha frecuencia para tratar de reducir el tiempo de cmputo aprovechando que los
registros de la CPU pueden efectuar un nmero de operaciones de enmascaramiento
de bits en paralelo equivalente al tamao de los registros de la ALU (tpicamente 32 o
64). Intuitivamente, si se consiguen asociar bits a unidades de informacin acerca del
dominio, entonces una sola operacin de enmascaramiento entre dos registros
permite realizar 32 o 64 operaciones con sentido, con la consiguiente ganancia en
eficiencia.
Las estructuras de datos empleadas son:
El tablero: La informacin del tablero, en cada nodo, se reduce a una
fila, y ms concretamente a las casillas libres (no atacadas) de la fila.
Cada fila se codifica como un nmero de 32 bits donde cada casilla
equivale a un bit. Una casilla libre (no atacada) se codifica con un bit a
uno y cero en caso contrario. La posicin relativa de los bits indica la
posicin de la casilla en la fila; el bit ms bajo representa la columna
ms a la derecha del tablero, el segundo bit la columna inmediatamente
a la izquierda y as sucesivamente, para un mximo de N bits por fila, el
nmero de columnas del tablero. El inconveniente principal de esta
codificacin es que slo es vlida para tableros de dimensin 32 x 32
como mximo.
Los movimientos de la reina: Los movimientos de la reina en el ajedrez
(todas las casillas en las 8 direcciones en el plano) se van a codificar
como operaciones de desplazamiento y enmascaramiento de bits. La
idea fundamental es que slo es necesario computar las casillas
atacadas en la fila correspondiente al estado actual y no las del resto
de filas todava sin rellenar. El procedimiento cmputo se reduce a
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 141
Universidad Politcnica de Madrid -UPM
generar las casillas libres en una determinada fila a partir del
conocimiento de casillas libres en la fila inmediatamente superior. Para
ello se utilizan 3 enteros izq, abajo y dcha. Una explicacin ms
en detalle se expone en la seccin siguiente.
Nmero de fila del nodo actual: Coincide con el nivel de profundidad
del nodo en el rbol de bsqueda empezando la cuenta por el borde
superior del tablero (fila 0) y terminando en la base (fila N-1). Se
almacena en un entero en cada nivel y se gestiona a travs de la pila de
llamadas.
Estructuras auxiliares: La configuracin de inicial de las casillas libres en
una fila se guarda en la variable TODOUNOS. Este valor es constante
durante toda la bsqueda y se calcula una vez al inicio. Otras estructuras
son: un entero nSOL que lleva la cuenta del nmero de soluciones
encontradas hasta el momento y la constante N que indica la dimensin
del tablero.
8.3.3 Control de la bsqueda
La funcin recursiva que controla la bsqueda se denomina FuncRec. Su
definicin es la siguiente:
void FuncRec(int fila, int izq, int abajo, int dcha)
{
int estado, sucesor;

if (fila == N) {
nSOL++;
} else {
estado = TODOUNOS & ~(izq | abajo | dcha);
while (estado) {
sucesor = -estado & estado;
estado ^= sucesor;
FuncRec(fila+1, (izq | sucesor )<<1,
abajo | sucesor, (dcha | sucesor)>>1);
}
}
}
Segn lo ya expuesto, los nodos del rbol de bsqueda son filas sin completar y
para cada nuevo estado-fila hay que de actualizar el conjunto de casillas libres (no
atacadas) en esa fila. Esta actualizacin se realiza a partir de la informacin que el
nodo padre pasa a su sucesor, los parmetros fila, izq, abajo y dcha.
Inicialmente se comprueba si el nuevo estado-fila es un nodo hoja solucin;
para ello basta con saber si se ha alcanzado la profundidad mxima del rbol N. En
caso afirmativo se suma uno al contador de soluciones nSOL y se vuelve atrs en el
rbol para continuar la bsqueda. Esta comprobacin se realiza en la instruccin
if (fila == N) nSOL++;
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 142
Universidad Politcnica de Madrid -UPM
Si la fila actual no es la ltima, entonces lo primero es generar de forma
explcita el estado a partir de la informacin recibida del nodo padre. Esto se efecta
mediante operaciones de enmascaramiento de bits en la lnea de cdigo
estado = TODOUNOS & ~(izq | abajo | dcha)
Esta instruccin requiere una explicacin ms detallada. El operador & en C es
el operador binario AND a nivel de bits. Su resultado es un bit a 1 si en esa posicin los
bits de ambos operandos estn tambin a uno. En caso contrario el bit toma el valor
cero.
Un ejemplo:

c1 = 0x4501000101
c2 = 0x7101110001
c1 & c2 = 0x4101000001
c1 = 0x4501000101
c2 = 0x7101110001
c1 & c2 = 0x4101000001

El operador ~ es el operador unario complemento a uno en C. Como
resultado, el nmero sobre el que opera intercambia los bits a cero por los bits a uno.
La combinacin de operadores c1 & ~ c2 es interpretada por el compilador como c1
&(~ c2 ). El resultado es la puesta a nivel bajo de los bits de c1 que estn en la posicin
ocupada por los bits a 1 de c2. Esta combinacin de operadores se conoce
comnmente como borrado de c1 ya que el segundo operando lleva la informacin
de los bits a borrar en el primero. Un ejemplo:

c1 = 0x4501000101
c2 = 0x7101110001
c1 &~ c2 = 0x0400000100
c1 = 0x4501000101
c2 = 0x7101110001
c1 &~ c2 = 0x0400000100

Volvamos ahora al cmputo de las casillas libres en la nueva fila. La instruccin
que genera el nuevo estado-fila lo hace borrando aquellas casillas libres (inicialmente
todas lo son por lo que estado coincide con TODOUNOS), que ahora resultan atacadas
por reinas ya emplazadas en el tablero.
Esta informacin est contenida en los parmetros izq, abajo y dcha
que, de manera intuitiva, se corresponden con las casillas atacadas por todas las reinas
ya colocadas, segn las tres direcciones del plano correspondientes (inferior izquierda,
abajo, inferior derecha). No es necesario analizar los ataques en las otras 5 direcciones
del plano porque las reinas se van colocando por filas en orden descendente y, por
tanto, cualquier casilla atacada en la fila actual solo se puede deber a reinas situadas
en filas superiores.
Los 3 parmetros con informacin de casillas atacadas son enteros de 32 bits.
Un bit a uno en cualquiera de ellos representa una casilla atacada por reinas situadas
en filas superiores en la direccin correspondiente. La figura 4 muestra un ejemplo del
valor de estas estructuras de datos para el problema de las 4-Reinas. Una reina acaba
de emplazarse en la fila superior y la nueva llamada recursiva a FuncRec recibe como
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 143
Universidad Politcnica de Madrid -UPM
parmetros izq = 0100
2
, abajo = 0010
2
y dcha = 0001
2
, que corresponden a las casillas
atacadas en las tres direcciones. El nuevo estado, las casillas libres en la fila
inmediatamente debajo, se construye borrando todos esos bits de TODOUNOS (todas
las casillas libres), lo que da como resultado una nica casilla libre estado = 1000
2

marcada por el cuarto bit a uno (la casilla del extremo izquierdo de la fila).
izq abajo dcha
izq = 0100
2
abajo = 0010
2
dcha = 0001
2
estado
nuevo
= 1000
2
F
nueva
izq abajo dcha
izq = 0100
2
abajo = 0010
2
dcha = 0001
2
estado
nuevo
= 1000
2
F
nueva

Figura 4. Valor de los parmetros izq, abajo y dcha tras colocar una reina en la fila
superior del tablero para el problema de las 4-Reinas. El estado en la fila nueva viene
determinado por la operacin estado = 1111
2
&~ (izq | abajo | dcha) = 1000
2
.
Computado el estado actual de la fila, los posibles sucesores se obtienen
situando una nueva reina en cualquiera de los bits a 1 de la variable estado. Esto se
realiza de forma iterativa en el bucle determinado por
while (estado)
{
sucesor = -estado & estado;
estado ^= sucesor;
FuncRec(fila+1, (izq | sucesor )<<1,
abajo | sucesor, (dcha | sucesor)>>1);
}
que nuevamente requiere cierta explicacin. La primera lnea de cdigo, nada
ms entrar en el bucle, obtiene el primer bit a 1 de la palabra estado mediante una
ingeniosa pero muy conocida operacin a nivel de bits, combinacin de operadores y
&:
sucesor = -estado & estado;
La operacin resta vista como operador unario calcula el complemento a 2 del
operando al que afecta. La secuencia de operaciones - y & sobre un mismo nmero
borra todos los bits exceptuando el bit a uno ms bajo de dicho nmero. Por ejemplo,
-1101
2
& 1101
2
devuelve 0001
2
mientras que -1100
2
& 1100
2
devuelve
0100
2
. El lector puede fcilmente comprobar que esta propiedad se cumple para
cualquier nmero. Por tanto, sucesor ser un nmero formado por un nico bit a 1, el
bit ms bajo de estado.
La siguiente lnea de cdigo dentro del bucle completa el control del mismo.
estado ^= sucesor;
El operador ^ en C es la mscara XOR bit a bit, operador binario tambin
conocido por distinto ya que mantiene a 1 aquellos bits que son diferentes en los dos
operandos y borra los que son iguales. En este caso, como el nico bit a uno de
sucesor tiene que estar en estado, el resultado es el borrado de ese bit en
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 144
Universidad Politcnica de Madrid -UPM
estado. Intuitivamente, en cada iteracin se elige el bit a uno ms bajo de estado
y despus se borra, lo que implica que las reinas se colocan de derecha a izquierda en
las casillas libres de cada fila. Cuando estado est vaco finaliza la ejecucin del
bucle. Es interesante hacer notar que el mismo resultado se obtendra mediante el
operador ya visto de borrado:
estado &= ~sucesor;
pero sera menos eficiente ya que se necesita una operacin ms de
enmascaramiento.
Finalmente, decidido una vez el sucesor, es necesario actualizar las estructuras
de datos izq, abajo y dcha antes de proceder a una nueva llamada recursiva.
En el cdigo esto se realiza en la propia instruccin de llamada a la funcin:
FuncRec(fila+1, (izq | sucesor )<<1,
abajo | sucesor, (dcha | sucesor)>>1);
El primer parmetro de FuncRec, el nmero de la fila, siempre se incrementa
en una unidad. Su valor inicial es 0, la fila superior del tablero. El segundo parmetro
es la actualizacin de la estructura izq a partir de su valor actual. Este cmputo
puede verse como un desplazamiento hacia la izquierda una unidad de un nmero que
tiene por bits a uno todas las columnas donde se encuentran las reinas ya colocadas en
el tablero (incluyendo la ltima, en la fila actual y posicin sucesor) segn se
desprende de la figura 5. En C, el operador desplazamiento a izquierdas tiene como
smbolo <<. La sintaxis es la misma que la del operador de flujo de salida pero, en este
caso, el operando de la derecha es un entero que indica el nmero unidades de
desplazamiento de los bits del operando de la izquierda en la direccin apuntada por el
smbolo.
estado
act
= 11100
2
sucesor = 00100
2
izq
act
= 00010
2
1 1
1
F
actual
F
nueva
izq
nue
= (izq
act
| sucesor) <<1 = 01100
2
estado
act
= 11100
2
sucesor = 00100
2
izq
act
= 00010
2
1 1
1
F
actual
F
nueva
izq
nue
= (izq
act
| sucesor) <<1 = 01100
2

Figura 5. Actualizacin de la estructura de datos izq. izq
act
es el valor en la fila
actual (F
actual
) e izq
nue
el nuevo valor calculado a partir del anterior. estado
act

contiene las casillas libres en la fila actual. De entre stas, se ha elegido colocar una
nueva reina en la casilla central de F
actual
, lugar que ocupa en la figura,
almacenndose su posicin en la variable sucesor.
Ahora bien, para obtener el nuevo valor de izq no basta con desplazar el
antiguo una posicin a la izquierda (equivalente a izq
nue
= izq
act
<<1) ya que
esta operacin tiene en cuenta los ataques en esta diagonal de todas las reinas
situadas en filas anteriores a F
actual
pero no incluye la ltima que se encuentra en
sucesor. De ah que izq
nue
sea compute a partir de la unin entre sucesor e
izq
act
.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 145
Universidad Politcnica de Madrid -UPM
Un razonamiento anlogo puede hacerse para computar las casillas atacadas en
la diagonal descendente dcha solo que, en este caso, el desplazamiento de bits es
hacia la derecha una posicin (operador >> de C). En este punto es interesante
destacar que la operacin de enmascaramiento que genera el estado actual al entrar
en FuncRec:
TODOUNOS & ~(izq | abajo | dcha)
lleva implcita tambin la gestin de bordes. Este problema es inherente a los
juegos de tablero y debe tenerse muy en cuenta en la seleccin de las estructuras de
datos para codificar el problema. Como ejemplo, considrese el problema de las 4-
Reinas nuevamente. Si se coloca una reina en la esquina superior izquierda, la
codificacin de izq
nue
para la segunda fila sera 10000
2
, pero al estar situada la reina
en el extremo, ese bit a 1 queda fuera del rango de columnas del tablero. TODOUNOS
es, en este caso, 1111
2
y lleva implcita la informacin del tamao del tablero. La
mscara & ~ , por tanto, acta slo sobre las 4 casillas posibles de la fila resolviendo
el problema de rangos de forma muy eficiente y elegante.
Por ltimo, los nuevos ataques en la direccin vertical, sentido descendente
(variable abajo) coinciden con el valor anterior aadiendo sucesor. Esto es as ya
que el ataque a lo largo de cualquier columna corresponde al mismo bit en cada fila. La
figura 6 muestra todas las estructuras de datos relacionadas con el cambio de estado
para el ejemplo de la figura 4.
izq
n
= sucesor << 1 = 0100
2
estado
act
= 0000
2
F
actual
F
nueva
izq abajo dcha
sucesor = 0010
2
dcha
n
= sucesor >> 1= 0001
2
abajo
n
= sucesor | 1 = 0010
2
estado
nuevo
= 1111
2
&~ (0010
2
| 0001
2
| 0010
2
) = 1000
2
izq
n
= sucesor << 1 = 0100
2
estado
act
= 0000
2
F
actual
F
nueva
izq abajo dcha
sucesor = 0010
2
dcha
n
= sucesor >> 1= 0001
2
abajo
n
= sucesor | 1 = 0010
2
estado
nuevo
= 1111
2
&~ (0010
2
| 0001
2
| 0010
2
) = 1000
2

Figura 6. Ejemplo de cmputo de casillas libres. La reina en la figura est codificada
en sucesor y provoca la transicin a la fila nueva. La fila actual es el borde superior
del tablero (nodo raz del rbol) y los valores de izq
act
, dcha
act
y abajo
act
en
ese nodo son 0000
2
. Los valores de izq
n
, abajo
n
y dcha
n
en la figura
representan las casillas atacadas en la nueva fila.
8.3.4 Algoritmo de bsqueda
Una vez explicada en detalle la funcin recursiva principal que dirige la
bsqueda, el resto de cdigo no ofrece especial dificultad. El cdigo completo para el
problema de las 8-Reinas es:

Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 146
Universidad Politcnica de Madrid -UPM
#include <stdio.h>
#define N 8 //Max 32

const int TODOUNOS =(1 << N) - 1;
int nSOL;

void FuncRec(int fila, int izq, int abajo, int dcha)
{
int estado, sucesor;

if (fila == N)
{
nSOL ++;
}
else
{
estado = TODOUNOS & ~(izq | abajo | dcha);
while (estado)
{
sucesor = -estado & estado;
estado ^= sucesor;
FuncRec(fila+1, (izq | sucesor )<<1,
abajo | sucesor, (dcha | sucesor)>>1);
}
}
}

int main(void)
{
nSOL = 0;
FuncRec(0, 0, 0, 0);
printf("N=%d -> %d\n", N, nSOL);
return 0;
}
El cmputo de soluciones lo lleva la variable nSol y el valor inicial de las filas
TODOUNOS, ambas definidas como globales. Es interesante destacar las operaciones
de bits que sirven para inicializar TODOUNOS:
const int TODOUNOS =(1 << N) - 1;
Primeramente se desplaza la constante 1 (que hay que visualizar como un
nmero de 32 bits con el bit ms bajo a uno) N posiciones a la izquierda, con lo que se
sita en la posicin N+1. Debido al acarreo, la operacin resta de una unidad convierte
a unos todos los ceros a la derecha del uno desplazado. El uno en la posicin N+1
acta como barrera y evita, al ponerse a nivel bajo, la propagacin indebida del bit de
acarreo ms all de su posicin.
La llamada inicial a la funcin de bsqueda se realiza con todos los parmetros
a cero (izq, abajo y dcha estn a nivel bajo al inicio). Con estos valores, el
cmputo del estado-fila en el nodo raz tiene tambin valor 0, o visto de otro modo, la
primera reina puede emplazarse en cualquier casilla del borde superior del tablero
vaco.
Por ltimo, cabe destacar que la bsqueda que realiza este procedimiento es
desinformada al no incorporar ninguna heurstica de decisin. Las reinas se emplazan
en filas consecutivas en direccin descendente y se van colocando por columnas de
derecha a izquierda (posiciones bajas a posiciones altas de bits a 1 en estado). Por
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 147
Universidad Politcnica de Madrid -UPM
esta razn, y a pesar de que la codificacin admite tableros de dimensin hasta 32x32,
valores de N superiores a 18 difcilmente pueden ser resueltos por un computador
comercial con este algoritmo. A partir de N = 20 la tarea es prcticamente imposible.
8.4. IMPLEMENTACIN DISTRIBUIDA
El problema de las N-Reinas pertenece a la categora de problemas fcilmente
paralelizables; basta considerar como particiones del espacio problema las casillas
libres en cualquier fila. Resulta evidente que cada subproblema resultante de ubicar
una reina en una casilla libre del estado-fila actual es totalmente independiente del
subproblema derivado de seleccionar otra casilla libre en la misma fila. En
consecuencia, cada subrbol puede ser resuelto en paralelo sin necesidad de
sincronizacin y con la seguridad de que no se estn repitiendo configuraciones
solucin, un escenario idlico en el marco del cmputo paralelo.
Se muestra en esta seccin una implementacin de esta paralelizacin tipo del
problema de las N-Reinas, en el marco de un sistema distribuido. La implementacin
se ha desarrollado para la plataforma Win32 y se emplean Sockets para establecer las
comunicaciones entre los ordenadores remotos. El objetivo de este ejemplo, sin
embargo, no es mostrar el empleo de Sockets en esta plataforma, sino el de presentar
el potencial que tienen los sistemas distribuidos para resolver tareas en paralelo de
forma ms eficiente que un sistema centralizado, al ser capaces de aprovechar el
trabajo de mltiples unidades de proceso conectadas en red. Por este motivo, se
asumir que existe una clase de tipo wrapper que encapsula los servicios del recurso
Socket y que se encuentra a disposicin del programador mediante el mecanismo de
herencia. En este sentido, la mayor parte de las explicaciones que aparecen en esta
seccin pueden considerarse multiplataforma.
8.4.1 Arquitectura cliente-servidor
La arquitectura distribuida elegida tiene a un cliente que centraliza la
distribucin de la carga sobre un conjunto de servidores. El cliente se encarga de
subdividir el problema en partes que sern resueltas por los diferentes servidores en la
red; stos ltimos son los que ejecutan el algoritmo de bsqueda y devuelven como
resultado al cliente el nmero de soluciones encontradas de cada problema parcial. El
cliente, por su parte, tras finalizar el reparto de la carga, enva una peticin de
resultado a los diferentes servidores cada segundo. Cuando todas las soluciones
parciales han sido recibidas, muestra la suma total por pantalla.
Con objeto de simplificar el ejemplo, la particin del espacio se ha realizado
asignando en la primera fila (correspondiendo al borde superior del tablero) una casilla
libre a cada servidor; ste resuelve el subproblema resultante tras la ubicacin de
dicha reina en el tablero. En consecuencia, habr un mximo de N subproblemas a
resolver y podrn existir un mximo de N servidores trabajando en paralelo. La figura 7
muestra la arquitectura descrita.

Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 148
Universidad Politcnica de Madrid -UPM
Cliente
Servidores
Cliente
+
+
+
+
Servidores
Cliente
Servidores
Cliente
Servidores
Cliente
+
+
+
+
Servidores
Cliente
+
+
+
+
Servidores

Figura 7. Arquitectura cliente-servidor para el problema de las 4-Reinas. El cliente
divide el problema en 4 partes y recibe las soluciones de cada parte para generar el
total.
8.4.2 Protocolo de comunicacin
La informacin que tiene que circular entre cliente y servidor es bastante
escasa. En la etapa de reparto de carga el cliente solo tiene que enviar dos nmeros
enteros: el tamao del problema (parmetro N) y la posicin de la reina en la primera
fila (que determina la subtarea a resolver). Este parmetro se mide desde el borde
derecho del tablero; para un tablero de lado N la esquina superior derecha tiene valor
0 y la esquina superior izquierda valor N-1. El protocolo de este envo es una cadena de
caracteres que tiene la forma siguiente:
CABECERA:Nqueens
DATOS:<Tamao del tablero> <Casilla de la primera reina>
Este mensaje tiene acuse de recibo mediante la cadena OK por parte de cada
servidor para indicar que se ha recibido satisfactoriamente.
Una vez realizado el envo anterior, el cliente central lanza, cada segundo, una
peticin de resultado a cada servidor y recibe de ellos un entero solucin si han
terminado su parte. El mensaje de peticin de resultado es la cadena de caracteres
Resultado. Cada servidor devuelve entonces la solucin obtenida o -1 si no ha
terminado an. Cuando todos los mensajes de peticin han sido contestados
satisfactoriamente, el cliente presenta la suma de los resultados en pantalla.
8.4.3 Implementacin del cliente
En esta seccin se describe en detalle todo lo relativo al funcionamiento de la
parte del cliente. Como se indic la comienzo de esta seccin, se van a omitir la
mayora de detalles acerca de los servicios de Win32 para Sockets. A todos los efectos,
estos servicios se van a considerar transparentes para el programador y heredados de
una clase Socket a su disposicin. En cambio, s se describir en detalle la manera de
hacer uso de esta clase mediante el mecanismo de herencia.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 149
Universidad Politcnica de Madrid -UPM
8.4.3.1. Comunicacin con el servidor
Las comunicaciones entre cliente y servidor se realizan a travs de Sockets. A
efectos del ejemplo, bastar saber que existe una clase Socket que encapsula la
recepcin y envo de mensajes y que tiene (entre otras) dos funciones miembro
pblicas:
class Socket
{
public:
int SendMsg(const char* cad, int length);
int ReceiveMsg(char* cad, int* size, int timeout = 200);
//
}
que se encargan de las comunicaciones. La funcin SendMsg permite enviar
una cadena de tamao length (medido en bytes) mientras que ReceiveMsg
recibe una cadena de tamao mximo size. Ambos servicios devuelven un 0 si la
comunicacin se ha efectuado con xito.
Para el ejemplo se ha creado una clase cliente MyLiveClient que hereda
estos servicios de comunicaciones mediante derivacin pblica de la clase Socket. Su
fichero de cabecera es:
class MyLiveClient : public Socket
{
public:
int RecibirResultado();
int EnviarNReinas(int size, int posq);
MyLiveClient() {};
~MyLiveClient();
};
Las dos funciones importantes de la clase son EnviarNReinas y
RecibirResultado. La primera enva la informacin de la particin del problema
(el tamao del tablero y la posicin de la reina en la fila superior) y la segunda realiza la
peticin de resultado, ambas siguiendo el protocolo descrito en la seccin anterior. El
cdigo de la funcin envo no requiere demasiado comentario:
int MyLiveClient::EnviarNReinas(int size, int posq)
{
char cad[100];
sprintf(cad,"Nqueens %d %d",size, posq);
if(0!=SendMsg(cad,strlen(cad)+1))
return -1;

int max_size=100;
if(0!=ReceiveMsg(cad,&max_size))
return -1;

cout<<cad<<endl;

return 0; //OK (-1 ERROR)
}
Se emplea el servicio SendMsg heredado de la clase Socket para realizar el
envo de la tarea al servidor remoto y ReceiveMsg para gestionar un acuse de
recibo que se muestra en pantalla. En ambos casos, el control de errores se gestiona a
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 150
Universidad Politcnica de Madrid -UPM
travs del parmetro de retorno. La funcin strlen empleada dentro del segundo
parmetro del servicio SendMsg devuelve el nmero de caracteres de la cadena
argumento excluyendo el carcter nulo al final de la cadena. Este es el motivo por el
que en la instruccin
if(0!=SendMsg(cad,strlen(cad)+1)) return -1;
hay que aadir una unidad al resultado de strlen.
La implementacin de la funcin que pide y recibe el resultado es la siguiente:
int MyLiveClient::RecibirResultado()
{
// 0 OK, -1 ERROR

char cad[100];
int max_size=10, res=-1;

sprintf(cad,"%s","Resultado");
if(0!=SendMsg(cad,strlen(cad)+1))
return -1;

if(0!=ReceiveMsg(cad,&max_size))
return -1;

sscanf(cad,"%d",&res);
if(res>=0){
cout<<"Recibido resultado correcto: "<<res<<endl;
return res;
}
return -1;
}
De nuevo el cdigo no requiere demasiada explicacin. Una vez enviada la
peticin mediante el mensaje Resultado la instruccin
if(0!=ReceiveMsg(cad,&max_size))
return -1;
recibe en la cadena de caracteres cad la posible solucin numrica. Tras
formatear la cadena como nmero (mediante el servicio sscanf), se comprueba que
ste es mayor o igual que cero en cuyo caso se muestra un mensaje en pantalla y se
devuelve su valor. En caso contrario la funcin devuelve -1 para indicar que la tarea no
ha finalizado. Ntese que se acepta el valor cero como resultado porque pueden existir
subproblemas sin ninguna configuracin solucin (e.g. N=4 con la primera reina
situada en una de las esquinas).
8.4.3.2. Hilo principal del cliente
El hilo principal del lado del cliente divide y enva cada subproblema a los
servidores remotos. Para ello es necesario inicializar un recurso cliente por cada
particin del problema y la comunicacin con cada servidor remoto se establece con
un Socket distinto del lado del cliente.
El hilo principal debe gestionar, por tanto, un vector de Sockets de tamao el
nmero de particiones del problema. Para el ejemplo, se ha definido un parmetro
global NUM_PARTES que, en tiempo de compilacin, proporciona las particiones
deseadas.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 151
Universidad Politcnica de Madrid -UPM
El cdigo completo del hilo principal se detalla a continuacin.
#include <iostream.h>
#include "MyLiveClient.h"
#define NUM_PARTES 1
#define N 9

int main()
{
MyLiveClient client_array[NUM_PARTES];

//Arranque del vector de sockets
for(int i=0; i<NUM_PARTES;i++)
client_array[i].Init("127.0.0.1",12000+i);

Sleep(1000); //Espera mientras arrancan los hilos de com.

//Enviar particiones
for(i=0; i<NUM_PARTES; i++)
client_array[i].EnviarNReinas(N ,i);

//Recoger resultados cada segundo
bool b_terminado;
int sol[NUM_PARTES];
while(1){
Sleep(1000); //1 segundo por peticin
b_terminado=true;
for(int i=0; i<NUM_PARTES; i++){
if( (sol[i]=client_array[i].RecibirResultado())==-1)
b_terminado=false;
}
if(b_terminado) break;
}

//Clculo de la solucin
int total=0;
for( i=0; i<NUM_PARTES; i++)
total+=sol[i];

//Presentacin de la solucin
cout<<"Numero de reinas: "<<total<<endl;

//Cierre de sockets
for( i=0; i<NUM_PARTES; i++)
client_array[i].Close();
return 0;
}
El vector de sockets est compuesto por objetos de la clase MyLiveClient
que se crean e inicializan nada mas comenzar la ejecucin del hilo principal mediante
la funcin heredada Init de la clase Socket. Las instrucciones de arranque son:
MyLiveClient client_array[NUM_PARTES];
for(int i=0; i<NUM_PARTES;i++)
client_array[i].Init("127.0.0.1",12000+i);
La funcin Init requiere dos argumentos, la direccin IP del servidor remoto
y el puerto. Como es lgico, ambos deben coincidir con el servicio de establecimiento
de conexin en el servidor remoto. En el ejemplo, se emplea la direccin genrica IP
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 152
Universidad Politcnica de Madrid -UPM
que existe en toda mquina para poder realizar pruebas en modo local y lo que cambia
para cada Socket cliente es la configuracin del puerto (empezando por el 12.000).
Posteriormente, el hilo principal del cliente realiza, cada segundo, la peticin
del resultado a todos los servidores remotos mediante un bucle del que slo se sale si
todos los servidores han finalizado. Esta funcionalidad se ha implementado de la
manera ms sencilla posible y es manifiestamente mejorable (por ejemplo, no se
distingue entre servidores que han finalizado el cmputo y los que no). El control de
esta operacin se lleva a cabo mediante el flag b_terminado.
Tras la recepcin de las soluciones parciales, el cliente calcula la suma total y
muestra el resultado en pantalla. Finalmente, la funcin miembro Close es invocada
para cada objeto MyLiveClient liberando el recurso Socket en memoria y cerrando
su hilo de ejecucin. Esta funcin, al igual que Init, es heredada de la clase Socket
mediante derivacin. La figura 8 muestra la traza de la sesin del cliente para el
problema de las 8-Reinas con la primera reina en la esquina derecha como nica
particin (en este caso solo hay 4 soluciones). La comunicacin se establece
localmente en el puerto 12000. La explicacin de la sesin es la siguiente:
La lnea Connection indica que se ha establecido comunicacin con el
servidor.
Tras el envo de los datos correctos del problema, el servidor responde
con un mensaje OK que se muestra en pantalla, de acuerdo con el
protocolo implementado en la funcin miembro EnviarNReinas de
MyLiveClient.
Una llegada de una solucin mayor o igual que cero tras la peticin de
resultado (mediante la funcin miembro RecibirResultado)
muestra el mensaje Recibido resultado correcto: 4. El hilo principal
sale entonces del bucle de peticiones.
Se calcula la suma total y se muestra en pantalla (mensaje Nmero de
reinas: 4).
El cierre del socket cliente provoca una advertencia en pantalla de
desconexin.

Figura 8. Traza de la sesin cliente para el problema de las 4-Reinas con la primera
reina en la esquina derecha como nica particin.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 153
Universidad Politcnica de Madrid -UPM
8.5. IMPLEMENTACIN DEL SERVIDOR
En el lado del servidor es donde se encuentra el procedimiento de bsqueda
para el problema de las N-Reinas que llega desde el lado del cliente. El servidor
empleado es sencillo y slo permite conexiones secuenciales de clientes; esto es,
atiende a un cliente cada vez y al terminar queda a la espera de un nuevo cliente. En
este caso slo est previsto un nico Socket cliente por sesin que pasa la
informacin del problema y recoge el resultado. Esto slo es aceptable en el caso de
disponer de todo el tiempo de procesamiento de los servidores remotos conectados,
ya que la tarea pasada tiene una complejidad computacional elevada. En la prctica,
sin embargo, raras veces se dar esta circunstancia y sera ms lgico una arquitectura
que permitiera a los servidores un acceso simultneo a varios clientes.
8.5.1 Comunicacin con el cliente
De forma similar al caso del cliente, se dispone de la clase Socket que
encapsula los servicios de comunicacin. Para la gestin del protocolo y el lanzamiento
del algoritmo de bsqueda se ha desarrollado una clase MyLiveServer que hereda
pblicamente de aqulla. Su fichero de cabecera (.h) es:
class MyLiveServer : public Socket
{
NQueen* m_pNQ;
public:
MyLiveServer(NQueen* pq);
~MyLiveClient();
virtual int OnMsg(char* cad,int length);
};
La clase est lo ms desacoplada posible de la implementacin del
procedimiento de bsqueda; la relacin se establece a travs del dato miembro
privado m_pNQ que es un puntero a la clase NQueen que encapsula el algoritmo
recursivo descrito con anterioridad. La direccin del objeto bsqueda se pasa en el
momento de la llamada al constructor:
MyLiveServer(NQueen* pq);
Para la gestin del protocolo, MyLiveServer dispone de una funcin
miembro OnMsg que es llamada cuando llega cualquier peticin del lado del cliente.
Esta funcin est prevista en la arquitectura heredada y se sobreescribe aqu para
implementar el protocolo. El calificativo virtual indica que se ha previsto polimorfismo
para este servicio. La implementacin de OnMsg es la siguiente:
int MyLiveServer::OnMsg(char* cad,int length)
{
//LLamada a funcin heredada Socket::OnMsg()

//Muestra el mensaje en pantalla
cout<<"Ha llegado el siguiente mensaje: "<<cad<<endl;

//Deserializacin
int N, posq;
char message[100]="";
char nombre[100];
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 154
Universidad Politcnica de Madrid -UPM
sscanf(cad,"%s %d %d",nombre, &N, &posq);

//Protocolo
if(strcmp(nombre,"Nqueens")==0){ //Recepcin de tarea
if((N<=0) || (N>=32) || (posq>N-1) || (posq<0) ){
sprintf(message,"%s","Error en Datos");
if( 0!=SendMsg(message,20) ) return -1;
}else{ //OK
m_pNQ->Set(N);
m_pNQ->SetReinaPrimeraFila(posq);
sprintf(message,"%s","OK");
if( 0!=SendMsg(message,20) ) return -1;
}
}
else if(strcmp(nombre,"Resultado")==0){ //Envo de resultado
sprintf(message,"%d",m_pNQ->GetCount());
if( 0!=SendMsg(message,10) ) return -1;
}
return 1;
}
Al recibir un mensaje nuevo, la funcionalidad heredada llama al servicio OnMsg
que extrae la informacin del mensaje prevista en el protocolo; se asigna a la variable
local nombre la cabecera del mensaje, a la variable local N el tamao del tablero y a
posq la posicin de la reina en la primera fila. Posteriormente se analiza la
informacin recibida. Si la cabecera es Nqueens la peticin se reconoce como un
envo
if(strcmp(nombre,"Nqueens")==0){}
mientras que si es una peticin de resultado se enva la informacin relativa a la
solucin
if(strcmp(nombre,"Resultado")==0{}
En ambos casos se emplea la funcin strcmp que devuelve un cero si la
cadena del argumento primero es exactamente igual que la del segundo.
Detectada la peticin de ejecutar una tarea, se comprueban posibles errores en
los parmetros y se actualizan los valores de la instancia de la clase NQueen que se
encarga del procedimiento de bsqueda. Esta instancia se pas como puntero en el
constructor del objeto MyLiveServer. La actualizacin de los datos se realiza en las
instrucciones:
m_pNQ->Set(N);
m_pNQ->SetReinaPrimeraFila(posq);
Caso de recibir una peticin de resultado, se llama a la funcin miembro
GetCount() de la clase NQueen para obtener dicho valor y se enva como cadena al
cliente:
sprintf(message,"%d",m_pNQ->GetCount());
if( 0!=SendMsg(message,10) )
return -1;
El significado de los parmetros de la funcin SendMsg es el mismo que en el
caso del cliente, por lo que no se aade ningn comentario adicional. Por ltimo,
destacar que si se detecta cualquier error en la transmisin de datos entre cliente y
servidor, OnMsg retorna -1; si no de detecta ningn problema la funcin devuelve 1.
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 155
Universidad Politcnica de Madrid -UPM
8.5.1.1. Procedimiento de bsqueda
El algoritmo recursivo para las N-Reinas ya descrito tiene que modificarse
ligeramente para recibir como parmetro la posicin de la reina en la primera fila.
Para una gestin ordenada del procedimiento de bsqueda se ha definido la clase
NQueen cuyo fichero de cabecera (.h) es el siguiente:
class NQueen
{
public:
NQueen();
NQueen (int N);
virtual ~NQueen();

void Reset();
void Set(int N);
int SetReinaPrimeraFila(int posq);
int GetSol();

int SolveQ();

private:
void FuncRec(int fila, int izq, int abajo, int dcha);
int m_TODOUNOS;
int m_sol;
int m_N;
int m_posq; //0 a (N-1)
};
Los datos miembro de la clase contienen la informacin inicial para el algoritmo
tal y como se present en las secciones anteriores; m_N tiene el valor de N y
m_TODOUNOS es un entero con los N primeros bits a 1 y el resto a cero. A stos se
aade ahora m_posq que contiene la posicin de la reina en la primera fila, punto de
partida de la bsqueda. La funcin miembro privada FuncRec lanza el procedimiento
recursivo de bsqueda y es idntica a la ya descrita e el caso general.
El interfaz de la clase consta de la funcin GetSol, que devuelve el valor
solucin almacenado en m_posq, diversas funciones de inicializacin y el proceso que
gestiona el inicio de la bsqueda SolveQ. El cdigo fuente de SolveQ es:
int NQueen::SolveQ()
{
int izq, dcha, abajo, pos;

m_sol = 0;
m_TODOUNOS = (1 << m_N) - 1;

//Reina en la primera fila
pos =(1<<m_posq);
izq=pos <<1;
dcha=pos >>1;
abajo=pos;

FuncRec(1, izq, abajo, dcha);
return m_sol;
}
Iniciados los parmetros m_sol y m_TODOUNOS, se procede de forma
manual a ubicar la primera reina en la casilla m_posq del borde superior del tablero
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 156
Universidad Politcnica de Madrid -UPM
(fila 0). Para ello, basta con actualizar las estructuras de datos izq, dcha, y
abajo que permiten computar el estado-fila siguiente mediante las operaciones con
bits ya explicadas anteriormente:
pos=(1<<m_posq): Traduce la posicin relativa de la reina a la
mscara con un nico bit a uno correspondiendo a esa posicin. La
operacin de desplazamiento determine que m_posq tome valores
entre 0 (pos = 0000..0012) y N-1.
izq=pos<<1: casilla de la segunda fila atacada por la reina situada
en la casilla m_posq de la primera fila, considerando su movimiento
en la direccin diagonal izquierda y sentido descendente.
dcha=pos>>1: casilla de la segunda fila atacada por la reina situada
en la casilla m_posq de la primera fila considerando su movimiento en
la direccin diagonal derecha y sentido descendente.
abajo=pos: casilla de la segunda fila atacada por la reina situada en la
casilla m_posq de la primera fila considerando su movimiento en
vertical y sentido descendente.
8.5.1.2. Hilo principal del servidor
Una vez que se lanza el hilo de comunicaciones mediante la funcin heredada
server.Init, el hilo principal del servidor entra en un bucle infinito y comprueba
cada segundo si existe una bsqueda que completar. Para ello se ha elegido el valor
del parmetro m_posq como elemento de comprobacin. Si recibe una tarea
correcta, m_posq toma un valor mayor que cero. Si la tarea recibida no es correcta o
ha terminado la bsqueda actual, m_posq toma el valor -1. La funcin main del
servidor es la siguiente:
int main(int argc, char* argv[])
{
NQueen queen;
MyLiveServer server(&queen);
server.Init("127.0.0.1",12000);

while(1)
{
if(queen.GetPos()>=0){ //Comprueba si existe tarea
queen.SolveQ();
cout<<"Solucion Encontrada: "<<queen.GetSol()<<endl;
queen.SetReinaPrimeraFila(-1); //Fin de bsqueda
}
Sleep(1000); //Esperar 1 segundo
}
}
Los parmetros de la funcin server.Init() son la direccin IP y el puerto
donde est escuchando el servidor. Los parmetros que figuran permiten realizar
pruebas con la arquitectura cliente-servidor en una sola mquina, para el cdigo del
cliente descrito en la seccin anterior. El servicio Sleep (Win32) suspende la
ejecucin del proceso que lo ejecuta durante el tiempo que figura como argumento
(medido en milisegundos). Al terminar la bsqueda, la instruccin
Rodrguez-Losada & San Segundo, 2009.
Programacin Avanzada, Concurrente y Distribuida 157
Universidad Politcnica de Madrid -UPM
queen.SetReinaPrimeraFila(-1)
asigna el valor -1 al dato miembro m_posq. De esta manera se consigue que el
hilo principal de ejecucin no entre en el bucle hasta que haya una nueva peticin del
cliente ya que queen.GetPos() devuelve ahora como resultado -1. La figura 9
muestra la traza de la sesin del cliente para el problema de las 8-Reinas con una reina
situada en la esquina derecha del tablero como nica particin. La comunicacin se
establece localmente en el puerto 12000. La explicacin de la sesin es la siguiente:
Las dos primeras lneas de la sesin Comenzando Thread Server y
Server: indican que se ha arrancado un Socket correctamente y que
se encuentra a la espera de la llegada de un mensaje por parte del
cliente. Esto se corresponde con la llamada a la funcin miembro
heredada Init. La aparicin de ambos mensajes pertenece tambin a
la funcionalidad heredada.
Tras la llegada del mensaje con el problema a resolver, se llama a la
funcin miembro OnMsg implementada en MyLiveServer. Esta
funcin llama, a su vez, a la funcin OnMsg miembro de la jerarqua
heredada (mensajes Client connected from: y Connection) y
posteriormente muestra los datos recibidos en pantalla
Al terminar el hilo principal el procedimiento de bsqueda recursivo, se
muestra la solucin en pantalla

Al llegar una peticin de resultado la funcin OnMsg muestra el
mensaje en pantalla (Ha llegado el siguiente mensaje: Resultado).
Al detectarse la desconexin del cliente lanza un mensaje de error y
elimina el Socket de comunicacin abierto para l, quedando a la
espera de la llegada de mensajes de nuevos clientes.

Figura 9. Traza de la sesin del servidor remoto correspondiente a la traza del lado
del cliente mostrada en la Figura 8.

cout<<"Solucion Encontrada: 4"<<queen.GetSol()<<endl

También podría gustarte