Zámek čtení a zápisu je synchronizační mechanismus, který umožňuje současné obecné čtení některých sdílených dat nebo jejich výhradní úpravu, čímž mezi sebou vymezuje zámky čtení a zápisu [1] . Mechanismus je navržen tak, aby řešil klasický problém čtenář-zapisovatel , ve kterém je některý objekt současně čten a zapisován souběžnými úlohami [2] .
Na rozdíl od mutexů zámky čtení a zápisu samostatně berou v úvahu čtení dat a oddělený zápis, což umožňuje přístup k datům, pokud se v tu chvíli nezmění. Mutexy umožňují pouze výhradní přístup k datům [1] . Existují však sdílené mutexy, které poskytují kromě výhradního zámku i sdílený zámek, který umožňuje sdílené vlastnictví mutexu, pokud neexistuje výhradní vlastník [3] . Sdílené mutexy jsou ve svém jádru zámky pro čtení a zápis, ale označují se jako mutexy.
V obecném případě zámky čtení a zápisu řeší stejný problém jako mutexy a lze je jimi nahradit, ale důvodem vzniku mechanismu zámku pro čtení a zápis je zvýšení účinnosti vzájemného vyloučení s odděleným čtením a zápisem [ 4] . Zámky čtení a zápisu jsou upřednostňovány před mutexy v případech, kdy se k datům přistupuje mnohem častěji, než je zapisováno. V tomto případě se úlohy čtení většinu času neblokují, pouze někdy při změně objektu. Priorita mezi psaním a čtením je často dána psaní úkolů, aby se předešlo nedostatku zdrojů při psaní úkolů [1] .
Problém čtenářů a zapisovačů nastává v každé situaci, kdy souběžné úlohy vyžadují souběžné čtení a úpravy datové struktury, souborového systému nebo databáze. Čtení neměnných dat může být prováděno současně mnoha úlohami, pokud však v tomto okamžiku dojde ke změnám dat, jejich paralelní čtení může vést k částečně upraveným datům, tedy k poškození dat [2] .
Řešení problému je asymetrické a spočívá v rozdělení zámku na čtení a zápis. Modifikace dat je povolena pouze výlučně, to znamená, že pouze jedna úloha může získat zámek zápisu najednou, pokud není získán zámek pro čtení. Čtení dat může být prováděno mnoha úlohami, takže libovolný počet úloh může získat zámek čtení současně, pokud není získán zámek pro zápis. To znamená, že zápis a čtení kritických sekcí nelze provádět paralelně, ale čtení kritických sekcí ano [2] .
Nejjednodušším implementačním algoritmem pro semafory a mutexy je použití binárního semaforového přepínače. Záznam musí být chráněn tímto semaforem. První úloha, která čte, musí zamknout semafor pomocí přepínače, čímž se zablokují vlákna pro zápis, a poslední úloha, která dokončí svou práci, musí semafor uvolnit, což umožní úlohám zápisu pokračovat ve své práci [5] . Tato implementace má však jeden závažný problém srovnatelný se zablokováním – nedostatek zdrojů při psaní úloh [6] .
Pseudokód pro jednoduchý algoritmus zámku pro čtení a zápisInicializace | Čtení úkol | Psací úkol |
---|---|---|
switch = Switch() oprávnění k zápisu = Semafor(1) | zámek (přepnout, povolení-zápis) // Kritická část úlohy čtení odemknout (přepnout, povolení-zápis) | zachytit (povolení k zápisu) // Kritická část úkolu psaní vydání (povolení k zápisu) |
Univerzální algoritmus, postrádající výše popsaný problém, zahrnuje binární semaforový přepínač A pro organizaci kritické části úloh čtení a turniket pro blokování nových úloh čtení v přítomnosti čekajících autorů. Když přijde první úloha ke čtení, zmocní se semaforu A pomocí přepínače a zabrání zápisu. Pro autory chrání semafor A kritickou část spisovatele, takže pokud jej zachytí čtenáři, všichni autoři zablokují vstup do své kritické části. Zachycení psacích úloh semaforu A a následné psaní je však chráněno turniketovým semaforem. Pokud tedy dojde k zablokování úlohy zápisu z důvodu přítomnosti čteček, turniket se zablokuje spolu s novými úlohami čtení. Jakmile poslední čtenář dokončí svou práci, přepínací semafor se uvolní a první zapisovač ve frontě se odblokuje. Na konci své práce uvolní turniketový semafor a opět umožní práci se čtením [7] .
Pseudokód algoritmu univerzálního zámku pro čtení a zápisInicializace | Čtení úkol | Psací úkol |
---|---|---|
switch = Switch() oprávnění k zápisu = Semafor(1) turniket = semafor(1) | zabavit (turniket) uvolnění (turniket) zámek (přepnout, povolení-zápis) // Kritická část úlohy čtení odemknout (přepnout, povolení-zápis) | zabavit (turniket) zachytit (povolení k zápisu) // Kritická část úkolu psaní pustit (turniket) vydání (povolení k zápisu) |
Na úrovni operačního systému existují implementace semaforů pro čtení a zápis, které jsou speciálním způsobem upraveny pro zvýšení efektivity při hromadném používání. Implementace zámků čtení a zápisu mohou být založeny na mutexech i spinlockech [4] .
Zatímco zámky čtení a zápisu mohou zlepšit rychlost některých algoritmů, mají skrytý problém, který nastává, když existuje jednotná hustota požadavků na čtení. V tomto případě může být získání zámku zápisu odloženo na neomezenou dobu, což způsobí nedostatek zdrojů při psaní úloh [4] . Nedostatek zdrojů u úloh zápisu je srovnatelný se zablokováním , protože zápis dat nebude možný, dokud přijdou nové úlohy čtení. V tomto případě nemusí být problém patrný, dokud není zatížení systému velmi vysoké, ale může se začít projevovat při zvýšení zatížení. Řešení lze zabudovat do implementace zámků čtení a zápisu a zahrnuje blokování jakýchkoli nových úloh čtení, pokud na zámek čeká alespoň jeden zapisovač [6] .
Koncept eskalace zámku umožňuje povýšit zachycený zámek čtení na výhradní zámek zápisu. Zámek je povýšen, když již nejsou žádné úlohy čtečky, jinak se úloha zablokuje, dokud úlohy čtečky neuvolní zámek. Tento koncept také umožňuje downgrade zámku pro zápis na zámek pro čtení [8] . Tento koncept je však často volitelný a nemusí být přítomen ve specifických implementacích.
Ve standardu POSIX jsou zámky čtení a zápisu reprezentovány typem pthread_rwlock_tv záhlaví souboru pthread.h. Zámkům mohou být přidělovány některé parametry prostřednictvím atributů, konkrétně zámek může být definován jako dostupný mezi procesy nebo pouze mezi vlákny a zámek dostupný mezi procesy je vyžadován standardem. Pokud neexistují žádné úlohy čtení, pořadí, ve kterém úlohy zápisu získávají zámek, je určeno vybranou zásadou plánovače. Priorita pořízení zámku mezi úkoly zapisovače a čtečky však není standardem definována [1] .
V API Windows jsou zámky reprezentovány strukturou SRWLOCKz hlavičkového souboru Synchapi.ha sadou funkcí pro práci s ní. Zámky jsou navrženy pro práci s vlákny v rámci jednoho procesu a není zaručeno, že žádná objednávka získá zámky. Z funkcí je podporováno použití zámku spolu s podmínkovou proměnnou prostřednictvím funkce SleepConditionVariableSRW()[9] .
Jazyk | Modul nebo knihovna | Datový typ |
---|---|---|
Xi | pthread | pthread_rwlock_t[jeden] |
C++ | std | std::shared_mutex[3] |
C# | System.Threading | ReaderWriterLock[deset] |
Jít | sync | RWMutex[jedenáct] |
Jáva | java.base,java.util.concurrent.locks | ReentrantReadWriteLock[12] |
Rez | std | std::sync::RwLock[13] |