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.

2 comentarios:

Unknown dijo...

Seguí así muy bueno y muy didáctico. Gracias por tus excelentes aportes.

Marcos dijo...

GRACIAS