Come implementeresti il ​​tuo lock reader / writer in C ++ 11?

Ho una serie di strutture di dati che ho bisogno di proteggere con un blocco di lettori / scrittori. Sono consapevole di boost :: shared_lock, ma mi piacerebbe avere un’implementazione personalizzata usando std :: mutex, std :: condition_variable e / o std :: atomic in modo che possa capire meglio come funziona (e modificarlo in seguito) .

Ogni struttura di dati (mobile, ma non copiabile) erediterà da una class denominata Commons che incapsula il blocco. Mi piacerebbe che l’interfaccia pubblica assomigliasse a qualcosa del genere:

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

… in modo che possa essere ereditato pubblicamente da alcuni:

 class DataStructure : public Commons {}; 

Sto scrivendo codice scientifico e generalmente posso evitare le corse di dati; questa serratura è soprattutto una salvaguardia contro gli errori che probabilmente farò più tardi. Quindi la mia priorità è un basso overhead di lettura, quindi non ostacolo troppo un programma correttamente funzionante. Ogni thread probabilmente girerà sul proprio core della CPU.

Potresti mostrarmi (pseudocodice è ok) un blocco di lettori / scrittori? Quello che ho ora dovrebbe essere la variante che impedisce la fame dello scrittore. Il mio problema principale finora è stato il divario in read_lock tra il verificare se una lettura è sicura per incrementare effettivamente un conteggio dei lettori, dopo di che write_lock sa aspettare.

 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; } 

Sono un po ‘nuovo al multithreading e mi piacerebbe davvero capirlo. Grazie in anticipo per il vostro aiuto!

Ecco lo pseudo-codice per un semplice blocco di lettore / scrittore usando un mutex e una variabile di condizione. L’API mutex dovrebbe essere auto-esplicativa. Si presume che le variabili di condizione abbiano un membro in wait(Mutex&) che (atomicamente!) Rilascia il mutex e attende che la condizione venga segnalata. La condizione viene segnalata con uno dei due signal() che sveglia un cameriere o signal_all() che sveglia tutti i camerieri.

 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(); } 

Questa implementazione ha alcuni inconvenienti, però.

Sveglia tutti i camerieri ogni volta che il blocco diventa disponibile

Se la maggior parte dei camerieri è in attesa di un blocco di scrittura, questo è uno spreco – la maggior parte dei camerieri non riuscirà ad acquisire il lucchetto, dopotutto, e riprenderà ad aspettare. Semplicemente usando signal() non funziona, perché vuoi svegliare tutti in attesa di uno sblocco del blocco di lettura. Quindi, per risolvere questo problema, sono necessarie variabili di condizione separate per la leggibilità e la scrittura.

Niente equità. I lettori affamano gli scrittori

Puoi risolvere il problema rintracciando il numero di blocchi di lettura e scrittura in sospeso e interrompi l’acquisizione di blocchi di lettura una volta che hai bloccato i blocchi di scrittura (anche se poi metti di fame i lettori!), Oppure svegliando in modo casuale tutti i lettori o uno scrittore (supponendo si utilizza una variabile di condizione separata, vedere la sezione precedente).

Le serrature non vengono distribuite nell’ordine in cui sono richieste

Per garantire questo, avrai bisogno di una vera coda di attesa. Ad esempio, è ansible creare una variabile di condizione per ogni cameriere e segnalare tutti i lettori o un singolo writer, entrambi all’inizio della coda, dopo aver rilasciato il blocco.

Persino i carichi di lavoro di lettura pura causano conflitti a causa del mutex

Questo è difficile da risolvere. Un modo è utilizzare le istruzioni atomiche per acquisire blocchi di lettura o scrittura (in genere confronto e scambio). Se l’acquisizione fallisce perché viene preso il lock, dovrai ricorrere al mutex. Farlo correttamente è piuttosto difficile, però. Inoltre, ci sarà ancora una contesa: le istruzioni atomiche sono tutt’altro che gratuite, specialmente su macchine con molti core.

Conclusione

Implementare correttamente le primitive di sincronizzazione è difficile . L’implementazione di primitive di sincronizzazione efficienti e corrette è ancora più difficile . E non paga quasi mai. pthread su Linux, ad esempio contiene un lock reader / writer che usa una combinazione di futex e istruzioni atomiche e che quindi probabilmente supera qualsiasi cosa tu possa inventare in pochi giorni di lavoro.

Controlla questa class :

 // // Multi-reader Single-writer concurrency base class for Win32 // // (c) 1999-2003 by Glenn Slayden ([email protected]) // // #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); } }; 

Non ho avuto la possibilità di provarlo, ma il codice sembra OK.