09 julio 2008

LetoDB rdd cliente servidor para xHarbour

Desde la soleada Mallorca leemos una muy interesante nota de BielSys (Gabriel Maimó "Biel") acerca de un proyecto Open Source de Alexander Kresin.

http://bielsys.blogspot.com/2008/07/letodb-rdd-cliente-servidor-para.html

Gracias BielSys, muy interesante tu blog.

Multihilos en xHarbour - Parte IV

En las entregas anteriores vimos como ejecutar varios hilos simultáneamente, lanzandolos uno a uno.
En esta oportunidad veremos un tema un poco más avanzado pero muy útil, los avisos o mensajes entre hilos.

Supongamos que estamos lanzando hilos para ejecutar un juego de una carrera.
Creamos un código que irá mostrando el avance de cada participante y un mutex nos permitirá identificar el orden de llegada de cada uno de ellos.

Lo primero que se nos ocurre es lanzarlos tal como vimos anteriormente y nos resulta un código como el del ejemplo 1.

NOTA: Los ejemplos contienen código que permite mostrar una velocidad pareja de ejecución independiente del procesador donde se ejecute, con el fin de facilitar
la visualización de la explicación.
De todas formas, debido a que el tiempo de ejecución es considerablemente alto (del orden de algunos segundos) una cantidad de cores menor a la cantidad de hilos a ejecutar, puede hacer que el ejemplo no siempre muestre el resultado que busco mostrar a pesar de algunas compensaciones de tiempo para evitarlo.




Static mtxGano
Static nGanador := 0, nPuesto := 1

Function Main()
Local nSec := Seconds() + 0.5, n
Local nCount := 0

cls
mtxGano := HB_MutexCreate()
do while Seconds() < nSec
   nCount++
enddo

For n := 1 To 5
   ThreadSleep(200) // Simula hacer otras tareas
   StartThread(@listos_ya(),n,int(nCount/50))
Next
WaitForThreads()

Procedure listos_ya( nHilo, nCount )
Local nMax := nCount * 60, nPos := 1, n

For n := 0 to nMax
   if n % nCount == 0
      DevOut(Replicate("-",nPos-1),,nHilo+5,1)
      DevOut(chr(2),,nHilo+5,nPos)
      nPos++
   endif
Next
DevOut(Replicate("-",nPos-1),,nHilo+5,1)
DevOut(chr(2),,nHilo+5,nPos)

HB_MutexLock(mtxGano)
If nGanador == 0
   nGanador := nHilo
   DevOut("GANADOR",,nHilo+5,65)
   nPuesto ++
Else
   DevOut(str(nPuesto,1,0)+" lugar",,nHilo+5,65)
   nPuesto++
Endif
HB_MutexUnlock(mtxGano)
Return


Ejemplo 1


Este ejemplo nos muestra una clara tendencia a que el jugador 1 gane la carrera.
Lo cual no es muy conveniente si estamos diseñando un juego.

Con lo cual necesitamos sincronizar el arranque.
Quizás lo visto en la entrega de Mutex nos sirva para mejorar el código, así que modificamos el código anterior para que cada hilo tenga un mutex y queden todos a la espera de la señal de arranque y nos queda algo como el ejemplo 2.



Static mtxGano
Static nGanador := 0, nPuesto := 1

Function Main()
Local nSec := Seconds() + 0.5, n
Local nCount := 0, aMutex := {}

cls
mtxGano := HB_MutexCreate()
do while Seconds() < nSec
   nCount++
enddo

For n := 1 To 5
   aadd(aMutex,HB_MutexCreate())
   HB_MutexLock( aMutex[-1] )
   StartThread(@listos_ya(),n,int(nCount/50),aMutex[-1])
Next
For n := 1 To 5
   ThreadSleep(100) // Simula hacer otras tareas
   HB_MutexUnlock(aMutex[n])
Next
WaitForThreads()

Procedure listos_ya( nHilo, nCount, mtxSincro )
Local nMax := nCount * 60, nPos := 1, n
HB_MutexLock(mtxSincro)
HB_MutexUnlock(mtxSincro)
For n := 0 to nMax
   if n % nCount == 0
      DevOut(Replicate("-",nPos-1),,nHilo+5,1)
      DevOut(chr(2),,nHilo+5,nPos)
      nPos++
   endif
Next
DevOut(Replicate("-",nPos-1),,nHilo+5,1)
DevOut(chr(2),,nHilo+5,nPos)

HB_MutexLock(mtxGano)
If nGanador == 0
   nGanador := nHilo
   DevOut("GANADOR",,nHilo+5,65)
   nPuesto ++
Else
   DevOut(str(nPuesto,1,0)+" lugar",,nHilo+5,65)
   nPuesto++
Endif
HB_MutexUnlock(mtxGano)
Return


Ejemplo 2


Si bien nuestros sentidos nos muestran una carrera más pareja, la realidad es que si tenemos una cantidad de cores mayor a la cantidad de hilos de ejecución, pasaría lo mismo que en el ejemplo 1.
Esto es así porque nuestro código es previsible y siempre pone en marcha los hilos en un orden prestablecido.
Cuanto mayor sea la cantidad de hilos, mayor será la diferencia entre la orden de arranque para el primer hilo y la orden de arranque para el último hilo.

Para sincronizar el arranque de uno o varios hilos, tenemos otra utilidad de la entidad Mutex. La pareja suscripción/notificación.
La idea de funcionamiento es que uno o varios hilos se inscriben o suscriben para recibir la orden o notificación de arraque.
Esta orden o notificación puede ser global o individual.
En el ejemplo 3 tenemos el código de la carrera que pondrá a todos los hilos a disposición del sistema operativo para que este los ponga en funcionamiento cuando existan los recursos necesarios para hacerlo.



Static mtxGano
Static nGanador := 0, nPuesto := 1

Function Main()
Local nSec := Seconds() + 0.5, n
Local nCount := 0, mtxSincro := HB_MutexCreate()

cls
mtxGano := HB_MutexCreate()
do while Seconds() < nSec
   nCount++
enddo

For n := 1 To 5
   StartThread(@listos_ya(),n,int(nCount/50),mtxSincro)
Next
ThreadSleep(200) // Simula hacer otras tareas
NotifyAll(mtxSincro)
WaitForThreads()

Procedure listos_ya( nHilo, nCount, mtxSincro )
Local nMax := nCount * 60, nPos := 1, n
Subscribe(mtxSincro)
For n := 0 to nMax
   if n % nCount == 0
      DevOut(Replicate("-",nPos-1),,nHilo+5,1)
      DevOut(chr(2),,nHilo+5,nPos)
      nPos++
   endif
Next
DevOut(Replicate("-",nPos-1),,nHilo+5,1)
DevOut(chr(2),,nHilo+5,nPos)

HB_MutexLock(mtxGano)
If nGanador == 0
   nGanador := nHilo
   DevOut("GANADOR",,nHilo+5,65)
   nPuesto ++
Else
   DevOut(str(nPuesto,1,0)+" lugar",,nHilo+5,65)
   nPuesto++
Endif
HB_MutexUnlock(mtxGano)
Return

Ejemplo 3

Las funciones de notificación y suscripción son las siguientes:

Subscribe( mutex, [timeout], [@event] ) -> info
Inscribe al thread para ser avisado de una notificación.
Si hay notificaciones pendientes, sale inmediatamente, si no, queda bloqueado a la espera de una notificación.
Se puede indicar un tiempo de espera, luego del cual se desbloqueará a pesar de no recibir ninguna notificación.
El thread que notifica, puede enviar una información en el momento de notificar la cual será recibida como valor de retorno de la función de suscripción.
Cuando se especifica un timeout, para saber si hubo o no notificación, se puede especificar un tercer parámetro por referencia, que retornará un valor lógico verdadero si ocurrió la notificación y falso en caso de finalizar por timeout.

SubscribeNow( mutex, [timeout], [@event] ) -> info
El funcionamiento es similar a la función Subscribe(), la única diferencia es que antes de comenzar, borra cualquier notificación pendiente.

Notify( mutex, [info] )
Emite una notificación. Si hay uno o más threads esperando, alguno de ellos es notificado y sale del estado de espera. Los demás threads suscriptos siguen esperando. No hay relación entre el orden de suscripción y el orden de notificación.
Si no hubiera threads en espera, la suscripción queda pendiente en una cola y se van asignando por orden a los threads que se vayan suscribiendo.
Es posible enviar información al thread que espera la suscripción, usando el segundo parámetro de la función.

NotifyAll( mutex, [info] )
Emite una notificación a todos los threads que esten en espera.
Si no hay threads en espera es como si nunca se hubiera ejecutado la función, no se agregan ni quitan notificaciones que esten en cola.
Es posible enviar información al thread que espera la suscripción, usando el segundo parámetro de la función.

Las suscripciones y notificaciones evitan que un proceso tenga que quedar en un loop preguntando cada cierto tiempo por alguna condición.
Si el loop es muy rápido consumirá mucho procesador, si el loop es muy lento se estaría reaccionando tardíamente a un cambio de estado.
Cuando un thread entra en suscripción y no hay notificaciones pendientes, se queda en un estado de espera sin consumir procesador.

Se les ocurren otras posibildades de uso de estas funciones ?
Espero sus comentarios.

30 junio 2008

Multihilos en xHarbour - Parte III

En la anterior entrega, vimos los problemas que ocurren cuando se incrementan o decrementan variables desde 2 o más hilos y no se protegen con un mutex.

Si ejecutan el último ejemplo de la entrega anterior, pero sin ejecutar la función ThreadSleep(10), casi con seguridad tendrán el siguiente error interno: Premature Pointer Release detected
El error ocurre mucho más frecuentemente con procesadores multicore porque verdaderamente están ejecutando el código en paralelo.

¿A qué se debe? Se debe justamente a que los contadores de referencia del puntero que mantiene al Mutex, se desincronizan porque no se actualizan correctamente, tal como se vió en el primer ejemplo de la última entrega.

¿Cómo solucionar este problema? Desde el 28/6/08 el CVS de xHarbour tiene la corrección aplicada y también en la distribución xHarbour 1.0 de PuertoSur tiene el problema corregido en todos los casos.

24 junio 2008

xHarbour 1.0 de PuertoSUR

Muchos lo han pedido y ya está disponible la distribución de xHarbour de PuertoSUR de la versión 1.0.
En estos momentos están disponibles para bajar las siguientes compilaciones:

xHarbour 1.0 compatible con FWH 2.4 para Borland 5.5
xHarbour 1.0 compatible con FWH 2.5/2.6 para Borland 5.5
xHarbour 1.0 compatible con FWH 2.7 y sup para Borland 5.5

Esta distribución como anteriores, se provee en forma de instalador que busca la ubicación del compilador de Borland, lo configura y configura los archivos para compilar y enlazar aplicaciones simples.

La distribución incluye junto con las librerías de xHarbour, algunas librerías de contribuciones y algunas librerías extras, además de algunos arreglos posteriores a la liberación:

rddads.lib compatible con Advantage 8.x
mysql3.lib compatible con MySql 3.x
mysql5.lib compatible con MySql 5.x
hbcomm.lib librería de comunicaciones serie con funciones compatibles en parámetros y funcionalidad a las de FW, para paliar la falta de esta funcionalidad en algunas versiones de FWH.

22 junio 2008

Multihilos en xHarbour - Parte II

El acceso a recursos compartidos

En la entrega anterior vimos como poner en marcha un hilo paralelo de ejecución, dando lugar a la ejecución multihilo.

En esta oportunidad, veremos una de las problemáticas más comunes de la programación multihilo. El acceso a recursos compartidos.

¿Qué es un recurso? Cualquier cosa que se pueda leer, escribir y/o ejecutar. Una variable, un archivo, un alias, una función, un socket, etc.

Cualquier programador de xBase que haya hecho programas multiusuario sabe que para poder modificar un registro, primero debe bloquearlo.
Supongo que la mayoría sabe por qué hay que bloquearlo y no me refiero "a que si no da error de registro no bloqueado".
La razón de bloquear el registro es para que dos o más usuarios no puedan modificar el mismo registro al mismo tiempo.

Esta protección a nivel de recursos en general se realiza usando la entidad Mutex.

HB_MutexCreate() es la función que permite crear una entidad Mutex.
El Valtype() de una variable que contiene un Mutex es P, o sea, un puntero.

Un Mutex sirve para varias cosas, pero en esta entrega veremos el uso más común, que es como semáforo.

HB_MutexLock() es la función que marca el Mutex como bloqueado por el thread que llamó a la función.
Esta función retorna solamente cuando obtuvo el bloqueo del Mutex.

Mientras un hilo bloquee un Mutex y cualquier otro hilo que intente bloquear el mismo Mutex, quedará bloqueado y en estado de espera a que se desbloquee el Mutex.
Cuando el Mutex esté disponible, sólo uno de los hilos obtendrá acceso al Mutex, mientras que los demás seguirán esperando.

HB_MutexUnlock() es la función que desbloquea el Mutex y lo deja disponible para que otro hilo lo pueda bloquear.

Además de estas dos funciones, que son las más usadas, existen otras.

HB_MutextryLock() es la función intenta bloquear el Mutex y si lo logra, retorna .T. o un .F. en caso contrario.

HB_MutexTimeOutLock() es la función que intenta bloquear el Mutex durante un tiempo que se indica como segundo parámetro. Retorna .T. si logra el bloqueo y .F. en caso contrario.

El ejemplo más trivial para usar un Mutex es como protección en la actualización de un contador. Más de uno se preguntará por qué hay que bloquear si solamente queremos incrementar?
El tema es que el proceso de incrementar requiere de 3 pasos. Leer, incrementar, grabar. Entonces, podría suceder que dos procesos leyeran simultaneamente y luego al grabar, el contador solamente se habría incrementado una vez en lugar de 2 veces.

Veamos un ejemplo que podamos probar para ver más claramente el asunto:


Static lFinalizar := .f.
Static nGlobalCounter := 0
Static aResult[2]

Function Main()
cls
StartThread(@mostrar())
StartThread(@enparalelo(),1)
StartThread(@enparalelo(),2)
DevOut("Presione una tecla para finalizar o espere 5 segundos",,1,0)
inkey(5)
lFinalizar := .t.
WaitForThreads()
cls
@2,0 say "Thread 1 - LocalCounter="+str(aResult[1])
@3,0 say "Thread 2 - LocalCounter="+str(aResult[2])
@4,0 say "Total "+str(aResult[1]+aResult[2])
@6,0 say " GlobalCounter="+str(nGlobalCounter)
@8,0 say "Diferencia "+str(aResult[1]+aResult[2]-nGlobalCounter)


Procedure enparalelo( nId )
Local nLocalCounter := 0
Local nLinea := GetThreadId()
do while !lFinalizar
  nLocalCounter++
  nGlobalCounter++
  ThreadSleep(10)
enddo
aResult[nId] := nLocalCounter
Return

Procedure mostrar()
Local nStatus := 1, cStatus := "|/-\"
Local nLinea := GetThreadId()
do while !lFinalizar
  DevOut(nGlobalCounter,,nLinea,10)
  ThreadSleep(500)
enddo
Return


La diferencia debería ser 0 (cero), pero no siempre es así. Esto muestra por qué es necesario proteger la variable al modificar su valor.
El código correcto es:


Static lFinalizar := .f.
Static nGlobalCounter := 0
Static aResult[2]

Function Main()
Local mtxCounter
cls

mtxCounter := HB_MutexCreate()

StartThread(@mostrar(),mtxCounter)
StartThread(@enparalelo(),1,mtxCounter)
StartThread(@enparalelo(),2,mtxCounter)
DevOut("Presione una tecla para finalizar o espere 5 segundos",,1,0)
inkey(5)
lFinalizar := .t.
WaitForThreads()
cls
@2,0 say "Thread 1 - LocalCounter="+str(aResult[1])
@3,0 say "Thread 2 - LocalCounter="+str(aResult[2])
@4,0 say "Total "+str(aResult[1]+aResult[2])
@6,0 say " GlobalCounter="+str(nGlobalCounter)
@8,0 say "Diferencia "+str(aResult[1]+aResult[2]-nGlobalCounter)


Procedure enparalelo( nId, mtxCounter )
Local nLocalCounter := 0
Local nLinea := GetThreadId()
do while !lFinalizar
  nLocalCounter++
  HB_MutexLock(mtxCounter)
  nGlobalCounter++
  HB_MutexUnlock(mtxCounter)
  ThreadSleep(10)
enddo
aResult[nId] := nLocalCounter
Return

Procedure mostrar(mtxCounter)
Local nStatus := 1, cStatus := "|/-\"
Local nLinea := GetThreadId()
do while !lFinalizar
  DevOut(nGlobalCounter,,nLinea,10)
  ThreadSleep(500)
enddo
Return


Un ejemplo más complejo es el de una clase que implemente una pila o cola. Es totalmente necesario que mientras un hilo intenta agregar o quitar un elemento, otro hilo no pueda ni agregar ni quitar elementos. Por lo tanto la actualización del array que mantiene la pila o cola, debe estar protegido por un mutex.

En la próxima entrega, más usos de la entidad Mutex.

11 junio 2008

Multihilos en xHarbour - como compilar

Como compilar los ejemplos:

Los ejemplos que encontrarán en estas notas son sencillos y se podrán compilar con el archivo \xharbour\tests\bldtest.bat de la siguiente forma:

c:\xharbour\tests> bldtest.bat /mt test1.prg

Por default el archivo .bat está preparado para usar el compilador de Borland.
Para usar otro compilador será necesario establecer algunas variables de entorno.

También pueden crear un archivo xcomp.bat con las siguientes lineas y que permite compilar con el compilador de Borland.


//------------------------------------------------
@harbour %1 /n %2 %3 %4 %5 %6 %7 %8 %9
@bcc32 -tWM -I\xharbour\include -L\xharbour\lib -5 -O2 -d %1.c debug.lib vmmt.lib rtlmt.lib gtwin.lib lang.lib rddmt.lib macromt.lib ppmt.lib dbfntxmt.lib common.lib codepage.lib pcrepos.lib sixcdxmt.lib dbffptmt.lib hbsixmt.lib

//------------------------------------------------


Hay que tener en cuenta que las líneas del archivo son solamente 2, las que comienzan con el caracter @.

Luego de construir el archivo .bat, podrán compilar de la siguiente forma:

x:\xharbour\tests> xcomp test1

02 junio 2008

Multihilos en xHarbour - Parte I

Como toda cosa nueva, como todo cambio, siempre cuesta y siempre genera incertidumbre.
Crear programas multihilos es un cambio respecto de la programación habitual, pero la verdad, es que la teoria de los multihilos es muy simple.

Usar multihilos es tan simple de usar como el SQL.
El SQL tiene 7 comandos.
Para usar multihilos es suficiente con conocer cerca de 10 instrucciones.

¿Tan simple es?
Bueno, la respuesta es un poco más compleja que un Si o un No.

Si nos guiamos por la cantidad de instrucciones/funciones a usar, y por la sencilla explicación de lo que hace cada una de ellas, contestaría que Si.

Pero. ¿Es posible medir la simplicidad de la programación por la cantidad de instrucciones que posee?

Yo, como muchos, creo que la programación es un arte como lo es la pintura. Ahora, si es tan simple combinar colores, ¿por qué yo no soy un pintor? ¿Por qué no pagarían ni 10 centavos por una "obra" mía?

Nuestra diseñadora gráfica me contestaría que no es suficiente con saber combinar. También hay que saber cómo combinar los colores (y tener buen gusto, algo que no es mi fuerte).

Ahh, así que no es suficiente con conocer 5, 7 o 10 instrucciones, también es necesario saber cómo y cuándo usarlas y cada una de sus opciones.

Al igual que los corredores profesionales de autos, no solo saben manejar muy bien, también conocen de mecánica y de aerodinámica, y además, horas y horas de prácticas para conocer los límites del auto.

Un programador debe conocer las instrucciones, cuándo aplicarlas y cómo combinarlas, pero en la alta competencia no es suficiente.
Además debe conocer pormenores del motor de ejecución y pormenores del sistema operativo.
Eso se aplica tanto al SQL como a los multihilos.

Quizás esta introducción no es muy amigable, pero es la realidad. La teoría es sencilla, pero la práctica algunas veces se complica, pero con práctica y buenas normas de programación para multihilos, se podrá salir adelante.

Pero mejor, vayamos a los multihilos que es el tema.

Primera ejecución de una función en paralelo

La ejecución de una rutina en paralelo con otra es muy sencilla. Sólo es suficiente con poner StartThread( @enparalelo() ) para que la función enparalelo() comience a ejecutarse.

//-----------------------------------------------
Static lFinalizar := .f.

Function Main()
cls
StartThread(@enparalelo())
DevOut("Presione una tecla para finalizar",,1,0)
inkey(0)

Procedure enparalelo()
Local nStatus := 1, cStatus := "|/-\"
Local nLinea := GetThreadId()
do while !lFinalizar
  DevOut(cStatus[nStatus],,nLinea,10)
  if ++nStatus > 3
    nStatus := 1
  endif
  ThreadSleep(500)
enddo
Return
//-----------------------------------------------


De la misma forma, si ejecutamos 2 veces StartThread(), estaremos haciendo que se ejecuten 2 hilos.


Function Main()
cls
StartThread(@enparalelo())
StartThread(@enparalelo())
DevOut("Presione una tecla para finalizar",,1,0)
inkey(0)


Es posible pasar parámetros al thread? Si.

StartThread(@enparalelo(),xParam1,xParam2,...)


Resumiendo, estas son las opciones de ejecución para lanzar threads.

// usando puntero a la funcion (puede ser publica o estatica)
StartThread(@funcion(),)

// usando el nombre de la funcion (debe ser publica)
StartThread("funcion",)

// usando el nombre del metodo
StartThread(objeto,"metodo",)

// usando el puntero al metodo
StartThread(objeto,__ObjPtr(objeto,"metodo"),)

// usando un codeblock
StartThread(codeblock,)

Antes de finalizar esta primera entrega, voy a dar una lista de las funciones que se usan en multihilos y de las cuales veremos sus usos, comportamientos, casos de uso y advertencias de uso.

HB_MutexCreate
HB_MutexLock
HB_MutexUnlock
HB_MutexTryLock
HB_MutexTimeOutLock

Subscribe
SubscribeNow
Notify
NotifyAll

StopThread
JoinThread
KillThread
WaitForAllThreads
GetThreadId

Hasta la próxima