06 diciembre 2008

Multihilos en xHarbour - Parte VII

Consideraciones acerca del rendimiento en multihilos.

Comentaba en una de las primeras entregas, que para acceder a recursos compartidos, había que usar un mutex. También comenté que el mutex genera una gran penalidad en la ejecución porque en algunos casos tiene un costo similar o superior al tiempo de ejecución del código que se quiere resguardar.
Por esta razón, lo mejor es trabajar sin mutex, claro, siempre que sea posible.
Pero teniendo en mente esto, en algunos casos podríamos idear un código que pueda no usar mutex.
Hay algunos casos en donde no es necesario poner un mutex para actualizar las variables, con la ventaja de ganar imporante cantidad de ciclos de reloj.

Uno de estos casos es cuando una variable no compleja es modificada desde un solo thread y los demas threads solamente consultan.


--------
Static lSalir := .f.
Func Main()
? "Presione una tecla"
StartThread(@Hilo())
while !lSalir
  inkey(0)
enddo
lSalir := .t.
Return nil

Proc Hilo()
Local nPaso:=1,cPasos:="-/|\"
while !lSalir // Sin mutex
  @1,70 say cPasos[nPaso]
  if ++nPaso > 4
    nPaso := 1
  endif
  HB_ThreadSleep(100)
enddo
Return
--------------


Otro de los casos es cuando las modificaciones no tienen dependencias entre si.


Func Main()
Local aArray[1000]
Afill(aArray,10)
StartThread(@Process(), aArray,;
  1, Int(Len(aArray)/2))
StartThread(@Process(), aArray,;
  Int(Len(aArray)/2)+1, Len(aArray)-Int(Len(aArray)/2))

Proc Process( aArray, nStart. nCount )
do while nCount > 0
  aArray[ nStart ] *= 1.10
  nStart ++
  nCount--
enddo
Return


Sin embargo, a pesar de que lógicamente la ejecución esta separada, internamente tienen puntos en comun. Esto es así porque los tipos de datos complejos tienen un contador de referencias que hace que los hilos deban turnarse para modificar el contador.
El siguiente código es mas óptimo porque cambia el contador menor cantidad de veces.


Proc Process( aArray, nStart, nCount )
Local n
For each n in aArray From nStart To nCount
  n *= 1.10
Next
Return n


Cuando un código que se ejecuta en 2 o mas hilos bloquean alternadamente un recurso, se dice que es un codigo ping-pong.
El bloqueo puede ser implicito (internos a nivel procesador) o explicito (usando funciones de bloqueo)
El problema del código ping-pong es que funciona tanto o mas lento que el mismo código en forma serial.

Otras formas de código ping-pong.
Supongamos que necesitamos obtener un promedio de los valores guardados en un array.
La forma serial seria acumular la suma y mantener un contador.


Func Process( aArray, nStart, nCount )
Local n
For each n in aArray from nStart to nCount
  s_nSuma += n
  s_nCount ++
Next
Return


Para hacer la misma tarea en forma paralela podríamos proteger los acumuladores con un mutex.


Func Process( aArray, nStart, nCount )
Local n
For each n in aArray from nStart to nCount
  hb_mutexLock( mtx )
  s_nSuma += n
  s_nCount ++
  hb_mutexUnlock( mtx )
Next
Return


Sin embargo este es el tipico ejemplo de un codigo ping-pong. Para evitar este problema el mejor código es:


Func Process( aArray, nStart, nCount )
Local n, nSuma:=0, nCount:=0
For each n in aArray from nStart to nCount
  nSuma += n
  nCount ++
  Next
hb_mutexLock( mtx )
s_nSuma += nSuma
s_nCount += nCount
hb_mutexUnlock( mtx )
Return


De esta forma se bloquea el mutex una sola vez y se evita el efecto ping-pong.

Efecto ping-pong relacionado con xHarbour.
Todos los datos complejos tienen un contador de referencias que se incrementa y decrementa con cada uso. Si el dato es compartido y usado simultáneamente por varios hilos, produce que los hilos deban turnarse para cambiar el contador y es eso lo que produce el efecto ping-pong. Por lo tanto, es conveniente minimizar los datos complejos compartidos.
Recordemos que los datos complejos son :
Array, objetos, codeblock, hash, punteros, strings.

En lugar de hacer:

Procedure Paralelo( aArray, nStart, nCount, bBlock )
Local nPos :=Ascan(aArray,bBlock,nStart,nCount)


Es mejor hacer:

Procedure Paralelo( aArray, nStart, nCount, cBlock)
Local nPos, bBlock
bBlock:=&(cBlock)
nPos:=Ascan(aArray,bBlock,nStart,nCount)


En definitiva, le código para multihilos implica una cuidadosa planificación para no caer en códigos ineficientes que tengan la misma e incluso peor performance que la ejecución en serie, con el consiguiente desperdicio de recursos.

04 septiembre 2008

Multihilos en xHarbour - Parte VI

En estas primeras entregas sobre los multihilos de xHarbour, he hablado de funciones para crear un nuevo hilo, para sincronizar ejecuciones y para sincronizar accesos a recursos.

Sin embargo, puede suceder que tengamos un hilo al que necesitamos finalizar su ejecución porque no se comporta como esperábamos.
Para eso existen 2 funciones. Stopthread y Killthread.
Además, junto con Stopthread, existe una función para sincronizar la finalización del thread con el thread que lo está finalizando.

Stopthread

Sirve para detener la ejecución de un thread de forma amigable, marcando al thread para que se cierre cuando llegue a uno de los puntos de control.
Los puntos de interrupción son:
  • antes y después de un acceso a disco.
  • antes y después de acceso a TCP/IP.
  • antes y después de llamadas a bloqueos.
  • cada vez que finaliza la ejecución de una función PRG, codebloc o macro.
  • cada 5000 ejecuciones de PCODE dentro de un mismo PRG.
Un thread no puede ser detenido amigablemente cuando esta:
  • esperando por un mutex.
  • esperando por conexión o datos de TCP/IP.
  • mientras ejecuta funciones de C o de PRG compilado en C nativo, que quedan en un loop infinito o muy largo.

Killthread

En general hay que evitar el uso de esta función, siempre hay que tratar de finalizar los threads amigablemente.
Windows en su documentación indica que se evite su uso porque puede dejar incluso registros del kernel en estado incorrecto si al matar el thread el proceso esta ejecutando una función en modo kernel.
También la documentación dice que si el thread tiene un bloqueo, este podría no ser desbloqueado o no ser avisados otros threads de que el mutex esta disponible.

Nos encontramos ante la disyuntiva de matar o no a un thread que no responde.
Si no lo matamos, no podremos finalizar la ejecución.
Si lo matamos, quizás tampoco podamos finalizarla.

Por este motivo, la función Killthread inicialmente marca al thread que se le indica para que se cierre amigablemente, si ya esta marcado entonces sí se lo mata.

En conclusión. Hay que tratar por todos los medios de detener los threads amigablemente, para esto es necesario diseñar el código pensando en incluir puntos de interrupción o de control, si fuera necesario, para ayudar a finalizar el/los threads por las buenas.

Jointhread

Esta función se complementa con Stopthread permitiendo sincronizar la finalización del thread.
Se usa para esperar a que el thread indicado termine su ejecución.

Ejemplo:


Procedure Main()
Local thThread

cls
@8,10 say "Presione una tecla para parar el hilo"
thThread := StartThread( @Work() )
StartThread( @Join(), thThread )

inkey(0)
StopThread( thThread )
WaitForThreads()
Return

Procedure Work()
Local aStat := "\|/-", nStat := 1
do while .t.
  DevOut( "Trabajando ... "+aStat[nStat],,10,10)
  if ++nStat > 4
    nStat := 1
  endif
enddo
return

Procedure Join( thThread )
DevOut("Esperando que finalize el thread de trabajo",,12,10)
JoinThread( thThread )
DevOut("El thread de trabajo ha finalizado correctamente",,12,10)
return

01 septiembre 2008

Sobre la Reunión del 30 de Agosto 2008

En primer lugar agradecer el aliento y los comentarios recibidos. Tambien la presencia de todos los que se pudieron acercarce, en especial a aquellos que llegaron de lejos o de países vecinos.

Tuvimos un ambiente de total camaradería y literaltemte nos encerramos en la reunión, a tal punto que ni siquiera fuimos a almorzar (de 9 a 19). Es que había mucho interes.

Respecto de los temas tratados, en orden de presentación fueron:
  • Paradigmas: una charla de Gustavo Valentin acerca de los paradigmas y como afectan la forma en que programamos.
  • Objetos: Walter explicó los conceptos básicos de objetos partiendo de un mini programa que no los tenía y llevandolo en una serie de modificaciones a uno que sí los tenía.
  • Impresoras Fiscales: Walter y Gustavo contaron que es y como funciona una impresora fiscal, la forma de comunicación y los programas que ellos utilizan. Luego se generó un debate entre los que estan necesitados de implementar impresora fiscal en sus programas y, con la donación de código por parte de softmagic quedaron en realizar una clase tanto para epson como para hasar.
  • Hash: si bien no estaba en la agenda de la reunión surgió espontaneamente. Walter nos explicó como inicializar un hash y luego se generó un debate sobre los diferente lugares donde utilizarlo. Fué la frutilla del postre.
  • Se habló de hacer la reunión mas seguido.

08 agosto 2008

Multihilos en xHarbour - Parte V

Funciones CRITICAL
Una funcion marcada como critical tiene un mutex interno que automáticamente se activa y desactiva al ingresar y salir de dicha función.
La ventaja es que no hay que crear un mutex para hacer esta tarea, lo cual reduce y simplifica el código.
La desventaja es que bloquea a una sola función.

Pueden existir muchas funciones CRITICAL ejecutandose en paralelo, pero un sólo thread por vez ejecutara cada una de ellas.

Forma de uso:

CRITICAL FUNCTION SoloUno()
CRITICAL PROCEDURE SoloUno()
CRITICAL STATIC FUNCTION SoloUno()
CRITICAL STATIC PROCEDURE SoloUno()



// No superar 32768 porque HB_Random() solo genera
// hasta 32768 numeros diferentes

#define MAXVALUE 30000

//#define PARALELO

Static nCount := 0, lWork := .t.
Static nTime, lEnd := .f.

Function Main()
Local aValues := Array(MAXVALUE)
cls
StartThread(@generate(),aValues)
StartThread(@generate(),aValues)
StartThread(@process())
DevOut("Presione una tecla para finalizar...",,5,0)
do while lEnd .and. inkey(0.1) == 0
  lWork := .f.
enddo
WaitForThreads()
If lEnd
  Devout("Tiempo de trabajo="+str(nTime),,8,0)
Endif
Return nil

Procedure Generate(aValues)
Local nValue, lRet
Do While lWork
  nValue := Int(HB_Random(0,MAXVALUE))+1
#ifdef PARALELO
  If aValues[nValue] == nil
#endif
    lRet := AddUnique(aValues,nValue)
    If lRet
      Exit
    Endif
#ifdef PARALELO
  ElseIf nCount == MAXVALUE
    Exit
  Endif
#endif
Enddo
Return

Critical Function AddUnique(aValues,nValue)
If nCount == 0
  nTime := Seconds()
Endif
If aValues[nValue] == nil
  ++nCount
  aValues[nValue] := nCount
Endif
If nCount == MAXVALUE
  If !lEnd
    nTime := Seconds()-nTime
  Endif
  lEnd := .t.
  Return .t.
Endif
Return .f.

Procedure Process(nThread)
Local cPaso := "\|/-", nPaso := 1
do while nCount < MAXVALUE .and. lWork
  DevOut(nCount,,7,0)
  DevOut(cPaso[nPaso],,7,15)
  If ++nPaso > 4
    nPaso := 1
  Endif
  ThreadSleep(100)
enddo
DevOut(nCount,,7,0)
Return


El código del ejemplo tiene 2 modos de funcionamiento.
El modo 1 es tal como está escrito.
El modo 2 es "des-comentando" la línea #define PARALELO

En el modo 1, realiza lo siguiente:
Unos hilos en paralelo, generan unos números aleatorios y luego llaman a una función que controla si dicho número está cargado en un array.
Si no está, lo carga. Si ya existe, lo descarta.
Cuando el array está completo, cambia el estado de la bandera lEnd para que finalice el programa.

En el modo 2, realiza lo siguiente:
El proceso es similar, solo que cada hilo controla por su lado, si el número generado ya existe. Si no existe, llama a la función para que lo cargue. Si ya existe, lo descarta y vuelve a calcular otro número.
Puede darse el caso que el hilo 1 calcula un número y detecta que el número no existe y llama a la función, pero el hilo 2 pudo haber calculado el mismo número y haberse anticipado en la llamada a la función AddUnique, pero esto no es problema ya que cuando el hilo 1 llame a la función, el número será descartado sin problemas como en el modo 1.





Muestra el uso del procesador en modo 1.





Muestra el uso del procesador en modo 2.



Si en un procesador multicore, se nota que el modo 2 se ejecuta más rápidamente, ya que los 2 hilos pueden estar en forma paralela averiguando si el número obtenido existe o no en el array, y solo en el caso de no existir llamará a la función AddUnique para que lo agregue.
Si bien la ejecución es bastante rápida, podemos concluir que en el modo 2, inicialmente el uso de los 2 cores (uno por cada hilo) es de un 60%, pero a medida que se va llenando el array, el uso tiende al 100%.
Cómo es esto?
El hilo 1, calcula un número, lo busca en el array y no existe, asi que llama a AddUnique. Por su parte, el hilo 2, calcula otro número, lo busca en el array y tampoco existe, asi que tambien llama a AddUnique.
El tema es que los hilos deben turnarse para ejecutar AddUnique, ya que es una función CRITICAL.
Pero a medida que crece la densidad de uso del array, los números aleatorios comienzan a duplicarse y la tasa de encontrados aumenta y por consiguiente, aumenta los reintentos (volver a calcular el número aleatorio). Cada hilo por su cuenta reintenta varias veces la búsqueda hasta encontrar un número que no existe.
Pero debido a que los hilos no se están turnando, hacen más trabajo en menos tiempo y permite terminar la tarea antes.

En el modo 1, los hilos siempre se están turnando, con lo cual el uso del procesador permanece constante en un 60% durante todo el proceso. Al hacer menos trabajo en el mismo tiempo, la tarea demora más en terminarse.

Con estas explicaciones me estoy adelantando a temas que voy a tocar en próximas entregas, pero bien viene la reseña.
Al trabajar con mutex es importante saber cuando y cuanto proteger para evitar ser "sobreprotectores" y tener un código que funcione lentamente.

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

13 abril 2008

Monopolio o No-Monopolio, esa es la cuestión

Comencé a pensar en este artículo hace un tiempo, pero ahora tengo la necesidad de terminar de escribirlo.

El movimiento del software libre ya tiene unos cuantos años y una gran cantidad de adeptos y simpatizantes.

Software libre vs Software propietario, es la cuestión?
Hoy muchos artículos en revistas y blogs discuten acerca de dos supuestas posiciones antagónicas. Sofware libre vs Software propietario.
Los partidarios del software libre enarbolan fuertemente la bandera de la libertad, incluso a costa de coartar algunas libertades.
Por otra parte los creadores de software propietario defienden sus creaciones y sus empresas y detrás de ellas su esfuerzo (pocas veces las grandes empresas se meten en estas discusiones).

Pero, llegan a alguna conclusión válida estas discusiones? o son peleas filosóficas?
Yo creo que son discusiones sin sentido, simplemente porque ambos bandos tienen una parte de la verdad y porque ambos pueden convivir.
Entonces, quién se beneficia? Los beneficiados son unos pocos y quizás hasta les interese estas discusiones, porque hace quitar la atención de la verdadera discusión.

La verdadera discusión debería ser: hasta cuando los gobiernos permitirán y alimentarán los monopolios?
Hasta cuando nosotros mismos alimentaremos y permitiremos los monopolios?

Sabemos que los monopolios son malos, pero como estamos siendo afectados?

Cómo nos afectan los monopolios ?
Los monopolios afectan todo lo que los rodea y cuando se trata de algo muy metido en la sociedad, afecta a toda la sociedad.
Hoy en día Microsoft está afectando a toda la sociedad, y si no se lo controla, Google podría llegar a hacer algo similar con internet, lo cual podría ser peor ya que internet es multiplataforma.

Un monopolio es como un virus y muy comparable con el virus del HIV.
Lo primero que afecta un monopolio es la competencia y busca por todos los medios combatir cualquier intento de surgimiento de una competencia.

Inicialmente no afecta a los consumidores y hasta parece bueno o tonto.
(los CD de Windows no tienen protección anticopia y hasta se consiguen los generadores de claves para instalar el producto)

Un monopolio, puede no poner precios altos y hasta regalar productos, pero el fin es simplemente matar la competencia (Netscape vs Explorer)

Cuando tiene a los consumidores totalmente dependientes y sin amenazas que combatir, comienza el proceso de ahogamiento de los mismos consumidores tal como una boa constrictora lo hace con su presa.

El proceso de ahogamiento de un monopolio
El proceso comienza con los canales de venta y los contratos abusivos que limitan la instalación de programas en computadoras nuevas.
O los contratos con otras empresas para impedir el desarrollo de productos.
(que pasó con Kylix y Corel Linux).

Paralelamente comienza a verse el principio de tratar al usuario de inepto, incluido al administrador del sistema.
Claro, crearon un producto que hasta un dummy podría usar y ahora creen que todos son unos dummys a los que hay que decirles que pueden y que no pueden hacer con el sistema.
(Ej: Esta carpeta contiene archivos que mantienen el sistema en correcto funcionamiento.
No debería modificar su contenido)

El siguiente nivel de estrangulamiento es dictaminar como deben crear sus productos las compañías de hardware para permitirles el soporte en la siguiente versión del sistema operativo. Incluyendo cláusulas de no publicación de cierta información técnica del producto y de no publicación de drivers para soporte de un sistema operativo vigente.
( Cómo puede ser que existan:
  • Notebooks con sistemas de disco no estándares sin drivers para Windows XP.
  • Placas de video de última generación con características que sólo pueden usarse bajo Windows Vista y si se quiere utilizar con otro sistema operativo, esas características no estarán disponibles. Un nuevo modelo de drivers hace necesario tener un driver para cada modelo de tarjeta lo cual aumenta el costo de desarrollo y testeo, y eso lo pagan tanto los usuarios de Windows Vista como los usuarios de otros sistemas operativos.
    Las nuevas características sólo se podrán usar bajo Windows Vista y siempre y cuando éste se los permita.
    Y para coartar aún más las libertades del usuario, al momento de utilizar ciertas características avanzadas, algunos dispositivos podrán ser desactivados bajo criterio y decisión del sistema operativo.
    No era mejor que el hardware se comunicara con el hardware y el proceso de autorización y autenticación fuera independiente del sistema operativo? No claro, porque esto abriría la competencia.
)

A estas alturas tenemos:
  • usuarios dependientes de sus computadoras pero que no pueden utilizar muchas de sus características.
  • Tenemos usuarios que compran notebooks nuevas con el logo de Windows Vista Capable sin embargo no son capaces de mostrar nada mejor que un Windows XP y funcionar peor que con un Windows XP y sin posibilidad de instalar Windows XP bajo la amenaza de perder la garantía !!!!
Como puede existir una cláusula de esta naturaleza? Como puede el fabricante del hardware decirme que tal programa no puedo instalarlo en esa computadora?
Alguien compraría un auto con la cláusula "Si bien este auto posee 4 asientos, el cargar más de 2 personas invalida la garantía sobre el motor".

Los afectados
En algunas notebooks modernas, no impiden que instale Linux, impiden que instale Windows XP al no proveer los drivers. (Al momento de escribir estas líneas Windows XP es un producto vigente y con soporte de la misma forma que Windows Vsita)
Y quién otra que la misma Microsoft puede estar interesada en que no puedas usar Windows XP?
Y quién es el único afectado? el usuario.

Otros afectados son los desarrolladores.
Microsoft en su sed de abarcar todo, intenta establecer que el único lenguaje válido para hacer programas para Windows Vista y posteriores sea algún lenguaje podado-recortado-adaptado para funcionar bajo la plataforma .NET

Y como los viejos desarrolladores son un poco reacios, hay que apuntar a los futuros desarrolladores.
Pero para que estos futuros desarrolladores no usen más que .NET, se instalan nuevos centros de cómputos con servers y máquinas de última generación en cada facultad en donde se enseñe programación, para que se use .NET, con profesores adeptos a .NET y nada de profesores que enseñen a pensar, solo profesores que enseñen .NET.
Pero TAN es malo .NET? no, .NET no es malo en si mismo, es mala la práctica monopólica de intentar imponer .NET como único sistema para utilizar las características de un sistema operativo.
Y esto sólo es posible gracias al monopolio.

Y esto no termina acá...
Y como si todo esto fuera poco y para seguir mostrando otra vuelta más de la boa constrictora.
En estos días, contra todo pronóstico coherente y con abrumadoras y nauseabundas prácticas de
corrupción y monopolio, Microsoft logró aprobar un estandar ISO para intercambio de documentos cuando ya existe un estandar ISO para esa función.
ATENCION: Los siguientes link pueden contener detalles de prácticas monopólicas no apto para cardíacos.
http://barrapunto.com/article.pl?sid=08/04/02/1047257
http://www.openxml.info/index.php?option=com_content&task=view&id=27&Itemid=7

Moraleja
Las empresas están para ganar plata, pero cuando la forma de ganar plata se realiza por prácticas anticompetitivas los gobiernos y/o la justicia deberían poner coto a esas prácticas.
Cuando esto no ocurre, los afectados somos nosotros mismos.

04 marzo 2008

Advantage Database Server 9

Texto de un mail recibido desde Abox:

SYBASE iANYWHERE Anuncia

ADVANTAGE DATABASE SERVER 9


Advantage lanza al mercado su nueva versión 9. Esta marcará el final del ciclo de vida de las versiones antiguas de Advantage data Server. A partir del 30 de marzo de 2008, las versiones 5,6, y 7.x no se les dará soporte técnico. Para continuar con el apoyo los usuarios de la versión 7.x de Advantage pueden comprar las actualizaciones de las versiones más recientes (versiones admitidas, Advantage 8 o 9.)

Por un período de transición de 6 meses después del lanzamiento de Advantage 9, los clientes pueden comprar nuevas licencias de Advantage 9 y también puede recibir un duplicado de Advantage 8 para ser utilizados de conformidad con los términos del acuerdo a la licencia de software. Este período de transición se ha diseñado para permitir que los usuarios actuales puedan integrar plenamente la versión 9 sin interrumpir los ciclos de liberación los cuales se hubieran planeado previamente. Después de este período de transición, los clientes todavía pueden comprar las versiones antiguas de Advantage, pero no tendrán derecho a nuevas versiones sin la compra de la actualización adecuada. Actualizaciones de estas licencias se pueden comprar en cualquier momento y en los precios vigentes.

Que hay de nuevo en la versión 9 de Advantage
  • Mejoras en el soporte de FoxPro: Advantage 9 soportara la versión de Visual FoxPro 9.
  • 64-bit en servidores Windows y Linux Advantage Database Server ha sido adaptado para funcionar como aplicación nativa de 64-bit en la versiones de x64 de Windows y Linux.
  • SQL Debugger: Un SQL debugger ha sido añadido a la versión 9 de Advantage Data Architect, Permitiendo a los desarrolladores depurar scripts SQL, procedimientos almacenados, disparadores, y funciones definidas por el usuario..
  • Notificaciones de Eventos : las notificaciones de eventos es un mecanismo que permite al servidor notificar proactivamente a los clientes de que un evento que les interesa se ha producido.
  • New Default User Groups: admin, public, debug, backup
  • TDataSet Descendant Version Switching Utility
  • Dynamic Server Configuration
  • Optimized Replication Functionality and Enhancements


Para ver información mas detallada de las novedades de la versión 9, pueden ir a la zona de desarrolladores de Advantage.

Download

La versión de Advantage 9 esta disponible para descarga.

06 febrero 2008

Comentarios sobre software de gestión

Un link de un blog con temas interesantes para los que estamos en el negocio de desarrollar software de gestión:

http://www.evaluandoerp.blogspot.com/

La definición en Wikipedia: http://es.wikipedia.org/wiki/Planificación_de_Recursos_Empresariales.

21 enero 2008

Sun compra MySQL y Oracle Bea Systems

Luego del primer impacto de la crisis inmobiliaria en EEUU (con sus ecos en todo el mundo) que dejó a todos estáticos parece que los capitales finalmente se van a inclinar por las inversiones tecnológicas.

La semana pasada se conocieron dos compras importantes por parte de Sun, quien compró MySQL AB, la base de datos de código abierto mas difundida del planeta y Oracle quien compró Bea Systems, un fabricante de software middleware (programas que corren encima de la base de datos, principalmente relacionados con Java).