Blocarea citire-scriere este un mecanism de sincronizare care permite citirea generală simultană a unor date partajate sau modificarea exclusivă a acestora, delimitând astfel blocările de citire și scriere între ele [1] . Mecanismul este conceput pentru a rezolva problema clasică cititor-scriitor , în care un obiect este citit și scris simultan de sarcini concurente [2] .
Spre deosebire de mutexuri , blocările de citire-scriere iau în considerare separat citirea datelor și scrierea separată, permițând accesul la date dacă acestea nu se modifică în acel moment. Mutexurile permit doar accesul exclusiv la date [1] . Cu toate acestea, există mutexuri partajate care oferă, pe lângă blocarea exclusivă, o blocare partajată care permite proprietatea comună a mutexului dacă nu există un proprietar exclusiv [3] . În esență, mutexurile partajate sunt blocări de citire-scriere, dar sunt denumite mutexuri.
În cazul general, blocările de citire-scriere rezolvă aceeași problemă ca și mutexurile și pot fi înlocuite de acestea, dar motivul apariției mecanismului de blocare de citire-scriere este creșterea eficienței excluderii reciproce cu citirea și scrierea separate . 4] . Blocările de citire-scriere sunt preferate față de mutexurile în cazurile în care datele sunt accesate mult mai des decât sunt scrise. În acest caz, sarcinile de citire nu se vor bloca de cele mai multe ori, doar uneori se vor bloca atunci când obiectul se schimbă. Prioritatea între sarcinile de scris și de citit este adesea acordată sarcinilor de scris pentru a evita lipsa de resurse a sarcinilor de scris [1] .
Problema cititorilor și scriitorilor apare în orice situație în care citirea și modificarea concomitentă a unei structuri de date, a unui sistem de fișiere sau a unei baze de date este cerută de sarcini concurente. Citirea datelor imuabile poate fi efectuată simultan de mai multe sarcini, totuși, dacă apar modificări de date în acest moment, citirea lor paralelă poate duce la date parțial modificate, adică date corupte [2] .
Soluția problemei este asimetrică și implică împărțirea lacătului în citire și scriere. Modificarea datelor este permisă exclusiv exclusiv, adică doar o singură activitate poate obține o blocare de scriere la un moment dat, cu excepția cazului în care este achiziționată o blocare de citire. Citirea datelor poate fi efectuată de mai multe sarcini, astfel încât câte sarcini se dorește pot obține o blocare de citire în același timp, cu excepția cazului în care este obținută o blocare de scriere. Adică scrierea și citirea secțiunilor critice nu pot fi executate în paralel, dar citirea secțiunilor critice poate [2] .
Cel mai simplu algoritm de implementare pentru semafore și mutexuri este utilizarea unui comutator de semafor binar. Intrarea trebuie protejată de acest semafor. Prima sarcină care citește trebuie să blocheze semaforul cu un comutator, blocând firele de scriere, iar ultima sarcină care își termină munca trebuie să elibereze semaforul, permițând sarcinilor de scriere să-și continue munca [5] . Totuși, această implementare are o problemă serioasă, comparabilă cu blocajul - lipsa de resurse a sarcinilor de scriere [6] .
Pseudocod pentru un algoritm simplu de blocare citire-scriereInițializare | Sarcina de citire | Sarcina de scriere |
---|---|---|
comutator = Comutator() permisiunea de scriere = Semafor(1) | blocare (comutator, permisiune-scriere) // Secțiunea critică a sarcinii de citire deblocare (comutator, permisiune-scriere) | captura (permisiune de scriere) // Secțiunea critică a sarcinii de scriere eliberare (permisiune de scriere) |
Algoritmul universal, lipsit de problema descrisă mai sus, include un comutator semafor binar A pentru a organiza o secțiune critică a sarcinilor de lectură și un turnic pentru a bloca noi sarcini de lectură în prezența scriitorilor în așteptare. Când sosește prima sarcină de citit, acesta captează semaforul A cu un comutator, împiedicând scrierile. Pentru scriitori, semaforul A protejează secțiunea critică a scriitorului, așa că dacă este capturată de cititori, toți scriitorii blochează intrarea în secțiunea lor critică. Totuși, sarcinile de captare de către scriitor a semaforului A și scrierea ulterioară este protejată de semaforul turnichet. Prin urmare, dacă are loc o blocare a unei sarcini de scriere din cauza prezenței cititorilor, turnichetul este blocat împreună cu noi sarcini de citire. De îndată ce ultimul cititor își termină treaba, semaforul comutatorului este eliberat și primul scriitor din coadă este deblocat. La sfârșitul activității sale, eliberează semaforul turnichetului, permițând din nou munca de citire a sarcinilor [7] .
Pseudocod al algoritmului universal de blocare citire-scriereInițializare | Sarcina de citire | Sarcina de scriere |
---|---|---|
comutator = Comutator() permisiunea de scriere = Semafor(1) turnichet = Semafor(1) | apuca (turnichet) eliberare (turnichet) blocare (comutator, permisiune-scriere) // Secțiunea critică a sarcinii de citire deblocare (comutator, permisiune-scriere) | apuca (turnichet) captura (permisiune de scriere) // Secțiunea critică a sarcinii de scriere da drumul (turnichet) eliberare (permisiune de scriere) |
La nivel de sistem de operare, există implementări de semafore de citire și scriere, care sunt modificate în mod special pentru a crește eficiența în utilizarea în masă. Implementările de blocări de citire-scriere se pot baza atât pe mutexuri, cât și pe spinlock -uri [4] .
În timp ce blocările de citire-scriere pot îmbunătăți viteza unor algoritmi, ele au o problemă ascunsă care apare atunci când există o densitate uniformă a cererilor de citire. În acest caz, achiziția unei blocări de scriere poate fi întârziată pentru perioade nelimitate de timp, provocând lipsa de resurse a sarcinilor de scriere [4] . Înfometarea de resurse a sarcinilor de redactare este comparabilă cu un impas , deoarece scrierea datelor va fi imposibilă în timp ce sosesc noi sarcini de citire. În acest caz, problema poate să nu fie vizibilă până când sarcina pe sistem este foarte mare, dar poate începe să se manifeste atunci când sarcina crește. Soluția poate fi integrată în implementarea blocărilor de citire-scriere și implică blocarea oricăror sarcini noi de citire dacă există cel puțin un scriitor care așteaptă blocarea [6] .
Conceptul de escaladare a blocării permite ca o blocare de citire capturată să fie promovată la o blocare de scriere exclusivă. O blocare este promovată atunci când nu mai există sarcini de citire, în caz contrar, sarcina se blochează până când sarcinile de citire eliberează blocarea. Conceptul permite, de asemenea, retrogradarea unei blocări de scriere la o blocare de citire [8] . Cu toate acestea, conceptul este adesea opțional și nu trebuie să fie prezent în implementări specifice.
În standardul POSIX , blocările de citire-scriere sunt reprezentate de un tip pthread_rwlock_tîn fișierul antet pthread.h. Blocările pot fi dați anumiți parametri prin atribute, în special, o blocare poate fi definită ca fiind disponibilă între procese sau numai între fire, iar o blocare disponibilă între procese este cerută de standard. Dacă nu există sarcini de citire, ordinea în care sarcinile de scriere dobândesc blocarea este determinată de politica de planificare selectată. Cu toate acestea, prioritatea de achiziție a blocării între sarcinile de scriitor și cititor nu este definită de standard [1] .
În API -ul Windows , blocările sunt reprezentate de o structură SRWLOCKdintr-un fișier antet Synchapi.hși un set de funcții pentru lucrul cu acesta. Blocările sunt proiectate să funcționeze cu fire într-un singur proces și nicio comandă nu este garantată pentru a obține blocări. Dintre caracteristici, utilizarea unui blocare este suportată împreună cu o variabilă de condiție printr-o funcție SleepConditionVariableSRW()[9] .
Limba | Modul sau bibliotecă | Tip de date |
---|---|---|
Xi | pthread | pthread_rwlock_t[unu] |
C++ | std | std::shared_mutex[3] |
C# | System.Threading | ReaderWriterLock[zece] |
Merge | sync | RWMutex[unsprezece] |
Java | java.base,java.util.concurrent.locks | ReentrantReadWriteLock[12] |
Rugini | std | std::sync::RwLock[13] |