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.