MUSICONICA
Páginas: [1]   Ir Abajo
  Imprimir  
Autor Tema: Manejo de Concurrencia en Windows  (Leído 473 veces)
0 Usuarios y 1 Visitante están viendo este tema.
Iker
Administrador
Cuasi CuTeano
*
Desconectado Desconectado

Mensajes: 90



Ver Perfil WWW
« en: 06 de Febrero de 2010, 08:54:07 »

Con la llegada de los procesadores multi núcleo el mundo de la programación a dado un interesante giro para los desarrolladores, imponiendo nuevos retos y sobre todo mayores cuidados a la hora de manejar la sincronización entre hilos y procesos asíncronos, es por esta razón que escribo este artículo con el fin de dar una mirada general de los diferentes métodos de sincronización entre hilos en situaciones especificas.


Diferencias entre sistemas Multi núcleo y mono núcleo

En los sistemas mono núcleo ( P4, AMD K6/7, etc.. ) estábamos acostumbrados a manejar un programa secuencial y aunque la sensación de multitarea todo desarrollador que se precie de serlo sabía que no era así, que en realidad los programas seguían siendo secuenciales y por ende la sincronización entre diversos hilos y su acceso a memoria compartida no representaba mayor problema; ya que el acceso a la misma se realizaba también de forma secuencial.

Con la llegada de los procesadores multi núcleo la historia cambio, mejoro el performance de las aplicaciones y los sistemas operativos pero.... los programas también comenzaron a fallar. La razón es muy simple, su diseño no estaba preparado para la verdadera multitarea, la inclusión de caches de segundo nivel en los procesadores cambio el método en que se accede a la memoria y las operaciones que antes creíamos atómicas dejaron de serlo.
Para más información sobre los diferentes modelos de memoria y como son manejados por los procesadores:
http://http://www.intel.com/products/processor/manuals/


Sistemas de sincronización entre hilos sobre Windows

En windows podemos encontrar diferentes métodos para sincronizar hilos como lo pueden ser:

Citar
Mutex
Semáforos
Secciones Críticas
Eventos
SpinLocks( En Drivers )

La pregunta que sale de estos diversos métodos es cual nos ofrece una implementación sencilla, redituable en performance y totalmente efectiva en cuanto a exclusión mutua se refiere.
En este artículo analizaremos una de las más extendidas y redituables en cuanto a performance como lo son las Secciones Criticas y una nueva API introducida con windows vista llamada variables condicionales.


Secciones Críticas

Las secciones criticas son ampliamente conocidas por los desarrolladores de windows en lenguajes como C y C++, estas proveen el acceso exclusivo a una porción de código ubicada entre "EnterCriticalSection" y "LeaveCriticalSection", aunque también a menudo causan dolores de cabeza con los deadlocks producto tanto de olvidos en el código como de posibles fallos del mismo.
El problema que atañe a todo desarrollador es que el uso de secciones críticas puede representarle un golpe fuerte en uso del procesador si no son usadas adecuadamente; el siguiente ejemplo es un código simple de uso de secciones críticas que demuestra lo anterior:

Generador:

Código:
DWORD WINAPI Generator( LPVOID lpvParam )
{
timeval tvInit, tvEnd;
int iAt = 0;

gettimeofday( &tvInit, NULL);

for ( ; iAt < 512; ++iAt) {
EnterCriticalSection( &g_csVar );

g_iVar = rand()%1024;
g_bReady = true;

LeaveCriticalSection( &g_csVar );

// Do Stuff
Sleep( 50 );
}

gettimeofday( &tvEnd, NULL);

g_bIsStarted = false;

printf("Generator time %d\r\n", DiffTime( &tvInit, &tvEnd));

return 0;
}

Trabajador:

Código:
DWORD WINAPI Worker( LPVOID lpvParam )
{
int lMyVal = 0, iReads = 0;
timeval tvInit, tvEnd;

gettimeofday( &tvInit, NULL);

while (g_bIsStarted == true) {
EnterCriticalSection( &g_csVar );

if (g_bReady == false) {
LeaveCriticalSection( &g_csVar );
continue;
}

lMyVal = g_iVar;
g_bReady = false;

++iReads;

LeaveCriticalSection( &g_csVar );

// do stuff
Sleep( 5 );
}

gettimeofday( &tvEnd, NULL);

printf("Reads %d - Time Elapsed: %d\r\n", iReads, DiffTime( &tvInit, &tvEnd));

return 0;
}

Aclarar que las funciones gettimeofday y DiffTime son de implementación propia y no hacen parte del API de windows.
En el anterior caso el número de workers es 2; ahora si lo ejecutamos podremos ver que el consumo de CPU por parte del programa es sumamente alto ( 100% ). El tiempo "perfecto" de ejecución del proceso debería estar por el orden de los 25.6 Segundos((50 * 512)/1000), si examinamos el resultado arrojado por el programa es:

Citar
Reads 293 - Time Elapsed: 29969
Reads 219 - Time Elapsed: 29969
Generator time 29970

Por qué sucede esto?, el bloqueo que generan los Workers sobre el generador hace que este no pueda acceder a la variable para setear su valor, retrasando el proceso de escritura y por ende el que los workers dispongan de información; para validar esto vamos a añadir una variable en el worker que contabilice los fallos registrados al intentar realizar una lectura:

Código:
if (g_bReady == false) {
LeaveCriticalSection( &g_csVar );
++iFails;
continue;
}

El resultado ahora es:

Citar
Generator time 29785
Reads 187 Fails 176088581 - Time Elapsed: 29785
Reads 325 Fails 166428580 - Time Elapsed: 29785

La cantidad de ciclos desperdiciados es increíblemente alta, para abolir el consumo de CPU excesivo y el desperdiciar ciclos podemos implementar un Sleep sobre nuestros workers:

Código:
if (g_bReady == false) {
LeaveCriticalSection( &g_csVar );
++iFails;
Sleep( 10 );
continue;
}

El resultado es:

Citar
Generator time 25691
Reads 135 Fails 2488 - Time Elapsed: 25692
Reads 377 Fails 2351 - Time Elapsed: 25699

Mucho mejor, inclusive se acerca bastante al tiempo "perfecto", pero aun así no es recomendable el desperdiciar ciclos cuando podemos señalar los eventos, así que mezclaremos secciones criticas, en este caso "TryEnterCriticalSection" y eventos. Un error muy común al usar secciones criticas y eventos recae en el implementar bien sea eventos Automáticos ( No son apropiados para sincronizar más de 2 hilos ) o el usar eventos manuales pero los cuales el procedimiento de Reset se encuentra por fuera de la sección critica; la razón es muy simple ya que en realidad si se implementan el reseteo del evento fuera de la sección critica se está consiguiendo una condición de carrera al estar el evento señalado y la sección critica libre; dando así paso para que los otros hilos accedan a información que posiblemente ya fue procesada o simplemente que no halla información. El ejemplo usado en nuestro caso será una pequeña modificación del anterior:

Generador:

Código:
for ( ; iAt < 10; ++iAt) {
EnterCriticalSection( &g_csVar );

g_iVar = rand()%1024;

SetEvent( g_hEvent );

LeaveCriticalSection( &g_csVar );

// Do Stuff
Sleep( 50 );
}

gettimeofday( &tvEnd, NULL);

g_bIsStarted = false;

SetEvent( g_hEvent );

Trabajador:

Código:
while (g_bIsStarted == true) {
WaitForSingleObject( g_hEvent, INFINITE);

if (g_bIsStarted == false)
break;

if (TryEnterCriticalSection( &g_csVar ) == FALSE) {
++iFails;
continue;
}

lMyVal = g_iVar;

++iReads;

ResetEvent( g_hEvent );

LeaveCriticalSection( &g_csVar );

// do stuff
Sleep( 5 );
}

El resultado arrojado es:

Citar
Reads 218 Fails 11131317 - Time Elapsed: 34848
Generator time 34848
Reads 370 Fails 10569731 - Time Elapsed: 34848

Podemos observar que realmente los tiempos no son los mejores, los fallos al intentar entrar a la sección critica son bastante altos, el consumo de CPU es del 25% y aparte de ello se ha cometido un error que es bastante común en este tipo de implementaciones por lo que se ha dejado abierta la posibilidad de una condición de carrera(a ver si lo descubren).

Viendo todo lo anterior nos hemos podido dar cuenta que algunos de los métodos que utilizan secciones criticas no son una muy buena idea y suelen estar sujetos a fallos muy comunes que bien pueden afectar tanto el performance como el acceso exclusivo a la información.


Variables Condicionales

Con la llegada de windows vista se introdujo una nueva API de sincronización con el fin de evitar los problemas que hemos visto anteriormente, que de por si son sumamente comunes.  Esta API tiene como objetivo el facilitar la implementación de métodos que aseguren el acceso exclusivo a porciones de código.
La forma de trabajar con las variables condiciones es muy parecida a los eventos, primeramente necesitamos una sección critica y una CONDITION_VARIABLE, una vez que entramos en la sección critica usamos "SleepConditionVariableCS", esta toma como parámetros la variable de variable de condición, la sección critica y el tiempo a esperar, el tomar la sección critica es debido a que primero debemos entrar en la sección critica, cuando llamemos a SleepConditioVariableCS este saldrá de la sección critica y esperara por el evento, cuando el evento sea señalado retomara la sección critica.
En cuanto al generador este debe utilizar la función WakeConditionVariable para señalar la condición, esta se limita a dejar pasar un solo hilo que se encuentre en espera, si queremos despertar todos los hilos en espera deberemos usar WakeAllConditionVariable.

En el siguiente ejemplo vamos a evaluar el rendimiento de los siguientes métodos de exclusión:

Citar
Secciones Críticas
Secciones Críticas con Sleep
Secciones criticas mediante TryEnterCriticalSection
Secciones criticas mediante TryEnterCriticalSection con Sleep
Variables condicionales

El código usado será el siguiente:

Generador:

Código:
bool WriteJob( long lJob )
{
DWORD dwWrite = 0;

EnterCriticalSection( &g_csPipe );

WriteFile( g_hPipeWrite, &lJob, sizeof( long ), &dwWrite, NULL);

LeaveCriticalSection( &g_csPipe );

return true;
}

DWORD WINAPI Generator( LPVOID lpvParam )
{
timeval tvInit, tvEnd;
long lIn = 0;
int iAt = 0;

srand( 0x0ABCDL );

gettimeofday( &tvInit, NULL);

while (g_bStarted == true && iAt < 512) {

while ((lIn = rand()) <= -1);

if (WriteJob( lIn ) == false)
continue;

Sleep( 150 );

++iAt;
}

g_bStarted = false;

gettimeofday( &tvEnd, NULL);

printf("Generators ends at %d\r\n", DiffTime( &tvInit, &tvEnd));

return 0;
}

Trabajador:

Código:
long ReadJob( void )
{
BOOL bOper = FALSE;
DWORD dwRead = 0;
long lRet = -1;

EnterCriticalSection( &g_csPipe );

if (g_bStarted == false) {
LeaveCriticalSection( &g_csPipe );
return -1;
}

bOper = PeekNamedPipe( g_hPipeRead, &lRet, sizeof( long ), &dwRead, NULL, NULL);

if (bOper == TRUE && dwRead == sizeof( long ))
ReadFile( g_hPipeRead, &lRet, sizeof( long ), &dwRead, NULL);

LeaveCriticalSection( &g_csPipe );

return lRet;
}

DWORD WINAPI Worker( LPVOID lpvParam )
{
timeval tvInit, tvEnd;
long lRes = 0;
int iReads = 0;

gettimeofday( &tvInit, NULL);

while (g_bStarted == true) {
if ((lRes = ReadJob()) <=  -1) {
Sleep( 10 );
continue;
}

++iReads;

// Do something xD
Sleep( 5 );
}

gettimeofday( &tvEnd, NULL);

printf("Worker ends at %d reads:%d\r\n", DiffTime( &tvInit, &tvEnd), iReads);

return 0;
}

Con las modificaciones propias para usar los diferentes métodos como es el caso de TryEnterCriticalSection:

Trabajador ( Lector ):

Código:
long ReadJob( void )
{
BOOL bOper = FALSE;
DWORD dwRead = 0;
long lRet = -1;


if (TryEnterCriticalSection( &g_csPipe ) == FALSE)
return -1;

Generador ( Escritor ):

Código:
bool WriteJob( long lJob )
{
DWORD dwWrite = 0;

if (TryEnterCriticalSection( &g_csPipe ) == FALSE)
return false;

En el caso de las variables condicionales anularemos los sleeps y modificaremos el código de la siguiente forma:

Trabajador ( Lector ):

Código:
long ReadJob( void )
{
BOOL bOper = FALSE;
DWORD dwRead = 0;
long lRet = -1;


EnterCriticalSection( &g_csPipe );

SleepConditionVariableCS( &g_cvVar, &g_csPipe,INFINITE);

if (g_bStarted == false) {
LeaveCriticalSection( &g_csPipe );
return -1;
}

// Anulamos el peek a la pipe ya que el evento señala la
// disponibilidad de informacion.

ReadFile( g_hPipeRead, &lRet, sizeof( long ), &dwRead, NULL);

LeaveCriticalSection( &g_csPipe );

return lRet;
}

Generador ( Escritor ):

Código:
bool WriteJob( long lJob )
{
DWORD dwWrite = 0;

EnterCriticalSection( &g_csPipe );

WriteFile( g_hPipeWrite, &lJob, sizeof( long ), &dwWrite, NULL);

LeaveCriticalSection( &g_csPipe );

WakeConditionVariable( &g_cvVar );

return true;
}

Generador:

Código:
g_bStarted = false;

WakeAllConditionVariable( &g_cvVar );

gettimeofday( &tvEnd, NULL);

printf("Generators ends at %d\r\n", DiffTime( &tvInit, &tvEnd));

return 0;
}


Como podemos ver en el caso de las variables condicionales el código llega a simplificarse debido a que sabemos que nos está señalando bien o el estado de disponibilidad de información "WakeConditionVariable" para un solo hilo o el de salida "WakeAllConditionVariable" para todos los hilos.

Teniendo en cuenta que el resultado "optimo" es de 76.800 Segundos; los resultados de los procedimientos son los siguientes:

http://http://3.bp.blogspot.com/_zWBNoSB1aQA/S24cLMLaGgI/AAAAAAAAAFc/f2e2cXFUdzQ/s400/Untitled2.png

La información relativa a los workers denota "tiempo/lecturas", es interesante el que en las Variables Condicionales se presenta un balance de carga para el acceso a la información, de igual manera parece ser el método mas efectivo, simple y optimizado en cuanto a operaciones de exclusión mutua se refiere.


Conclusión

Con la llegada de esta nueva API de exclusión en windows vista podemos ahorrarnos múltiples dolores de cabeza además de mejorar el desempeño de nuestro programa, aun así en el llegado caso de que no podamos utilizarla debido a problemas de compatibilidad o soporte de otras plataformas deberíamos tener en cuenta las opciones que se nos presentan en estas pruebas como candidatas a desarrollar este trabajo de una buena forma.

Salu2.

Att: Iker
« Última modificación: 06 de Febrero de 2010, 08:57:55 por Iker » En línea
ZEALOT
Administrador
Desocupado
*
Desconectado Desconectado

Mensajes: 156


Ver Perfil WWW
« Respuesta #1 en: 08 de Febrero de 2010, 05:28:52 »

hola todos,

Es cierto, generalmente se utiliza,

Código:
while(!TryEnterCriticalSection(&Lock))
sleep(0);

lo cual es menos ineficiente, porque no pasa a modo kernel, teniendo en cuenta los convoy locks que pudieran ocurrir...

Para evitar la perdida de rendimiento con TryEnterCriticalSection, tambien podemos usar el spinlock de la seccion critica usando InitializeCriticalSectionAndSpinCount, pero cuando no se tiene multinucleo se nos jode la vaina...

Es hacer ese machetazo o hacer una implementacion mas complicada con otra opcion sin multinucleo... Esas opciones en vista aguantan mucho... infortunadamente no son compatibles con versiones anteriores...
En línea

_
De la programacion y otros demonios: http://http://www.preludioobsesivo.tk
Iker
Administrador
Cuasi CuTeano
*
Desconectado Desconectado

Mensajes: 90



Ver Perfil WWW
« Respuesta #2 en: 08 de Febrero de 2010, 11:29:32 »

Son hermosas las variables condicionales, el problema con los spinscounts es que si ponemos un numero alto podemos perder performance y si ponemos uno bajo nos fritamos el procesador, hay que evaluarlo en el numero de hilos, el tiempo que demora cada reader/writer y que tantos van a acceder al tiempo, por eso no los incluí ya que las pruebas de performance se iban al tope o no le ganaban al TryEnterblabla con sleep y de eventos y mutex mejor ni hablamos xD.

Salu2.

PD: Gracias por responder ( me siento menos sólito Cry Kiss )

Att: Iker
En línea
nitr0k1ller
Zen cracking!!!
Miembro [CuT]
Vago degenerado
***
Conectado Conectado

Mensajes: 618


Interneeeee!!!


Ver Perfil
« Respuesta #3 en: 09 de Febrero de 2010, 03:23:06 »

si dejara de poner códigos en c y lo hiciera en un lenguaje mas pussy no estaría tan solo xD
En línea
flacman
Administrador
Vago degenerado
*
Desconectado Desconectado

Mensajes: 2.800


Trabajar, trabajar y trabajar! . Uribe


Ver Perfil WWW
« Respuesta #4 en: 11 de Febrero de 2010, 10:29:58 »

xD nitro es q el hecho es hacer las comparacioens de api windows, igual no es tan jodo, hasta yo lo entendí xD... m1 y la otra razón es exceso de tiempo, jajaja cut entra en standby en época de U
En línea

Posted by
tronador
Administrador
Vago degenerado
*
Desconectado Desconectado

Mensajes: 417


Linuxsss


Ver Perfil WWW
« Respuesta #5 en: 14 de Febrero de 2010, 04:09:40 »

Ole pongamos a Iker a trabajar en Scylla a que mejore a un mas el rendimiento de esa cosa. (Y para que programe en C# xD)

P.D: Yo me quedo esperando el de "Manejo de Concurrencia en UNIX"
« Última modificación: 14 de Febrero de 2010, 04:12:30 por tronador » En línea





Iker
Administrador
Cuasi CuTeano
*
Desconectado Desconectado

Mensajes: 90



Ver Perfil WWW
« Respuesta #6 en: 14 de Febrero de 2010, 10:45:32 »

Se le have, lo que no se le have es tiempo, pereme un rato xD.

Salu2.

Att: Iker
En línea
Páginas: [1]   Ir Arriba
  Imprimir  
 
Ir a:  

Modify by RPM.
Página creada en 0.075 segundos con 20 queries.