Bloqueo de lectura / escritura de Win32 usando solo secciones críticas

Tengo que implementar un locking de lectura / escritura en C ++ utilizando la API de Win32 como parte de un proyecto en el trabajo. Todas las soluciones existentes utilizan objetos del kernel (semáforos y mutex) que requieren un cambio de contexto durante la ejecución. Esto es demasiado lento para mi aplicación.

Me gustaría implementar uno usando solo secciones críticas, si es posible. El locking no tiene que ser seguro para el proceso, solo debe ser seguro para subprocesos. ¿Alguna idea sobre cómo hacer esto?

No creo que esto se pueda hacer sin usar al menos un objeto de nivel de kernel (Mutex o Semaphore), porque necesita la ayuda del kernel para bloquear el proceso de llamada hasta que el locking esté disponible.

Las secciones críticas proporcionan locking, pero la API es demasiado limitada. por ejemplo, no puede tomar un CS, descubrir que un locking de lectura está disponible pero no un locking de escritura, y esperar a que el otro proceso termine de leer (porque si el otro proceso tiene la sección crítica, bloqueará a otros lectores, lo que está mal, y si no lo hace, entonces su proceso no se bloqueará sino girará, quemando ciclos de CPU.)

Sin embargo, lo que puede hacer es usar un locking de giro y retroceder a un mutex siempre que haya disputa. La sección crítica se implementa de esta manera. Tomaría una implementación de la sección crítica existente y reemplazaría el campo PID con conteos separados de lectores y escritores.

Si puede apuntar a Vista o superior, debe usar el SRWLock incorporado . Son ligeros como secciones críticas, totalmente en modo de usuario cuando no hay contención.

El blog de Joe Duffy tiene algunas entradas recientes sobre la implementación de diferentes tipos de lockings de locking / lectura sin locking. Estas cerraduras giran, por lo que no serían apropiadas si tiene la intención de hacer mucho trabajo mientras mantiene la cerradura. El código es C #, pero debe ser sencillo de portar a nativo.

Puede implementar un locking de lector / escritor utilizando secciones y eventos críticos; solo debe mantener el estado suficiente para señalar solo el evento cuando sea necesario para evitar una llamada innecesaria del modo kernel.

Antigua pregunta, pero esto es algo que debería funcionar. No gira en la contención. Los lectores incurren en un costo adicional limitado si tienen poca o ninguna contención, porque SetEvent se llama perezosamente (consulte el historial de edición para obtener una versión más pesada que no tenga esta optimización).

 #include  typedef struct _RW_LOCK { CRITICAL_SECTION countsLock; CRITICAL_SECTION writerLock; HANDLE noReaders; int readerCount; BOOL waitingWriter; } RW_LOCK, *PRW_LOCK; void rwlock_init(PRW_LOCK rwlock) { InitializeCriticalSection(&rwlock->writerLock); InitializeCriticalSection(&rwlock->countsLock); /* * Could use a semaphore as well. There can only be one waiter ever, * so I'm showing an auto-reset event here. */ rwlock->noReaders = CreateEvent (NULL, FALSE, FALSE, NULL); } void rwlock_rdlock(PRW_LOCK rwlock) { /* * We need to lock the writerLock too, otherwise a writer could * do the whole of rwlock_wrlock after the readerCount changed * from 0 to 1, but before the event was reset. */ EnterCriticalSection(&rwlock->writerLock); EnterCriticalSection(&rwlock->countsLock); ++rwlock->readerCount; LeaveCriticalSection(&rwlock->countsLock); LeaveCriticalSection(&rwlock->writerLock); } int rwlock_wrlock(PRW_LOCK rwlock) { EnterCriticalSection(&rwlock->writerLock); /* * readerCount cannot become non-zero within the writerLock CS, * but it can become zero... */ if (rwlock->readerCount > 0) { EnterCriticalSection(&rwlock->countsLock); /* ... so test it again. */ if (rwlock->readerCount > 0) { rwlock->waitingWriter = TRUE; LeaveCriticalSection(&rwlock->countsLock); WaitForSingleObject(rwlock->noReaders, INFINITE); } else { /* How lucky, no need to wait. */ LeaveCriticalSection(&rwlock->countsLock); } } /* writerLock remains locked. */ } void rwlock_rdunlock(PRW_LOCK rwlock) { EnterCriticalSection(&rwlock->countsLock); assert (rwlock->readerCount > 0); if (--rwlock->readerCount == 0) { if (rwlock->waitingWriter) { /* * Clear waitingWriter here to avoid taking countsLock * again in wrlock. */ rwlock->waitingWriter = FALSE; SetEvent(rwlock->noReaders); } } LeaveCriticalSection(&rwlock->countsLock); } void rwlock_wrunlock(PRW_LOCK rwlock) { LeaveCriticalSection(&rwlock->writerLock); } 

Podría reducir el costo para los lectores utilizando una sola CRITICAL_SECTION :

  • countsLock se reemplaza con writerLock en rdlock y rdunlock

  • rwlock->waitingWriter = FALSE se elimina en wrunlock

  • El cuerpo de Wrlock ha cambiado a

     EnterCriticalSection(&rwlock->writerLock); rwlock->waitingWriter = TRUE; while (rwlock->readerCount > 0) { LeaveCriticalSection(&rwlock->writerLock); WaitForSingleObject(rwlock->noReaders, INFINITE); EnterCriticalSection(&rwlock->writerLock); } rwlock->waitingWriter = FALSE; /* writerLock remains locked. */ 

Sin embargo, esto pierde en justicia, por lo que prefiero la solución anterior.

Eche un vistazo al libro ” Progtwigción concurrente en Windows “, que tiene muchos ejemplos de referencia diferentes para lockings de lector / escritor.

Echa un vistazo a spin_rw_mutex de los Thread Building Blocks de Intel …

spin_rw_mutex está estrictamente en la zona de usuario y emplea el método de espera de espera para el locking

Esta es una pregunta antigua, pero tal vez alguien lo encuentre útil. Desarrollamos un RWLock código RWLock para Windows de alto rendimiento que utiliza Vista + SRWLock Michael mencionado, si está disponible, o si no, SRWLock a una implementación de espacio de usuario.

Como un bono adicional, hay cuatro “sabores” diferentes (aunque puede apegarse a lo básico, que también es el más rápido), cada uno con más opciones de sincronización. Comienza con el RWLock() básico RWLock() que no es reentrante, se limita a la sincronización de un solo proceso, y no se intercambian los lockings de lectura / escritura a un RCP de IPC de proceso cruzado completo con soporte de reingreso y de lectura / escritura. elevación.

Como se mencionó, se cambian dinámicamente a los lockings de lectura-escritura delgados Vista + para obtener el mejor rendimiento cuando sea posible, pero no tiene que preocuparse por eso, ya que recurrirá a una implementación totalmente compatible en Windows XP y su ilk

Si ya conoce una solución que solo usa exclusión mutua, debería poder modificarla para usar secciones críticas en su lugar.

Rodamos el nuestro usando dos secciones críticas y algunos contadores. Se adapta a nuestras necesidades: tenemos muy pocos escritores, los escritores tienen prioridad sobre los lectores, etc. No tengo la libertad de publicar la nuestra, pero puedo decir que es posible sin mutexos y semáforos.

Aquí está la solución más pequeña que pude encontrar:

http://www.baboonz.org/rwlock.php

Y pegado textualmente:

 /** A simple Reader/Writer Lock. This RWL has no events - we rely solely on spinlocks and sleep() to yield control to other threads. I don't know what the exact penalty is for using sleep vs events, but at least when there is no contention, we are basically as fast as a critical section. This code is written for Windows, but it should be trivial to find the appropriate equivalents on another OS. **/ class TinyReaderWriterLock { public: volatile uint32 Main; static const uint32 WriteDesireBit = 0x80000000; void Noop( uint32 tick ) { if ( ((tick + 1) & 0xfff) == 0 ) // Sleep after 4k cycles. Crude, but usually better than spinning indefinitely. Sleep(0); } TinyReaderWriterLock() { Main = 0; } ~TinyReaderWriterLock() { ASSERT( Main == 0 ); } void EnterRead() { for ( uint32 tick = 0 ;; tick++ ) { uint32 oldVal = Main; if ( (oldVal & WriteDesireBit) == 0 ) { if ( InterlockedCompareExchange( (LONG*) &Main, oldVal + 1, oldVal ) == oldVal ) break; } Noop(tick); } } void EnterWrite() { for ( uint32 tick = 0 ;; tick++ ) { if ( (tick & 0xfff) == 0 ) // Set the write-desire bit every 4k cycles (including cycle 0). _InterlockedOr( (LONG*) &Main, WriteDesireBit ); uint32 oldVal = Main; if ( oldVal == WriteDesireBit ) { if ( InterlockedCompareExchange( (LONG*) &Main, -1, WriteDesireBit ) == WriteDesireBit ) break; } Noop(tick); } } void LeaveRead() { ASSERT( Main != -1 ); InterlockedDecrement( (LONG*) &Main ); } void LeaveWrite() { ASSERT( Main == -1 ); InterlockedIncrement( (LONG*) &Main ); } }; 

Mira mi implementación aquí:

https://github.com/coolsoftware/LockLib

VRWLock es una clase de C ++ que implementa un solo escritor: lógica de varios lectores.

Mira también proyecto de prueba TestLock.sln.

UPD. A continuación se muestra el código simple para lector y escritor:

 LONG gCounter = 0; // reader for (;;) //loop { LONG n = InterlockedIncrement(&gCounter); // n = value of gCounter after increment if (n <= MAX_READERS) break; // writer does not write anything - we can read InterlockedDecrement(&gCounter); } // read data here InterlockedDecrement(&gCounter); // release reader // writer for (;;) //loop { LONG n = InterlockedCompareExchange(&gCounter, (MAX_READERS+1), 0); // n = value of gCounter before attempt to replace it by MAX_READERS+1 in InterlockedCompareExchange // if gCounter was 0 - no readers/writers and in gCounter will be MAX_READERS+1 // if gCounter was not 0 - gCounter stays unchanged if (n == 0) break; } // write data here InterlockedExchangeAdd(&gCounter, -(MAX_READERS+1)); // release writer 

La clase VRWLock admite el conteo de giros y el conteo de referencia específico de subprocesos que permite liberar lockings de subprocesos terminados.

Escribí el siguiente código usando solo secciones críticas.

 class ReadWriteLock { volatile LONG writelockcount; volatile LONG readlockcount; CRITICAL_SECTION cs; public: ReadWriteLock() { InitializeCriticalSection(&cs); writelockcount = 0; readlockcount = 0; } ~ReadWriteLock() { DeleteCriticalSection(&cs); } void AcquireReaderLock() { retry: while (writelockcount) { Sleep(0); } EnterCriticalSection(&cs); if (!writelockcount) { readlockcount++; } else { LeaveCriticalSection(&cs); goto retry; } LeaveCriticalSection(&cs); } void ReleaseReaderLock() { EnterCriticalSection(&cs); readlockcount--; LeaveCriticalSection(&cs); } void AcquireWriterLock() { retry: while (writelockcount||readlockcount) { Sleep(0); } EnterCriticalSection(&cs); if (!writelockcount&&!readlockcount) { writelockcount++; } else { LeaveCriticalSection(&cs); goto retry; } LeaveCriticalSection(&cs); } void ReleaseWriterLock() { EnterCriticalSection(&cs); writelockcount--; LeaveCriticalSection(&cs); } }; 

Para realizar un giro-espera, comente las líneas con Sleep (0).