¿Cómo implementaría su propio locking lector / escritor en C ++ 11?

Tengo un conjunto de estructuras de datos que necesito proteger con un locking para lectores / escritores. Soy consciente de boost :: shared_lock, pero me gustaría tener una implementación personalizada usando std :: mutex, std :: condition_variable y / o std :: atomic para que pueda comprender mejor cómo funciona (y modificarla más tarde) .

Cada estructura de datos (movible, pero no copiable) heredará de una clase llamada Commons que encapsula el locking. Me gustaría que la interfaz pública se vea así:

class Commons { public: void read_lock(); bool try_read_lock(); void read_unlock(); void write_lock(); bool try_write_lock(); void write_unlock(); }; 

… para que pueda ser heredado públicamente por algunos:

 class DataStructure : public Commons {}; 

Estoy escribiendo código científico y generalmente puedo evitar carreras de datos; este locking es principalmente una salvaguarda contra los errores que probablemente cometeré más adelante. Por lo tanto, mi prioridad es la sobrecarga de lectura baja, por lo que no obstaculizo demasiado el funcionamiento correcto de un progtwig. Cada hilo probablemente se ejecutará en su propio núcleo de CPU.

¿Podría mostrarme (el seudocódigo está bien) un locking para lectores / escritores? Lo que tengo ahora se supone que es la variante que evita la inanición del escritor. Mi problema principal hasta ahora ha sido la brecha en read_lock entre verificar si una lectura es segura o incrementar el recuento de lectores, luego de lo cual write_lock sabe esperar.

 void Commons::write_lock() { write_mutex.lock(); reading_mode.store(false); while(readers.load() > 0) {} } void Commons::try_read_lock() { if(reading_mode.load()) { //if another thread calls write_lock here, bad things can happen ++readers; return true; } else return false; } 

Soy algo nuevo en multihilo, y realmente me gustaría entenderlo. ¡Gracias de antemano por tu ayuda!

Aquí hay un pseudocódigo para un locking de lector / escritor simplemente usando un mutex y una variable de condición. La API mutex debe ser autoexplicativa. Se supone que las variables de condición tienen una wait(Mutex&) miembro wait(Mutex&) que (atómicamente!) Elimina el mutex y espera a que se señalice la condición. La condición se señala con cualquiera de las signal() que despierta a un camarero, o signal_all() que despierta a todos los camareros.

 read_lock() { mutex.lock(); while (writer) unlocked.wait(mutex); readers++; mutex.unlock(); } read_unlock() { mutex.lock(); readers--; if (readers == 0) unlocked.signal_all(); mutex.unlock(); } write_lock() { mutex.lock(); while (writer || (readers > 0)) unlocked.wait(mutex); writer = true; mutex.unlock(); } write_unlock() { mutex.lock(); writer = false; unlocked.signal_all(); mutex.unlock(); } 

Sin embargo, esa implementación tiene bastantes inconvenientes.

Despierta a todos los camareros cuando el candado está disponible

Si la mayoría de los camareros esperan un locking de escritura, esto es un desperdicio: la mayoría de los camareros no podrán adquirir el candado, después de todo, y reanudarán la espera. Simplemente el uso de la signal() no funciona, porque desea despertar a todos los que esperan un deslocking de locking de lectura. Entonces, para solucionar eso, necesita variables de condición separadas para legibilidad y escritura.

No es justo. Los lectores mueren de hambre escritores

Puede solucionarlo rastreando el número de lockings de lectura y escritura pendientes, y deje de adquirir lockings de lectura una vez que haya lockings pendientes de escritura (¡aunque morirá de hambre a los lectores!) O despertando al azar a todos los lectores o a un escritor (asumiendo utiliza una variable de condición separada, consulte la sección anterior).

Las cerraduras no se reparten en el orden en que se solicitan

Para garantizar esto, necesitarás una cola de espera real. Podrías, por ejemplo, crear una variable de condición para cada camarero, y señalar a todos los lectores o a un solo escritor, ambos en la cabecera de la cola, después de soltar el locking.

Incluso las cargas de trabajo de lectura pura causan conflicto debido al mutex

Este es difícil de arreglar. Una forma es usar instrucciones atómicas para adquirir lockings de lectura o escritura (por lo general, comparar e intercambiar). Si la adquisición falla porque se toma el locking, tendrá que recurrir al mutex. Hacer eso correctamente es bastante difícil, sin embargo. Además, todavía habrá contención: las instrucciones atómicas están lejos de ser gratuitas, especialmente en máquinas con muchos núcleos.

Conclusión

Implementar primitivas de sincronización correctamente es difícil . Implementar primitivas de sincronización eficientes y justas es aún más difícil . Y casi nunca vale la pena. pthreads en linux, por ejemplo, contiene un locking de lector / escritor que usa una combinación de futexes e instrucciones atómicas, y que, por lo tanto, probablemente supere cualquier cosa que se te ocurra en unos pocos días de trabajo.

Verifique esta clase :

 // // Multi-reader Single-writer concurrency base class for Win32 // // (c) 1999-2003 by Glenn Slayden (glenn@glennslayden.com) // // #include "windows.h" class MultiReaderSingleWriter { private: CRITICAL_SECTION m_csWrite; CRITICAL_SECTION m_csReaderCount; long m_cReaders; HANDLE m_hevReadersCleared; public: MultiReaderSingleWriter() { m_cReaders = 0; InitializeCriticalSection(&m_csWrite); InitializeCriticalSection(&m_csReaderCount); m_hevReadersCleared = CreateEvent(NULL,TRUE,TRUE,NULL); } ~MultiReaderSingleWriter() { WaitForSingleObject(m_hevReadersCleared,INFINITE); CloseHandle(m_hevReadersCleared); DeleteCriticalSection(&m_csWrite); DeleteCriticalSection(&m_csReaderCount); } void EnterReader(void) { EnterCriticalSection(&m_csWrite); EnterCriticalSection(&m_csReaderCount); if (++m_cReaders == 1) ResetEvent(m_hevReadersCleared); LeaveCriticalSection(&m_csReaderCount); LeaveCriticalSection(&m_csWrite); } void LeaveReader(void) { EnterCriticalSection(&m_csReaderCount); if (--m_cReaders == 0) SetEvent(m_hevReadersCleared); LeaveCriticalSection(&m_csReaderCount); } void EnterWriter(void) { EnterCriticalSection(&m_csWrite); WaitForSingleObject(m_hevReadersCleared,INFINITE); } void LeaveWriter(void) { LeaveCriticalSection(&m_csWrite); } }; 

No tuve la oportunidad de probarlo, pero el código se ve bien.