Läs- och skrivlås är en synkroniseringsmekanism som tillåter samtidig allmän läsning av vissa delade data eller deras exklusiva modifiering, vilket avgränsar läs- och skrivlås mellan sig [1] . Mekanismen är utformad för att lösa det klassiska läsare-skribentproblemet , där något objekt läses och skrivs samtidigt av samtidiga uppgifter [2] .
Till skillnad från mutexes tar läs- och skrivlås separat hänsyn till att läsa data och separat skrivning, vilket tillåter åtkomst till data om de inte ändras vid den tidpunkten. Mutexes tillåter endast exklusiv åtkomst till data [1] . Det finns dock delade mutex som tillhandahåller, förutom det exklusiva låset, ett delat lås som tillåter delat ägande av mutexen om det inte finns någon exklusiv ägare [3] . I sin kärna är delade mutexes läs-skrivlås, men kallas mutexes.
I det allmänna fallet löser läs-skrivlås samma problem som mutexes och kan ersättas med dem, men anledningen till att läs-skrivlåsmekanismen dyker upp är att öka effektiviteten av ömsesidig uteslutning med separat läsning och skrivning [ 4] . Läs- och skrivlås föredras framför mutexes i de fall där data nås mycket oftare än det skrivs. I det här fallet kommer läsuppgifter inte blockera för det mesta, bara ibland blockeras när objektet ändras. Prioritering mellan skriv- och läsuppgifter ges ofta till skrivuppgifter för att undvika resurssvält på skrivuppgifter [1] .
Problemet med läsare och skribenter uppstår i alla situationer där samtidig läsning och modifiering av en datastruktur, filsystem eller databas krävs av samtidiga uppgifter. Läsning av oföränderlig data kan utföras samtidigt av många uppgifter, men om dataändringar inträffar vid denna tidpunkt kan deras parallellläsning leda till delvis modifierade data, det vill säga korrupta data [2] .
Lösningen på problemet är asymmetrisk och innebär uppdelning av låset i läs och skriv. Datamodifiering är endast tillåten exklusivt, det vill säga endast en uppgift kan förvärva ett skrivlås åt gången, om inte ett läslås förvärvas. Att läsa data kan utföras av många uppgifter, så så många uppgifter som önskas kan få ett läslås samtidigt, om inte ett skrivlås förvärvas. Det vill säga att skriva och läsa kritiska avsnitt kan inte utföras parallellt, men läsa kritiska avsnitt kan [2] .
Den enklaste implementeringsalgoritmen för semaforer och mutexes är att använda en binär semaforväxel. Posten måste skyddas av denna semafor. Den första uppgiften som läser måste låsa semaforen med en switch, blockera skrivtrådarna, och den sista uppgiften som avslutar sitt arbete måste släppa semaforen, vilket gör att skrivuppgifterna kan fortsätta sitt arbete [5] . Denna implementering har dock ett allvarligt problem som kan jämföras med dödläge - resurssvält av skrivuppgifter [6] .
Pseudokod för en enkel läs-skrivlåsalgoritmInitialisering | Läsuppgift | Skrivuppgift |
---|---|---|
switch = Switch() skrivbehörighet = Semafor(1) | lås (switch, permission-write) // Kritisk del av läsuppgiften låsa upp (switch, permission-write) | fånga (skrivbehörighet) // Kritisk del av skrivuppgiften release (skrivbehörighet) |
Den universella algoritmen, utan det ovan beskrivna problemet, inkluderar en binär semaforomkopplare A för att organisera en kritisk sektion av läsuppgifter och en vändkors för att blockera nya läsuppgifter i närvaro av väntande skribenter. När den första uppgiften att läsa kommer, griper den semafor A med en switch, vilket förhindrar skrivning. För författare skyddar semafor A den kritiska delen av författaren, så om den fångas av läsare blockerar alla författare att gå in i deras kritiska avsnitt. Emellertid skyddas infångningen av skribentuppgifter av semafor A och efterföljande skrivning av vändkors-semaforen. Därför, om en blockering av en skrivuppgift inträffar på grund av närvaron av läsare, blockeras vändkorset tillsammans med nya läsuppgifter. Så snart den sista läsaren avslutat sitt jobb släpps switch-semaforen och den första skribenten i kön avblockeras. I slutet av sitt arbete släpper den vändkorsets semafor, vilket återigen tillåter arbetet med läsuppgifter [7] .
Pseudokod för den universella läs- och skrivlåsalgoritmenInitialisering | Läsuppgift | Skrivuppgift |
---|---|---|
switch = Switch() skrivbehörighet = Semafor(1) vändkors = Semafor(1) | gripa (vändkors) släppa (vändkors) lås (switch, permission-write) // Kritisk del av läsuppgiften låsa upp (switch, permission-write) | gripa (vändkors) fånga (skrivbehörighet) // Kritisk del av skrivuppgiften släppa taget (vändkors) release (skrivbehörighet) |
På operativsystemnivå finns implementeringar av läs- och skrivsemaforer, som modifieras på ett speciellt sätt för att öka effektiviteten vid massanvändning. Implementeringar av läs- och skrivlås kan baseras på både mutexer och spinlocks [4] .
Medan läs- och skrivlås kan förbättra hastigheten för vissa algoritmer, har de ett dolt problem som dyker upp när det finns en enhetlig täthet av läsbegäranden. I det här fallet kan förvärvet av ett skrivlås försenas under obegränsade tidsperioder, vilket orsakar resurssvält på skrivuppgifter [4] . Resurssvälten för skribentuppgifter är jämförbar med ett dödläge , eftersom det kommer att vara omöjligt att skriva data medan nya läsuppgifter kommer. I det här fallet kanske problemet inte märks förrän belastningen på systemet är mycket hög, men kan börja visa sig när belastningen ökar. Lösningen kan byggas in i implementeringen av läs-skriv-lås och går ut på att blockera eventuella nya läsuppgifter om det finns minst en skribent som väntar på låset [6] .
Låseskaleringskonceptet gör att ett fångat läslås kan befordras till ett exklusivt skrivlås. Ett lås främjas när det inte finns fler läsaruppgifter, annars blockeras uppgiften tills läsaruppgifterna släpper låset. Konceptet tillåter även nedgradering av ett skrivlås till ett läslås [8] . Konceptet är dock ofta valfritt och behöver inte finnas i specifika implementeringar.
I POSIX -standarden representeras läs- och skrivlås av en typ pthread_rwlock_ti rubrikfilen pthread.h. Lås kan ges vissa parametrar genom attribut, i synnerhet kan ett lås definieras som tillgängligt mellan processer eller endast mellan trådar, och ett lås tillgängligt mellan processer krävs av standarden. Om det inte finns några läsuppgifter, bestäms ordningen i vilken skrivuppgifterna skaffar låset av den valda schemaläggarpolicyn. Låsförvärvsprioriteten mellan skribent- och läsaruppgifter definieras dock inte av standarden [1] .
I Windows API representeras lås av en struktur SRWLOCKfrån en rubrikfil Synchapi.hoch en uppsättning funktioner för att arbeta med den. Lås är utformade för att fungera med gängor inom en enda process, och ingen beställning garanteras för att få lås. Av funktionerna stöds användningen av ett lås tillsammans med en villkorsvariabel genom en funktion SleepConditionVariableSRW()[9] .
Språk | Modul eller bibliotek | Data typ |
---|---|---|
Xi | pthread | pthread_rwlock_t[ett] |
C++ | std | std::shared_mutex[3] |
C# | System.Threading | ReaderWriterLock[tio] |
gå | sync | RWMutex[elva] |
Java | java.base,java.util.concurrent.locks | ReentrantReadWriteLock[12] |
Rost | std | std::sync::RwLock[13] |