A Digitalmars D programozási nyelv

Párhuzamosság

A D nyelvi szinten támogatja a párhuzamos végrehajtást, és szabványos könyvtárat, kulcsszavakat biztosít a végrehajtási szálak kezelésére.

Adatok megosztása

A D nyelv tervezésekor a párhuzamosság biztosítása mellett alapvető fontosságú volt a párhuzamos programok könnyű, ugyanakkor biztonságos használatának a biztosítása. A több szál által közösen használt változók kezelése, a különböző szálak által látott állapotok összhangban tartása komoly feladat. Ráadásul az osztott adatokkal bonyolult, követhetetlen dolgok történhetnek, ha a programozó nem köti magát szigorú szabályokhoz a használatuk során.
Ezért a nyelv tervezői azt a döntést hozták, hogy alapértelmezetten minden változó, még a globálisak is szálszintűek lesznek. Ha egy változót a folyamat szintjén szeretnénk deklarálni, azaz azt akarjuk, hogy több szál is ugyanazt a példányát használja, akkor ezt a shared módosítóval explicit jelölnünk kell.

int perThread; shared int perProcess;

A fenti első definíció egy szálszintű változót definiál, ezáltal biztosítva, hogy soha nem kerülhet sor a változóhoz való konkurens hozzáférésre. Az ilyen tárolást Thread-Local Storage (TLS) néven emlegetik. A második definíció által létrehozott változónak egyetlen példánya lesz, mely komoly odafigyelést kíván a végrehajtás alatt, hogy a konkurens hozzáférések szikronizáltak legyenek.
Bár a nyelv biztosítja az osztott változókon végzett műveletek szinkronizáltságát, nem adhat garanciát a magasabb szintű invariánsok megtartására.
Természetesen konstansokat és immutable változókat nem szükséges a shared módosítóval ellátni ahhoz, hogy a párhuzamos szálak is láthasság. Mivel az ilyen változók használata során biztosított, hogy ne változzon meg az értékük, tehát minden szál csak olvashatja azokat, nincs szükség a konkurens hozzáférések szinkronizáltságának biztosítására. Így az állandó értékkel rendelkező változók mindenhol olvashatók, ahol láthatók. Ezt nevezik immutable sharingnek.

Azonban a közösen használt változókon keresztüli kommunikáció sok problémát okozhat. A párhuzamosan futó szálak közti kommunikáció sokkal biztonságosabb módja az üzenetek használata. Amikor az egymástól elkülönítetten működő szálak aszinkron üzenetek küldésével léphetnek egymással kapcsolatba. Sok nyelv ezt az elvet használja a szálak közti kommunikáció biztosítására, és a D-ben is ez az ajánlott megoldás.

Szálak indítása

A konkurens működéssel kapcsolatos alapvető függvények és adatszerkezetek a std.concurrency modulban találhatók. Új szál indítására a spawn függvény használható.

import std.concurrency, std.stdio; void main() { auto low = 0, high = 100; spawn(&fun, low, high); foreach (i; low .. high) { writeln("Main thread: ", i); } } void fun(int low, int high) { foreach (i; low .. high) { writeln("Secondary thread: ", i); } }

A spawn függvény első paraméterének egy függvény, melyet a hívó szállal párhuzamosan hajt végre a rendszer. További paraméterei a párhuzamosan végrehajtandó függvény paraméterei, melyeket a spawn átad a függvénynek.
A fenti példában a fő szál és a létrehozott szál 100 sort fog konkurensen a képernyőre írni.

Az immutable sharing szemléletésére nézzük a fenti példa kicsit módosított változatát:

import std.concurrency, std.stdio; void main() { auto low = 0, high = 100; auto message = "Yeah, hi #"; spawn(&fun, message, low, high); foreach (i; low .. high) { writeln("Main thread: ", message, i); } } void fun(string text, int low, int high) { foreach (i; low .. high) { writeln("Secondary thread: ", text, i); } }

A fun függvény text paramétere referencia szerint kerül átadásra, ugyanis a karater-láncokat a C-hez hasonlóan tömbök tárolják. Azonban a main függvényben a message változó típusa string lesz, ami egy alias az immutable char[] típusra. Tehát a szöveg nem változtatható meg, így bár közösen használja a két szál, nem kell semmilyen implicit szinkronizációt biztosítani a változóra.

Explicit Threadek

A szálakat, más nyelvekhez hasonlóan, Thread objektumok reprezentálják, melyek osztálya a core.thread modulban van definiálva. Az ebből származtatott osztályok a Javához nagyon hasonló módon képesek a konkurens működésre. Azonban nem biztosítják a csatornákon keresztüli aszinkron kommunikáció lehetőségét. Viszont a fordító nem ellenőrzi a változók használatát, így kicselezhetők a közösen használt változókra vonatkozó megkötések.
Bár a Threadek explicit használata nagy szabadságot biztosít a programozónak, a változók konkurens használatára a nyelv által garantált biztonság elvész, amely nagy ár lehet egy kis kényelemért.

Kommunikáció üzenetekkel

A fenti példában a két szál véletlenszerű sorrendben ír a képernyőre. Ha azt szeretnénk, hogy felváltva tegyék ezt meg, akkor a szinkronizálásuk szükséges. Ezt üzenetek küldésével valósíthatjuk meg.
Minden szál rendelkezik egy Tid típusú leíróval, melynek ismeretében üzeneteket küldhetünk az adott szálnak a send függvény segítségével. A send paramétereiként megadott értékek egy üzenetként kerülnek elküldésre. Üzeneteket fogadni a receive és receiveOnly függvényekkel lehet, melyek blokkolják a végrehajtást, amíg nem érkezik egy üzenet. Először használjuk a receiveOnly függvényt, mely megadott típusú üzenet fogadására alkalmas. Ha több értéket is várunk egy üzenetben, akkor azt a receiveOnly egy n-esben (Tuple) adja meg, ahogy a következő példában is látható.

import std.concurrency, std.stdio; void main() { auto low = 0, high = 100; auto tid = spawn(&writer); foreach (i; low .. high) { writeln("Main thread: ", i); tid.send(thisTid, i); enforce(receiveOnly!Tid() == tid); } } void writer() { for (;;) { auto msg = receiveOnly!(Tid, int)(); writeln("Secondary thread: ", msg[1]); msg[0].send(thisTid); } }

A fenti program már sorban újra ki a számokat 100-ig, először a fő szál, majd a spawnnal létrehozott.

Ha a receiveOnly hívása alatt nem a megadott típusú üzenet érkezik, akkor MessageMismatch kivétel váltódik ki.

Mintaillesztés

A receiveOnly használata esetén tehát mindig tudnunk kell, hogy milyen típusú üzenet fog érkezni, különben kivételt dob a függvény. Ez a gyakorlatban igen komoly megszorítás, hiszen sokszor csak azt lehet tudni, hogy milyen típusú üzenetek várhatók, de hogy pontosan melyik fog legközelebb érkezni, az nem. Az ilyen esetekben használható a receive függvény, melynek paraméterei függvények. A függvények paraméterei határozzák meg, hogy milyen típusú üzenetet képes feldolgozni az adott függvény.

receive( (string s) { writeln("Got a string with value ", s); }, (int x) { writeln("Got an int with value ", x); } );

A fenti receive-hívás képes string és int típusú üzenetet is fogadni, és minden esetben a megfelelő függvény törzsét hajtja végre az üzenet tartalmát paraméterként átadva neki.
Fontos megjegyezni, hogy a receive addig vár, amíg olyan üzenet nem érkezik, amelyet valamely paramétere fel tud dolgozni. Ha közben más típusú üzenetek is befutnak, azok a postaládát reprezentáló sorban maradnak.

Természetesen nem csak függvény-literállal adhatók meg a receive paraméterei, hanem egy függvény címét is átadhatjuk.

void handleString(string s) { { writeln("Got a string with value ", s); } receive( &handleString, (int x) { writeln("Got an int with value ", x); } );

Mivel a send függvénnyel több értéket is küldhetünk egyetlen üzenetként, a receiveOnly-hoz hasonlóan, a receive függvénnyel is képesek vagyunk ezen üzenetek fogadására. Az üzenet egyes értékei a feldolgozó függvény egyes paraméterei lehetnek, vagy egy n-est is megadhatunk, ahogyan azt a receiveOnly esetén már láttuk. Így a következő két üzenetfogadás ugyanazon üzeneteket képes feldolgozni:

receive( (long x, double y) { ... }, (int x) { ... } ); receive( (Tuple!(long, double) tp) { ... }, (int x) { ... } );

Ha egy üzenet típusa több mintának is megfelelne, akkor a receive fordítása sikertelen lesz. A következő kódrészletben például egy int értéket már az első függvény is képes feldolgozni, így a harmadik függvény nem lesz elérhető. Ilyen esetekben fordítási hibát kapunk.

receive( (long x) { ... }, (string x) { ... }, (int x) { ... } ); // Compile-time error, the third pattern is not reachable

Ahogy fentebb említettük, a receive azon üzeneteket, melyeket egyik paramétere sem képes fogadni, a postaládában hagyja. Ez viszont a postaláda telítődését okozhatja fölösleges üzenetekkel. Ennek kivédésére az std.variant modulban lévő Variant típusú minta használható. A Variant egy olyan dinamikus típus, mely pontosan egy más típus értékét tárolhatja. Így egy ilyen paramétert fogadó függvényt a lista végére helyezve biztosítható, hogy minden beérkezett üzenetet kivegyünk a postaládából.

receive( (long x) { ... }, (string x) { ... }, (double x, double y) { ... }, ... (Variant any) { ... } );

A receive másik problémája, hogy ha nem érkezik egyetlen üzenet sem, akkor sosem tér vissza. Ennek kiküszöbölésére használható a receiveTimeout függvény, melynek első paramétere az az idő ezredmásodpercben, amennyit legfeljebb várakozhat feldolgozható üzenet érkezésére. A függvény egy logikai értékkel tér vissza, mely megadja, hogy sikerült-e fogadni egy üzenetet.

auto gotMessage = receiveTimeout ( 1000, // Time in milliseconds (string s) { writeln("Got a string with value ", s); }, (int x) { writeln("Got an int with value ", x); } ); if (!gotMessage) { stderr.writeln("Timed out after one second."); }

Szálak terminálása

Nézzük a következő példakódot, mely egy adatfolyamot másol. A fő szál olvassa a folyamot, a beolvasott darabot átküldi a másik szálnak, amely kiírja azt.

import std.algorithm, std.concurrency, std.stdio; void main() { enum bufferSize = 1024 * 100; auto tid = spawn(&fileWriter); // Read Loop foreach (immutable(ubyte)[] buffer; stdin.byChunk(bufferSize)) { send(tid, buffer); } } void fileWriter() { // Write Loop for (;;) { auto buffer = receiveOnly!(immutable(ubyte)[])(); tgt.write(buffer); } }

Kérdés, hogy a fenti programban mi történik, amikor a főszál kilép a foreach-ciklusból.
Minden egyes szálnak van egy tulajdonosa, amely alapértelmezetten a szülője, az a szál amely létrehozta. Azonban ezt később meg lehet változtatni a setOwner(tid) hívással. Amikor egy szál üzenetet akar fogadni, és a tulajdonosa terminált, akkor a receive vagy receiveOnly egy OwnerTerminated kivételt vált ki. Természetesen ha még vannak feldolgozatlan üzenetek, akkor először azokat kiolvassa. Csak akkor váltódik ki az OwnerTerminated kivétel, ha már üres a postaláda, és blokkolódna az üzenetfogadás. Az OwnerTerminated kivétel "elkapására" lehetőség van a receive egy paramétere segítségével is. Így az előző programrészlet fileWriter függvénye a következő módon alakítható át úgy, hogy a kiíró szál is rendesen termináljon.

// Ends without an exception void fileWriter( ) { // Write Loop for (bool running = true; running; ) { receive( (immutable(ubyte)[] buffer) { tgt.write(buffer); }, (OwnerTerminated) { running = false; } ); } stderr.writeln("Normally terminated."); }

Soron kívüli üzenetküldés

Képzeljük el, hogy a fenti fájlmásoló kódrészlet egy gyors lokális tárolóról olvas és egy lassú hálózati meghajtóra ír. Ekkor az üzenetek feltorlódhatnak, mivel az írás sokkal lassabban halad az olvasásnál. Tegyük fel, hogy ekkor az olvasás valamiért meghiúsul, és az olvasó szál kivétellel elszáll. Ilyen esetben nyilván célszerű lenne erről azonnal értesíteni az író szálat is, hiszen felesleges a még ki nem írt adatokkal tovább foglalkoznia.

Ilyen soron kívüli üzenetek küldésére szolgál a prioritySend függvény, melynek paraméterezése a send függvényével egyezik meg, különbség csak a fogadó oldalán van. Ha egy olyan T típusú üzenet érkezik, melyet prioritySend függvénnyel küldtek, akkor az azonnal a fogadó postaládájának elejére kerül, és a következő történik:

A prioritySend függvény bármikor használható, amikor azt szeretnénk, hogy a címzett által megkapott következő üzenet az éppen elküldött legyen.
Visszatérve a kiindulási példához, ahol az olvasó szál kivétellel elszállt. Ilyen esetben nem kell a programozónak felkészülni minden kivétel explicit propagálására. Egy szál terminálását okozó lekezeletlen kivétel esetén minden szálnak, melynek tulajdonosa volt a terminált szál, egy OwnerFailed kivétel kerül elküldésre a prioritySend függvény segítéségével.

A postaláda mérete

Minden szálnak korlátozott méretű postaláda áll rendelkezésére. A postaláda mérete változtatható az std.concurrency modul void setMaxMailboxSize(Tid tid, size_t messages, bool(Tid) onCrowdingDoThis) függvényével. Az utolsó paraméterként megadott függvény hívódik meg, ha egy üzenet érkezik, de az már nem fér be a postaládába. Ha a függvény false értékkel tér vissza, akkor az üzenetet a rendszer eldobja, különben újra ellenőrzi a postaládát, és ha belefér az üzenet, akkor beteszi, egyébként újra meghívja a függvényt. Fontos, hogy ez a függvény a küldő oldalán hívódik meg. Hiszen rossz döntés lenne az amúgy is terhelt fogadóra bízni még ennek a végrehajtását is. Belegondolva, ha a fogadó hajtaná végre a függvényt, akkor biztos, hogy közben nem tudna kivenni semmit a postaládájából.

Van néhány előre definiált tevékenység az std.concurrency modulban. Ezeket a következő felsorolás tartalmazza: enum OnCrowding { block, throwException, ignore }.

A shared módosító

Ahogy a fejezet bevezető részében láttuk, a D nyelvben alapértelmezetten a deklarált változók a szálok lokális változói, azokat nem érheti el más szál. Ahhoz, hogy egy változót a szálak közösen használjanak a shared módosítóval kell deklarálni.

A következő példában definiálunk egy modulszintű közös változót, melyet így minden szál láthat, tehát gyakorlatilag egy C-beli globális változó megfelelője lesz. Definiálunk egy függvényt is, mely megpróbálja ezt a változót megnövelni.

shared uint threadsCount; void bumpThreadsCount() { ++threadsCount; //Error! Cannot increment a shared int }

A fenti függvény nem fog lefordulni, mert egy megosztott változón nem-atomi műveletet akar elvégezni. A közösen használt változókon csak atomi műveletek végezhetők, ezzel biztosítva a hozzáférések szinkronizáltságát. Atomi műveleteket biztosít a core.atomic modul. A fenti kódrészt a következő módon javíthatjuk ki:

import core.atomic; shared uint threadsCount; void bumpThreadsCount() { // core.atomic defines // atomicOp(string op)(ref shared uint, int) atomicOp!"+="(threadsCount, 1); // Fine }

Fontos megjegyezni, hogy a shared tulajdonság tranzitív. Ha egy adatszerkezetet ellátunk vele, akkor minden része is az lesz. Ha egy mutatót sharedként deklarálunk, akkor a hivatkozott entitás is az lesz.

A közösen használt értékeken végzett műveletek atomicitását a nyelv biztosítja és megköveteli. Ezáltal az osztott adatok szekvenciális konzisztenciáját biztosítja.
Egyszerű típusok (kivéve a real, aminek tárolása platform-függő, és általában több gépi szót igényel) olvasása és írása atomi művelet, a rajtuk elvégzendő összetettebb műveletekhez pedig a core.atomic modul által biztosított műveletek segítségével van atomi módon lehetőség. A felhasználói típusok bonyolultabb műveletei, például egy osztály tagfüggvényei csak külön jelzéssel tehető atomivá.

Szinkronizált osztályok

A párhuzamos programok készítése során nagyon elterjedt megoldás a zároláson alapuló szinkronizáció annak biztosítására, hogy mindig csak egy szál tartózkodhasson egy kritikus szakaszban. Ezt a közösen használt adatokat őrző mutexek segítségével biztosított kölcsönös kizárással érik el. Mikor egy szál be akar lépni a kritikus szakaszba, akkor megszerzi a mutexet, ha az éppen másnál van, akkor blokkolódik, amíg elérhetővé nem válik. Majd a kritikus szakaszból kilépve elengedi a mutexet, így lehetőséget adva a többi szálnak is a megszerzésére.

Tipikus példa a szinkronizáció szükségességére egy számlát reprezentáló objektum, melyhez mindig csak egyetlen szál férhet hozzá az interferencia elkerülése miatt.

import std.exception; // Single-threaded bank account class BankAccount { private double _balance; void deposit(double amount) { _balance += amount; } void withdraw(double amount) { enforce(_balance >= amount); _balance -= amount; } @property double balance() { return _balance; } }

A fenti osztály csak egyszálú környezetben működik megbízhatóan, mivel az egyes függvényekhez egymást átfedve, nem-atomi módon is hozzáférhetnek a szálak. Éppen ezért egy ilyen osztályból létrehozott shared változó egyik tagfüggvénye sem hívható meg. A kölcsönös kizárás biztosításához szükség lenne mutexek használatára a függvényeken belül, ezzel biztosítva a hozzáférések szinkronizáltságát. Például a következő pseudo-kód már biztonságos megvalósítást nyújt többszálú környezetben is.

// This is not D code // Multithreaded bank account in a language with explicit mutexes class BankAccount { private double _balance; private Mutex _guard; void deposit(double amount) { _guard.lock(); _balance += amount; _guard.unlock(); } void withdraw(double amount) { _guard.lock(); try { enforce(_balance >= amount); _balance -= amount; } finally { _guard.unlock(); } } @property double balance() { _guard.lock(); double result = _balance; _guard.unlock(); return result; } }

A mutexek explicit használata fárasztó, és sok lehetőséget ad a hibázásra, ugyanakkor nagyon mechanikus feladat, melyet a fordító program is el tud végezni. Ezt kihasználva például a Javában a synchronized kulcsszóval megjelölt metódusokra a fordító kölcsönös kizárást biztosít.

// Java version of an inter-Locked bank account using // automated scoped Locking with the synchronized statement class BankAccount { private double _balance; public synchronized void deposit(double amount) { _balance += amount; } public synchronized void withdraw(double amount) { enforce(_balance >= amount); _balance -= amount; } public synchronized double balance() { return _balance; }

A synchronized kulcsszó használatával a kód könnyen átlátható marad, ugyanakkor teljesen biztonságosan használható többszálú környezetben is. A tagfüggvények szintjén biztosított kölcsönös kizárást a D nyelv is támogatja, szintén a synchronized kulcsszóval. Így a fenti kód lényegében D nyelven is lefordítható.

Ugyanakkor egy osztályon belül szinkronizált és szinkronizálatlan függvényeket vegyesen használni nagyon veszélyes dolog, hiszen a szinkronizálatlan függvényeken keresztül olyan adatokhoz való hozzáférésre is lehetőség nyílhat, melyeket szinkronizált módon kellene elérni. Ezért a biztonságos használathoz célszerű vagy minden tagfüggvényt szinkronizáltan definiálni, vagy egyiket sem.
Viszont ha mindet szinkronizáltnak akarjuk definiálni, akkor egyrészt nagyon kényelmetlen minden egyes függvény elé odaírni a synchronized kulcsszót, másrészt véletlenül ki is felejthetünk egy-két függvényt. Ezért a D lehetőséget biztosít egész osztályok szinkronizált definiálására.

// D interlocked bank account using a synchronized ciass synchronized class BankAccount { private double _balance; void deposit(double amount) { _balance += amount; } void withdraw(double amount) { enforce(_balance >= amount); _balance -= amount; } double balance() { return _balance; } }

A fenti osztály minden függvénye szinkronizált, így nincs lehetőség a védett adathoz, a _balance változóhoz, szinkronizálatlanul hozzáférni, még véletlenül sem.

A szinkronizált osztályok esetén fontos az adatokhoz való hozzáférés szigorú ellenőrzése, ezért a láthatósági szinteket a szinkronizált osztályok esetén nem modul-, hanem osztály-szinten veszi figyelembe a fordító. (A védelmi szinteket lásd itt.)
A szinkronizált osztályokra a következő speciális szabályok érvényesek:

A szinkronizált osztályok az adatmezőket védik a többszörös hozzáférések ellen. Ez a védelem azon kívül, hogy egyszerre az osztálynak csak egy függvénye lehet végrehajtás alatt, a következőt is jelenti: A szinkronizált osztályok függvényei nem juttathatják ki az osztály adatainak referenciáit a külvilágba, hiszen akkor kívülről bárki hozzáférhetne az adatmezőkhöz. Ilyen formán biztosítottnak tűnik, hogy az osztály adatait csak a szinkronizált osztály függvényei érjék el. Azonban fontos megjegyezni, hogy lehetnek olyan adattagok, melyeket az osztály referencián keresztül ér el. Ha az ilyen adattagokat az osztály kívülről kapta, például a konstruktorán keresztül, akkor ezen entitásokat elérhetik az osztályon kívül is, amire az osztálynak nincs ráhatása. Vagyis a kívülről szerzett referenciák esetén csak annyi biztosítható, hogy azt az osztályon belül egyszerre csak egy szál érheti el az osztály függvényein keresztül.

Szinkronizált adattagok

A fenti probléma kiküszöbölésére a D nyelvben lehetőség van a szinkronizált objektumok közötti birtoklási kapcsolatok kifejezésére. Erre a setSameMutex(shared Object ownee, shared Object owner) globális függvény szolgál. Ennek hatására az ownee objektum az owner mutexét fogja használni. Így biztosítható, hogy ha esetleg kívülről el is érik azt az objektumot, csak úgy tehessék meg, hogy az őt birtokló szinkronizált osztály működésében ez ne okozhasson problémát. Persze ehhez az szükséges, hogy a birtokolt osztály is szinkronizált legyen. Más esetben nem biztosítható, hogy egy esetleges külső referencián csak szinkronizált módon férjenek hozzá.

Példaként tekintsük a következő, módosított BankAccount osztályt, mely egy szálbiztos listában tárolja a számlát érintő tranzakciókat.

// Thread-aware synchronized class List(T) { ... void append(T value) { ... } } // Keeps a List of transactions synchronized class BankAccount { private double _balance; private List!double _transactions; this() { // The account owns the list setSameMutex(_transactions, this); } ... }

A fenti kódban biztosított, hogy a BankAccount objektumhoz és a hozzá tartozó List objektumhoz egyszerre csak egy szál férhessen hozzá. Azonban az alobjektum szinkronizálása extra műveletet igényel. Ha biztosak vagyunk benne, hogy senki sem próbálja kívülről elérni, akkor hatékonyabbá tehetjük a programot, ha szinkronizálás nélkül használjuk, mégpedig egy típuskényszerítés segítségével.

synchronized class BankAccount { private double _balance; private List!double _transactions; void deposit(double amount) { _balance += amount; (cast(List!double) _transactions).append(amount); } void withdraw(double amount) { enforce(balance >= amount); _balance -= amount; (cast(List!double) _transactions).append(-amount); } double balance() { return _balance; } }

Ez a megoldás azonban veszélyes, hiszen ekkor a programozónak teljesen biztosnak kell lennie abban, hogy kívülről nem használják az objektumot. Éppen ezért ennek a módszernek a használata nem is ajánlatos.

Zárolás és holtpont

Ha már rendelkezünk megbízhatóan működő számla objektumokkal, akkor azok között végezzünk átutalásokat. Egy átutalás két lépésből áll: a forrás-számláról levesszük a kívánt összeget, majd hozzáadjuk a cél-számla egyenlegéhez.

// Transfer version 1: non-atomic void transfer(shared BankAccount source, shared BankAccount target, double amount) { source.withdraw(amount); target.deposit(amount); }

A fenti megvalósítás nem atomi, ami azt eredményezi, hogy van egy olyan időintervallum a két művelet végrehajtása között, amikor az átutalás alatt lévő összeg egyik számlán sincs jelen. Ennek elkerülésére atomian kellene végrehajtani az átutalást, amit úgy tudunk biztosítani, ha az egész átutalás idejére megtiltjuk a számlákhoz való hozzáférést a többi szálnak, azaz megszerezzük a mutexeiket.

// Transfer version 2: PROBLEMATIC void transfer(shared BankAccount source, shared BankAccount target, double amount) { synchronized (source) { synchronized (target) { source.withdraw(amount); target.deposit(amount); } } }

A synchronized utasítások csak akkor kezdik el a hozzájuk tartozó utasítások végrehajtását, ha megszerezték a paraméterként megadott objektum mutexét.
Képzeljük el, hogy a két számláról egyszerre akarnak átutalást intézni a másikra. Első lépésként mindkét szál megszerzi a saját forrás-számlájához való kizárólagos hozzáférést, majd próbálja ugyanezt megtenni a cél-számlával is. Azonban ez nem fog sikerülni, mivel azt már a másik szál megszerezte. Ebben a helyzetben mindkét szál vég nélkül fog várakozni, amit holtpontnak nevezünk. Elkerülhetjük a holtpontot, ha a szükséges mutexek megszerzését is atomian végezzük el, azaz vagy mindet megszerezzük egyetlen lépésben, vagy egyiket sem. Erre a következő módon biztosít lehetőséget a D:

// Transfer version 3: correct void transfer(shared BankAccount source, shared BankAccount target, double amount) { synchronized (source, target) { source.withdraw(amount); target.deposit(amount); } }

A fenti megoldás már holtpont-mentesen biztosítja az átutalás atomi végrehajtását a két számla között.

Zárolás-mentes adatszerkezetek

A zároláson alapuló szinkronizáció 1960-as évekbeli kitalálása után nem sokkal elkezdtek gondolkozni azon, hogy mi módon lehetne a kölséges mutexeket lecserélni valami hatékonyabb megoldással. A hardverek fejlődésével idővel lehetővé vált bizonyos műveletek ténylegesen atomi végrehajtása. Ilyenek például az atomi inkrementálás, a test-and-set művelet, mely ellenőrzi egy memóriacella értékét, és ha megfelelő akkor egy másik értékre állítja.

Napjainkban már minden processzor támogatja a compare-and-swap (cas) műveletet, melyet a D core.atomic modulja a programozók rendelkezésére is bocsát. Ennek szemantikája a következő függvénnyel fogalmazható meg:

// This function executes atomically bool cas(T)(shared(T) * here, shared(T) ifThis, shared(T) writeThis) { if (*here == ifThis) { *here = writeThis; return true; } return false; }

Ha az adott címen a kívánt érték volt, akkor azt lecseréli a harmadik paraméter értékével, és true-val tér vissza, egyébként false értéket ad.

Elméletben minden szinkronizációs probléma megoldható a zárolás alapú szinkronizáció helyett cas műveletek alkalmazásával, bár szemmel láthatóan ez nem egyszerű feladat. Főleg nem egyszerű az ilyen megoldás helyességének bizonyítása.
Mindenesetre a D lehetővé teszi, hogy ilyen zárolás-mentes adatszerkezetek létrehozásával próbálkozzunk. Osztályok és structok definíciójában használható a shared módosító a következő módon:

shared struct LockFreeStruct { ... } shared class LockFreeClass { ... }

A shared módosító hatásának fentebb említett tranzitivitása ebben az esetben is teljesül. Tehát a shared osztályok és structok adattagjai is sharedek lesznek. Azonban a tagfüggvényekre nincs semmilyen védelem, azokra ugyanazok a szabályok érvényesek, mint az egyszerű osztályok esetén. Amit a fordító biztosít ebben az esetben, az a következő: az értékadások és cas műveletek atomian futnak le, valamint az utasítások a leírt sorrendben kerülnek végrehajtásra, tehát sem a fordító, sem a processzor nem rendezi át őket. Emellett szükség még van egy nagy adag önbizalomra, hogy hozzákezdjünk egy zárolás-mentes szálbiztos adatszerkezet megvalósításához.