09 julio 2008

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.

No hay comentarios.: