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.