Zkratka proMutual ExclusionmjútexTakéMutual exclusion lock, Zámek, Lock, Mutexový zámekPokročilý

Definice

Mutex je synchronizační primitivum, které zaručuje, že do chráněné sekce kódu vstoupí v jednu chvíli nejvýše jedno vlákno. Vlákno zámek zamkne, provede práci nad sdílenými daty a zámek odemkne; ostatní vlákna mezitím čekají. Název vznikl zkrácením anglického mutual exclusion, tedy vzájemné vyloučení přístupu ke sdílenému prostředku.

Kategorie: Softwarový vývojAktualizováno

Nezaměňujte: V programování jde o synchronizační zámek vláken, zatímco v databázích se podobný mechanismus označuje jako zamykání řádků či transakční zámek s jinou sémantikou.

Proč vzájemné vyloučení vůbec potřebujeme

Operace, která vypadá v jazyce jako jeden příkaz, se na úrovni procesoru rozpadá na několik kroků. Typické counter++ znamená načíst hodnotu z paměti, přičíst jedničku a zapsat zpět. Když stejnou trojici provádějí dvě vlákna současně, mohou obě načíst stejnou původní hodnotu a jeden z přírůstků se ztratí. Přesně tomuhle se říká race condition a mutex je nejběžnější obrana proti ní.

Úsek kódu, který musí proběhnout bez souběhu, se nazývá kritická sekce. Mutex kolem něj vytvoří pomyslné turniketové dveře: kdo je zamkne, projde, ostatní stojí frontu, dokud nebude odemčeno.

Co se děje při zamykání

Zamčení se opírá o atomickou instrukci procesoru, například compare-and-swap. Ta v jednom nedělitelném kroku zkontroluje, jestli je zámek volný, a pokud ano, označí ho za obsazený. Žádné jiné jádro se mezi kontrolu a zápis nevejde.

Pokud je zámek obsazený, běhové prostředí obvykle vlákno uspí a plánovač mu odebere procesor. Uspání a probuzení stojí řádově mikrosekundy, což je u velmi krátkých kritických sekcí drahé. Proto některé implementace nejdřív krátce aktivně čekají v cyklu (spin) a teprve pak vlákno uspí.

Vlastnictví a co z něj plyne

Klasický mutex má vlastníka: odemknout ho smí jen to vlákno, které ho zamklo. Tím se liší od binárního semaforu, který může uvolnit kdokoli. Vlastnictví umožňuje jádru řešit inverzi priorit a detekovat pokus o dvojí zamčení stejným vláknem. Rekurzivní mutex naopak dovolí témuž vláknu zamknout se vícekrát, ale musí stejněkrát odemknout.

Uváznutí a jak se mu vyhnout

Nejnepříjemnější selhání je deadlock: vlákno A drží zámek 1 a čeká na zámek 2, vlákno B to má obráceně. Program se zastaví bez chybové hlášky a bez vytížení procesoru. Osvědčená prevence je globální pořadí zámků, tedy pravidlo, že se zámky nabírají vždy ve stejném pořadí, a držení zámku po co nejkratší dobu. Uvnitř kritické sekce se nikdy nemá volat síťová operace, čekání na disk ani cizí callback.

Kdy mutex není správný nástroj

Pro čítač stačí atomická proměnná, která je výrazně levnější. Pro scénář „mnoho čtenářů, málo zapisovatelů“ je vhodnější read-write zámek. Když je sporů o zámek tolik, že vlákna tráví většinu času čekáním, bývá lepší návrh přepsat tak, aby si každé vlákno drželo vlastní data a slučovalo je až na konci.

Podpora v jazycích

C má pthread_mutex_t, C++ std::mutex se strážcem std::lock_guard, Java synchronized a ReentrantLock, Go typ sync.Mutex, Rust Mutex<T>, kde data jdou přečíst jen skrz zamčený zámek. JavaScript v jednom vlákně mutex nepotřebuje; s SharedArrayBuffer a Atomics už ano.

Příklady z praxe

  1. Ztracený přírůstek u sdíleného čítače

    Deset goroutin v Go zvyšuje společný čítač milionkrát. Bez zámku vyjde výsledek pokaždé jinak a nižší než očekávaných deset milionů, protože se souběžné zápisy přepisují. Obalení kritické sekce mutexem výsledek stabilizuje.

    var (
    	mu      sync.Mutex
    	counter int
    )
    
    func increment() {
    	mu.Lock()
    	defer mu.Unlock()
    	counter++
    }
  2. Deadlock při převodu mezi účty

    Funkce převodu zamkne nejdřív zdrojový a pak cílový účet. Když jedno vlákno převádí z A na B a druhé současně z B na A, obě se navzájem zablokují a aplikace zamrzne. Řešením je nabírat zámky vždy v pevném pořadí, typicky podle ID účtu.

    void transfer(Account& from, Account& to, int amount) {
        Account* first  = from.id < to.id ? &from : &to;
        Account* second = from.id < to.id ? &to : &from;
        std::lock_guard<std::mutex> l1(first->m);
        std::lock_guard<std::mutex> l2(second->m);
        from.balance -= amount;
        to.balance   += amount;
    }

Časté omyly

MýtusKdyž dám mutex kolem všeho, program bude bezpečný.
Ve skutečnostiHrubé zamykání sice odstraní souběh, ale program tím fakticky převede zpět na jednovláknový a výkon vícejádrového procesoru zahodí. Navíc více zámků držených najednou zvyšuje riziko uváznutí.
MýtusMutex a semafor jsou totéž.
Ve skutečnostiMutex má vlastníka a odemyká ho jen zamykající vlákno, zatímco semafor je čítač povolení, který smí uvolnit i jiné vlákno. Semafor se hodí na omezení počtu souběžných přístupů, mutex na ochranu jedné kritické sekce.
MýtusZamykání je nutné jen tam, kde se zapisuje.
Ve skutečnostiČtení hodnoty, kterou jiné vlákno současně mění, může vrátit rozpracovaný nebo zcela neplatný stav, zvlášť u struktur o více polích. Bez synchronizace navíc kompilátor a procesor smějí operace přeuspořádat.

Časté dotazy

Jak poznám, že aplikace uvázla na mutexu?
Uváznutí na mutexu se pozná podle toho, že proces běží, ale nespotřebovává procesorový čas a přestal odbavovat požadavky. Výpis zásobníků všech vláken ukáže, že několik z nich stojí ve funkci pro zamykání. V Javě pomůže jstack nebo jcmd, v Go se při hlídaném uváznutí vypíše přehled goroutin, v C a C++ se používá gdb s výpisem všech vláken. Sanitizery jako ThreadSanitizer navíc dokážou nahlásit porušené pořadí zámků ještě dřív, než k uváznutí reálně dojde.
Kolik stojí zamčení mutexu v praxi?
Zamčení nesporného mutexu je levné, řádově desítky nanosekund, protože se odehraje jedinou atomickou instrukcí bez vstupu do jádra operačního systému. Cena roste až při sporu, kdy se vlákno musí uspat a později probudit; to už jsou jednotky až desítky mikrosekund plus ztráta obsahu cache. Právě proto se doporučuje držet kritickou sekci krátkou a nikdy v ní nečekat na disk nebo síť. Přesná čísla se liší podle platformy, takže se vyplatí měřit.
Potřebuji mutex v Node.js nebo v prohlížeči?
V běžném JavaScriptu mutex potřeba není, protože kód běží v jediném vlákně a funkce se nepřeruší uprostřed synchronního bloku. Situace se mění u Web Workers a Node.js worker_threads, které sdílejí paměť přes SharedArrayBuffer; tam už je synchronizace pomocí objektu Atomics namístě. Samostatnou kapitolou jsou logické zámky nad asynchronními operacemi: když se dvě promises snaží obnovit stejný přístupový token, hodí se jednoduchý zámek v aplikační logice, i když skutečný mutex operačního systému to není.
Čím nahradit mutex, když je sporů o zámek příliš mnoho?
Při silné kontenci se nejdřív zkouší zmenšit chráněná data: rozdělit jeden velký zámek na několik menších podle klíče, takzvané sharding zámků. Pro prosté čítače stačí atomické operace, pro převažující čtení read-write zámek nebo strategie copy-on-write. Nejúčinnější bývá změna návrhu: každé vlákno pracuje nad vlastní kopií dat a výsledky se slučují až na konci, případně se přejde na předávání zpráv místo sdílené paměti. Bezzámkové datové struktury jsou možnost, ale jejich správná implementace je náročná.

Zdroje

  1. pthread_mutex_lock(3) - Linux manual page(otevře se v novém okně)The Linux man-pages project
  2. sync package - Go standard library(otevře se v novém okně)Wikipedia
  3. Synchronized Methods - The Java Tutorials(otevře se v novém okně)Oracle
  4. threading - Thread-based parallelism(otevře se v novém okně)Python Software Foundation
  5. Synchronization (Windows)(otevře se v novém okně)Microsoft

Související pojmy

Potřebujete to vyřešit v praxi?

Poradíme, jak na to ve vašem projektu

Vysvětlit pojem je jedna věc, navrhnout kolem něj funkční řešení druhá. Ozvěte se a probereme, co dává smysl u vás.