Az informatikában a lock, zár vagy mutex szinkronizációs primitív, ami többszálú környezetben szabályozza a megadott erőforráshoz való hozzáférést. A zár a kölcsönös kizárás érvényesítésére szolgál. Típusai A zár többnyire felügyelő zár, ami elvárja a szálak együttműködését. Egyes rendszerek kötelező zárakat is megvalósítanak, amik kivételt váltanak ki abban a szálban, ami az objektumhoz annak zárolt állapotában próbálnak hozzáférni.
A zár legegyszerűbb típusa a bináris szemafor. Kizárólagos hozzáférést biztosít a zárolt adathoz. Vannak sémák, amelyek lehetővé teszik a hozzáférést egyszerre több olvasó számára. További példák a kizáró, a kizárást lehetővé tevő és az upgrade-elhető.
Egy másik osztályozás azt veszi figyelembe, hogy mi történik, ha a zár megállít egy szálat. A legtöbb zár blokkolja a szál végrehajtását, ezért a szálnak várnia kell a futás folytatásával, amíg meg nem szerzi a zárat. Ez megvalósulhat úgy, hogy a szál újra és újra lekérdi, hogy megkapja-e a zárat. Ez akkor hatékony, ha a zárolás rövid ideig tart, mert az ütemezőnek nem kell az alvó száltól elvennie a vezérlést és odaadni egy másik szálnak, tehát adminisztrációt lehet spórolni. Viszont ha a zárolás hosszú, vagy a szál folytatása függ a zároló szál végrehajtásától, akkor nem jó ötlet.
Támogatása A zárak hardveres támogatást igényelnek ahhoz, hogy hatékonyan lehessen őket használni. Ez rendszerint egy vagy több atomi utasítás formájában valósul meg ("test-and-set", "fetch-and-add" vagy "compare-and-swap"). Ezek lehetővé teszik, hogy atomi lépésben lehessen ellenőrizni a zárat, és ha szabad, akkor lefoglalni.
Egyprocesszoros architektúrákon van lehetőség megszakíthatatlan utasítássorozatok definiálására. Ezt speciális utasításokkal vagy prefixekkel teszik lehetővé, ami viszont többprocesszoros architektúrákon nem áll rendelkezésre. Egy ilyen környezetben bonyolult a zárolás hardveres és szoftveres megvalósítása, és fontos szinkronizációs következményekkel jár.
Az atomiság a párhuzamosság miatt fontos, mivel az ütemező közben elveheti a vezérlést a száltól, és egy másik szál is elkezdheti az utasítássorozat végrehajtását, ami bajt okozhat. Például tekintsük a következő C-kódot:
if(lock == 0) { // lock free, set it lock = myPID; }
Ez a példa nem garantálja a zár megszerzését, hiszen egyszerre több szál is tesztelheti a feltételt, és megpróbálhatja beállítani az azonosítót, nem tudva a másikról. Dekker és Peterson algoritmusai segítenek áthidalni azt a problémát, amit az atomi zárolóművelet hiánya vet fel.
A zárak nem elég körültekintő használata holtpontot vagy livelockot okozhat. Különböző stratégiák vannak ezek elkerülésére vagy feloldására, akár tervezési, akár futásidőben. A leggyakoribb módszer az, hogy csak bizonyos sorrendben lehet lefoglalni az erőforrásokat.
Vannak nyelvek, amelyek beépítetten tartalmaznak zárolást. Az alábbi példa C#-ban:
class Account { // this is a monitor of an account long val = 0; object thisLock = new object(); public void deposit(const long x) { lock(thisLock) { // only one thread at a time may execute this statement val += x;
public void withdraw(const long x) { lock(thisLock) { // only one thread at a time may execute this statement val -= x;
A lock(this) problémát okozhat, ha a példány publikus.
A Javához hasonlóan C#-ban is lehet teljes metódusokat szinkronizálni a MethodImplOptions szinkronizációs attributum használatával. [MethodImpl(MethodImplOptions.Synchronized)] public void someMethod() { // do stuff
Szemcsézettség További fogalmak: zárolási adminisztráció: a zároláshoz használt extra műveletek és memória, például a zár létrehozásához és megszüntetéséhez, kezeléséhez használt idő. Minél több zárolást tartalmaz a program, annál több időt tölt a zárak kezelésével. kontentív zárolás: egy szál milyen gyakran fut bele olyan zárba, amit egy másik szál fog. Minél finomabban szemcsézett a zárolás, annál ritkábban történik ez meg. holtpont: egy olyan helyzet, ahol az összes szál vár legalább egy másikra. Beavatkozás nélkül ez a helyzet nem oldódik meg, örökké tartana, és a program örökre lefagyna.
Az adminisztráció és a kontentív működés között cserekapcsolat van. Azt mondjuk, hogy a zárak durván szemcsézettek, ha nagy kódszakaszokat és egész adatszerkezeteket zárolnak. Ha a zárak finoman szemcsézettek, akkor rövid kódszakaszokat, és az adatszerkezetekben egyes részeket lehet zárolni; sőt, például egy faszerkezetben keresve az egyes részfák külön-külön zárolhatók, így a nagyobb részfa zára a kisebb részfa zárának megszerzése után feloldható. Ebben az esetben több az adminisztráció. A durva szemcsézettség problémája viszont az, hogy kevéssé kontentív, a szálaknak sokat kell várakozniuk. Nem célszerű annyira finomítani a zárolást, hogy összetartozó objektumok zárát külön kelljen megszerezni, mert ez szükségtelen függőségeket vezet be a programba, és holtpontot okozhat.
Adatszerkezetekben és adatbázisokban kisebb-nagyobb elemek is zárolhatók igény szerint. Egész táblák zárolása kevés, adatrekordok és részeik zárolása sok szál esetén jobb. Adatbázisok Az adatbázisok zárolásának célja a tranzakciók szinkronizálása. Összefésüléses esetben kétfázisú zárak biztosítják, hogy a konkurensen futó tranzakciók végrehajtásának eredménye megegyezzen a tranzakciók bizonyos sorrendű futásával. Mellékhatásként azonban előfordulhat holtpont. A holtpont megelőzhető azzal, hogy a tranzakciók között zárolási sorrendet állítunk fel, vagy várakozási zárakkal deríthetők fel. Egy alternatíva a teljesen rendezett globális időpontok használata.
Különböző mechanizmusok támogatják az adatbázis konkurens használatát; a cél az, hogy megelőzzék a piszkos adatolvasást vagy a frissítések elvesztését. Pesszimista zárolás: Az olvasó kizárólagos zárat tesz az olvasott rekordra, hogy addig ne manipulálhassák mások. Ennek következményeként kevésbé lehet hozzáférni, aminek az a hátránya, hogyha sokáig tart a zárolás, akkor a műveletek lelassulnak, ami frusztrálja a felhasználókat. Optimista zárolás: Egyszerre többen férhetnek hozzá az adatbázis egyes részeihez. A rendszer másolatot készít az először olvasott adatokról. Ha valaki frissíteni akar egy rekordot, akkor az alkalmazás meghatározza, hogy más megváltoztatta-e a rekordot az utolsó olvasás óta. Ha igen, akkor a rendszer nem engedi a módosítást, és a felhasználónak újra kell kezdenie a tranzakciót. A szükséges zárolások számának csökkentésével javítja a performanciát, így az adatbázis szerver terhelését is. Akkor hatékony, ha csak ritkán kell frissíteni, de a frissítések elveszhetnek. Ha a frissítések nem olyan ritkák, akkor nem hatékony, mert gyakran kell újra elküldeni a kérést, ami szintén frusztrálja a felhasználókat, akik lehet, hogy inkább nem próbálkoznak többször.
A pesszimista zárolás hatékony, ha: egyszerre sok kérés érkezik, vagy a zárolás gyorsabb, mint a tranzakciók visszagörgetése. Elvárja, hogy a zárolások rövid ideig tartsanak, továbbá azt, hogy a kapcsolat ne szakadjon meg a felhasználók és az adatbázis között. Nem skálázódik a feldolgozott adatmenyiséggel arányosan, mert a zárolások hosszabb ideig tarthatnak. Webalkalmazások számára nem a legjobb.
Az optimista zárolás hatékony, ha: egyszerre kevés a kérés, vagy az adatokat sokkal inkább olvassák, mint írják. A .NET kiterjedten használja, mobil és nem csatlakozó alkalmazásokban, ahol a hosszas zárolás megengedhetetlen. Ezekben nem lehet feltételezni az állandó kapcsolatot az adatbázissal sem, amit a pesszimista zárolás megkövetelne.
Hátrányok A zárolásos adatvédelem és szinkronizáció több hátránnyal jár: Várakozás: a szálaknak néha várniuk kell. Ha a zároló szál blokkolódik, akkor sokáig kell várni, ha pedig elveszti a kapcsolatot az adatbázissal, vagy holtpontba fut, akkor az adathoz sosem lehet hozzáférni, hacsak nem lép valaki közbe. Adminisztrációs költség: a zárolás lelassítja a hozzáférést az adatokhoz, még akkor is, ha a várakozás ritka. Hibakeresés nehezebb: a zárolással összefüggő hibák nehezen vehetők észre és reprodukáltak, mert függenek az időzítéstől, ahogy a holtpont is. Instabilitás: a szemcsézettség optimális mérete függ a konkrét problémától, és a hozzáférés gyakoriságától. Érzékeny a tervezésre, megvalósításra, még az alacsony szintű architekturális változásoktól is. Az alkalmazás életciklusa alatt sokat változhat, ami felveti a módosíthatóság kérdését. Komponálhatóság: a zárak kombinálása csak bonyolult módon és merev szabályokhoz alkalmazkodva valósítható meg. Tehát bonyolult dolog lehetővé tenni azt, hogy egy tranzakció töröljön egy X itemet az A táblából, és ugyanezt az elemet beszúrja a B táblába. A prioritás megfordítása: egy sok adatot zároló alacsonyabb prioritású szál visszatarthat egy magasabb prioritású szálat, ha sorra jönnek a kettő közötti prioritású szálak. Ez kiküszöbölhető a prioritás öröklésével, vagy a prioritástető használatával. Ez a holtpontot is megelőzheti. Konvoj: a szálak mind egy szálra várnak, mert nekik is szükségük lenne arra az adatra, amit az a szál zárolt.
Vannak más módszerek is, például a kürtők vagy a szerializációs tokenek, amelyek kiküszöbölik a holtpont lehetőségét. Az alternatívák közé tartoznak a nem blokkoló programozási módszerek, mint a tranzakciós memória és a zárolásmentes technikák. Ezek azonban gyakran megkövetelik, hogy a zárolás alacsonyabb szinten implementálva legyen. Ezzel a fenti problémák nem kerülhetők el, csak a felső szinten nem kell zárolni.
A legtöbb esetben a zárolás konkrét mechanizmusa függ attól, hogy a processzor nyújt metódust az atomi utasítássorozat szinkronizációjára. Például ha egy csővezetékből itemeket törölnek, akkor fel kell függeszteni a többi műveletet, ami konkurensen fut, és ehhez a csővezetékhez hozzáadna vagy elvenne elemeket. Így az alkalmazás robusztusabb lehet, ha felismeri az operációs rendszer korlátait, és képes felismerni a lehetetlen kéréseket. A komponálhatóság hiánya A zárolás egy fontos problémája az, hogy a zárak nem komponálhatók; nehéz komponálni úgy a modulokat, hogy nem nyúlunk beléjük, vagy legalábbis nem ismerjük a tartalmukat. Simon Peyton Jones, a tranzakcionális memória egyik szószólója a következő példát adja egy banki alkalmazásra: Tervezzük meg az Account (Számla) osztályt úgy, hogy lehessen pénzt betenni, kivenni, és átutalni. Az első rész megoldása:
class Account: member balance : Integer member mutex : Lock method deposit(n : Integer) mutex.lock() balance ← balance + n mutex.unlock() method withdraw(n : Integer) deposit(−n)
A megoldás másik része sokkal nehezebb. Az átutalás szekvenciális esetben ilyen lenne:
function transfer(from : Account, to : Account, amount : integer) from.withdraw(amount) to.deposit(amount)
Konkurens esetben ez a megvalósítás hibás, mivel ha az első utasítás után elveszik a vezérlést az aktuális száltól, és egy másiknak adják, akkor pénz tűnhet el vagy jelenhet meg. Zárolni kell, a megfelelő sorrendben:
function transfer(from : Account, to : Account, amount : integer) if from < to // arbitrary ordering on the locks from.lock() to.lock() else to.lock() from.lock() from.withdraw(amount) to.deposit(amount) from.unlock() to.unlock()
Még bonyolultabb műveletek esetén még több zár szerepelhet, és a műveletnek mindegyikről tudnia kell, tehát nem maradhatnak rejtve.
Nyelvi támogatás A különböző nyelvek különbözőképpen támogatják a szinkronizációt: Az ISO/IEC C szabvány szabványos kölcsönös kizárást biztosít a C11 óta. A jelenlegi ISO/IEC C++ szabvány a C++11 óta biztosít konkurens programozást Az OpenMP szabvány szerint a kritiokus szakaszok pragmákkal jelölhetők meg; ezt több fordító támogatja. A POSIX pthread API támogatja a zárolást. A Visual C++ a synchronized attribútumot vezeti be, de ez specifikus a COM objektumok, a Windows API és a Visual C++ számára. A C és a C++ könnyen hozzáfér az operációs rendszerek zárolási lehetőségeihez.