D Programlama Dili – Programlama dersleri ve D referansı
Ali Çehreli

çokuzlu: [tuple], bir kaç parçanın diziye benzer biçimde bir araya gelmesinden oluşan yapı
değişmez: [immutable], programın çalışması süresince kesinlikle değişmeyen
emekliye ayrılan: [deprecated], hâlâ kullanılan ama yakında geçersiz olacak olanak
eş zamanlı programlama: [concurrency], iş parçacıklarının birbirlerine bağımlı olarak işlemeleri
görev: [task], programın geri kalanıyla koşut işletilebilen işlem birimi
iş parçacığı: [thread], işletim sisteminin program işletme birimi
kesintisiz işlem: [atomic operation], bir iş parçacığı tarafından kesintiye uğramadan işletilen işlem
kilitsiz veri yapısı: [lock-free data structure], kilit nesnesi gerektirmeden eş zamanlı programlamada doğru işleyen veri yapısı
koşut işlemler: [parallelization], bağımsız işlemlerin aynı anda işletilmeleri
yarış hali: [race condition], verinin yazılma ve okunma sırasının kesin olmaması
... bütün sözlük



İngilizce Kaynaklar


Diğer




Veri Paylaşarak Eş Zamanlı Programlama

Bir önceki bölümdeki yöntemler iş parçacıklarının mesajlaşarak bilgi alış verişinde bulunmalarını sağlıyordu. Daha önce de söylediğim gibi, eş zamanlı programlamada güvenli olan yöntem odur.

Diğer yöntem, iş parçacıklarının aynı verilere doğrudan erişmelerine dayanır; iş parçacıkları aynı veriyi doğrudan okuyabilirler ve değiştirebilirler. Örneğin, sahip işçiyi bool bir değişkenin adresi ile başlatabilir ve işçi de sonlanıp sonlanmayacağını doğrudan o değişkenin değerini okuyarak karar verebilir. Başka bir örnek olarak, sahip bir kaç tane iş parçacığını hesaplarının sonuçlarını ekleyecekleri bir değişkenin adresi ile başlatabilir ve işçiler de o değişkenin değerini doğrudan arttırabilirler.

Veri paylaşımı ancak paylaşılan veri değişmez olduğunda güvenilirdir. Verinin değişebildiği durumda ise iş parçacıkları birbirlerinden habersizce yarış halinde bulunurlar. İşletim sisteminin iş parçacıklarını ne zaman duraksatacağı ve ne zaman tekrar başlatacağı konusunda hiçbir tahminde bulunulamadığından programın davranışı bu yüzden şaşırtıcı derecede karmaşıklaşabilir.

Bu başlık altındaki örnekler fazlaca basit ve anlamsız gelebilir. Buna rağmen, veri paylaşımının burada göreceğimiz sorunlarıyla gerçek programlarda da çok daha büyük ölçeklerde karşılaşılır. Ek olarak, buradaki örnekler iş parçacığı başlatırken kolaylık olarak std.concurrency.spawn'dan yararlanıyor olsalar da, burada anlatılan kavramlar core.thread modülünün olanakları ile başlatılmış olan iş parçacıkları için de geçerlidir.

Paylaşım otomatik değildir

Çoğu dilin aksine, D'de değişkenler iş parçacıklarına özeldir. Örneğin, her ne kadar aşağıdaki programdaki değişken tekmiş gibi görünse de her iş parçacığı o değişkenin kendi kopyasını edinir:

import std.stdio;
import std.concurrency;
import core.thread;

int değişken;

void bilgiVer(string mesaj) {
    writefln("%s: %s (@%s)", mesaj, değişken, &değişken);
}

void işçi() {
    değişken = 42;
    bilgiVer("İşçi sonlanırken");
}

void main() {
    spawn(&işçi);
    thread_joinAll();
    bilgiVer("İşçi sonlandıktan sonra");
}

işçi'de değiştirilen değişkenin main'in kullandığı değişkenin aynısı olmadığı hem main'deki değerinin sıfır olmasından hem de adreslerinin farklı olmasından anlaşılıyor:

İşçi sonlanırken: 42 (@7F299DEF5660)
İşçi sonlandıktan sonra: 0 (@7F299DFF67C0)

Değişkenlerin iş parçacıklarına özel olmalarının doğal bir sonucu, onların iş parçacıkları tarafından otomatik olarak paylaşılamamalarıdır. Örneğin, sonlanmasını bildirmek için işçiye bool türündeki bir değişkenin adresini göndermeye çalışan aşağıdaki kod D'de derlenemez:

import std.concurrency;

void işçi(bool * devam_mı) {
    while (*devam_mı) {
        // ...
    }
}

void main() {
    bool devam_mı = true;
    spawn(&işçi, &devam_mı);      // ← derleme HATASI

    // ...

    // Daha sonra işçi'nin sonlanmasını sağlamak için
    devam_mı = false;

    // ...
}

std.concurrency modülündeki bir static assert, bir iş parçacığının değişebilen (mutable) yerel verisine başka iş parçacığı tarafından erişilmesini engeller:

src/phobos/std/concurrency.d(329): Error: static assert
"Aliases to mutable thread-local data not allowed."

main() içindeki devam_mı değişebilen yerel bir veri olduğundan ona erişim sağlayan adresi hiçbir iş parçacığına geçirilemez.

Verinin iş parçacıklarına özel olmasının bir istisnası, __gshared olarak işaretlenmiş olan değişkenlerdir:

__gshared int bütünProgramdaTek;

Bu çeşit değişkenlerden bütün programda tek adet bulunur ve o değişken bütün iş parçacıkları tarafından paylaşılır. __gshared değişkenler paylaşımın otomatik olduğu C ve C++ gibi dillerin kütüphaneleri ile etkileşirken gerekirler.

Veri paylaşımı için shared

Değişebilen yerel verilerin iş parçacıkları tarafından paylaşılabilmeleri için "paylaşılan" anlamına gelen shared olarak işaretlenmeleri gerekir:

import std.concurrency;

void işçi(shared(bool) * devam_mı) {
    while (*devam_mı) {
        // ...
    }
}

void main() {
    shared(bool) devam_mı = true;
    spawn(&işçi, &devam_mı);

    // ...

    // Daha sonra işçi'nin sonlanmasını sağlamak için
    devam_mı = false;

    // ...
}

(Not: İş parçacıklarının haberleşmeleri için bu örnekteki gibi veri paylaşımını değil, bir önceki bölümdeki mesajlaşmayı yeğleyin.)

Öte yandan, immutable verilerin değiştirilmeleri olanaksız olduğundan onların paylaşılmalarında bir sakınca yoktur. O yüzden immutable veriler açıkça belirtilmeseler de shared'dirler:

import std.stdio;
import std.concurrency;
import core.thread;

void işçi(immutable(int) * veri) {
    writeln("veri: ", *veri);
}

void main() {
    immutable(int) bilgi = 42;
    spawn(&işçi, &bilgi);         // ← derlenir

    thread_joinAll();
}

Yukarıdaki program bu sefer derlenir ve beklenen çıktıyı üretir:

veri: 42

bilgi'nin yaşamı main() ile sınırlı olduğundan, ona erişmekte olan iş parçacığı sonlanmadan main()'den çıkılmamalıdır. Bu yüzden, yukarıdaki programda main()'den çıkılması programın sonundaki thread_joinAll() çağrısı ile engellenmekte ve bilgi değişkeni işçi() işlediği sürece geçerli kalmaktadır.

Veri değiştirirken yarış halinde olma örneği

Değişebilen verilerin paylaşıldığı durumda programın davranışının doğruluğunu sağlamak programcının sorumluluğundadır.

Bunun önemini görmek için aynı değişebilen veriyi paylaşan birden fazla iş parçacığına bakalım. Aşağıdaki programdaki iş parçacıkları iki değişkenin adreslerini alıyorlar ve o değişkenlerin değerlerini değiş tokuş ediyorlar:

import std.stdio;
import std.concurrency;
import core.thread;

void değişTokuşçu(shared(int) * birinci, shared(int) * ikinci) {
    foreach (i; 0 .. 10_000) {
        int geçici = *ikinci;
        *ikinci = *birinci;
        *birinci = geçici;
    }
}

void main() {
    shared(int) i = 1;
    shared(int) j = 2;

    writefln("önce : %s ve %s", i, j);

    foreach (adet; 0 .. 10) {
        spawn(&değişTokuşçu, &i, &j);
    }

    // Bütün işlemlerin bitmesini bekliyoruz
    thread_joinAll();

    writefln("sonra: %s ve %s", i, j);
}

Ne yazık ki, yukarıdaki program büyük olasılıkla yanlış sonuç üretir. Bunun nedeni, 10 iş parçacığının main() içindeki i ve j isimli aynı değişkenlere erişmeleri ve farkında olmadan yarış halinde birbirlerinin işlerini bozmalarıdır.

Yukarıdaki programdaki toplam değiş tokuş adedi 10 çarpı 10 bindir. Bu değer bir çift sayı olduğundan i'nin ve j'nin değerlerinin program sonunda yine başlangıçtaki gibi 1 ve 2 olmalarını bekleriz:

önce : 1 ve 2
sonra: 1 ve 2      ← beklenen sonuç

Program farklı zamanlarda ve ortamlarda bazen o sonucu üretebilse de aşağıdaki yanlış sonuçların çıkma olasılığı daha yüksektir:

önce : 1 ve 2
sonra: 1 ve 1      ← yanlış sonuç
önce : 1 ve 2
sonra: 2 ve 2      ← yanlış sonuç

Bazı durumlarda sonuç "2 ve 1" bile çıkabilir.

Bunun nedenini A ve B olarak isimlendireceğimiz yalnızca iki iş parçacığının işleyişiyle bile açıklayabiliriz. İşletim sistemi iş parçacıklarını belirsiz zamanlarda duraksatıp tekrar başlattığından bu iki iş parçacığı verileri birbirlerinden habersiz olarak aşağıda gösterildiği biçimde değiştirebilirler.

i'nin ve j'nin değerlerinin sırasıyla 1 ve 2 olduğu duruma bakalım. Aynı değişTokuşçu() işlevini işlettikleri halde A ve B iş parçacıklarının yerel geçici değişkenleri farklıdır. Ayırt edebilmek için onları aşağıda geçiciA ve geçiciB olarak belirtiyorum.

Her iki iş parçacığının işlettiği aynı 3 satırlık kodun zaman ilerledikçe nasıl işletildiklerini yukarıdan aşağıya doğru gösteriyorum: 1 numaralı işlem ilk işlem, 6 numaralı işlem de son işlem. Her işlemde i ve j'den hangisinin değiştiğini de sarıyla işaretliyorum:

İşlem            İş parçacığı A                      İş parçacığı B
─────────────────────────────────────────────────────────────────────────────

  1:   int geçici = *ikinci; (geçiciA==2)
  2:   *ikinci = *birinci;   (i==1, j==1)

                (A duraksatılmış ve B başlatılmış olsun)

  3:                                       int geçici = *ikinci; (geçiciB==1)
  4:                                       *ikinci = *birinci;   (i==1, j==1)

                (B duraksatılmış ve A tekrar başlatılmış olsun)

  5:   *birinci = geçici;    (i==2, j==1)

                (A duraksatılmış ve B tekrar başlatılmış olsun)

  6:                                       *birinci = geçici;    (i==1, j==1)

Görüldüğü gibi, yukarıdaki gibi bir durumda hem i hem de j 1 değerini alırlar. Artık programın sonuna kadar başka değer almaları mümkün değildir.

Yukarıdaki işlem sıraları bu programdaki hatayı açıklamaya yeten yalnızca bir durumdur. Onun yerine 10 iş parçacığının etkileşimlerinden oluşan çok sayıda başka karmaşık durum da gösterilebilir.

Veri korumak için synchronized

Yukarıdaki hatalı durum aynı verinin birden fazla iş parçacığı tarafından serbestçe okunması ve yazılması nedeniyle oluşmaktadır. Bu tür hataları önlemenin bir yolu, belirli bir anda yalnızca tek iş parçacığı tarafından işletilmesi gereken kod bloğunu synchronized olarak işaretlemektir. Yapılacak tek değişiklik programın artık doğru sonuç üretmesi için yeterlidir:

    foreach (i; 0 .. 10_000) {
        synchronized {
            int geçici = *ikinci;
            *ikinci = *birinci;
            *birinci = geçici;
        }
    }

Çıktısı:

önce : 1 ve 2
sonra: 1 ve 2      ← doğru sonuç

synchronized, isimsiz bir kilit oluşturur ve bu kilidi belirli bir anda yalnızca tek iş parçacığına verir. Yukarıdaki kod bloğu da bu sayede belirli bir anda tek iş parçacığı tarafından işletilir ve i ve j'nin değerleri her seferinde doğru olarak değiş tokuş edilmiş olur. Değişkenler foreach döngüsünün her adımında ya "1 ve 2" ya da "2 ve 1" durumundadırlar.

Not: Bir iş parçacığının bir kilidin açılmasını beklemesi ve tekrar kilitlemesi masraflı bir işlemdir ve programın farkedilir derecede yavaş işlemesine neden olabilir. Bazı programlarda veri paylaşımı synchronized ile kilit kullanılmasına gerek kalmadan kesintisiz işlemlerden yararlanılarak da sağlanabilir ve program bu sayede daha hızlı işleyebilir. Bunun örneklerini biraz aşağıda göreceğiz.

Kullanacağı kilit veya kilitler synchronized'a açıkça da verilebilir. Bu, belirli bir anda birden fazla bloktan yalnızca birisinin işlemesini sağlar.

Bunun bir örneğini görmek için aşağıdaki programa bakalım. Bu programda paylaşılan veriyi değiştiren iki kod bloğu bulunuyor. Bu blokları shared(int) türündeki aynı değişkenin adresi ile çağıracağız. Birisi bu değişkenin değerini arttıracak, diğeri ise azaltacak:

void arttırıcı(shared(int) * değer) {
    foreach (i; 0 .. adet) {
       *değer = *değer + 1;
    }
}

void azaltıcı(shared(int) * değer) {
    foreach (i; 0 .. adet) {
        *değer = *değer - 1;
    }
}

Not: Yukarıdaki ifadeler yerine daha kısa olan ++(*değer) ve ‑‑(*değer) ifadeleri kullanıldığında derleyici o ifadelerin shared değişkenler üzerinde işletilmelerinin emekliye ayrıldığını bildiren bir uyarı mesajı verir.

Aynı veriyi değiştirdikleri için bu iki bloğun da synchronized olarak işaretlenmeleri düşünülebilir, ancak bu yeterli olmaz. Bu bloklar farklı olduklarından her birisi farklı bir kilit ile korunacaktır ve değişkenin tek iş parçacığı tarafından değiştirilmesi yine sağlanamayacaktır:

import std.stdio;
import std.concurrency;
import core.thread;

enum adet = 1000;

void arttırıcı(shared(int) * değer) {
    foreach (i; 0 .. adet) {
        synchronized {  // ← bu kilit aşağıdakinden farklıdır
            *değer = *değer + 1;
        }
    }
}

void azaltıcı(shared(int) * değer) {
    foreach (i; 0 .. adet) {
        synchronized {  // ← bu kilit yukarıdakinden farklıdır
            *değer = *değer - 1;
        }
    }
}

void main() {
    shared(int) ortak = 0;

    foreach (i; 0 .. 100) {
        spawn(&arttırıcı, &ortak);
        spawn(&azaltıcı, &ortak);
    }

    thread_joinAll();
    writeln("son değeri: ", ortak);
}

Eşit sayıda arttırıcı ve azaltıcı iş parçacığı başlatılmış olduğundan ortak isimli değişkenin son değerinin sıfır olmasını bekleriz ama büyük olasılıkla sıfırdan farklı çıkar:

son değeri: -3325  ← sıfır değil

Farklı blokların aynı kilidi veya kilitleri paylaşmaları gerektiğinde kilit veya kilitler synchronized'a parantez içinde bildirilir:

    synchronized (kilit_nesnesi, başka_kilit_nesnesi, ...)

D'de özel bir kilit nesnesi yoktur; herhangi bir sınıf türünün herhangi bir nesnesi kilit olarak kullanılabilir. Yukarıdaki programdaki iş parçacıklarının aynı kilidi kullanmaları için main() içinde bir nesne oluşturulabilir ve iş parçacıklarına parametre olarak o nesne gönderilebilir. Programın değişen yerlerini işaretliyorum:

import std.stdio;
import std.concurrency;
import core.thread;

enum adet = 1000;

class Kilit {
}

void arttırıcı(shared(int) * değer, shared(Kilit) kilit) {
    foreach (i; 0 .. adet) {
        synchronized (kilit) {
            *değer = *değer + 1;
        }
    }
}

void azaltıcı(shared(int) * değer, shared(Kilit) kilit) {
    foreach (i; 0 .. adet) {
        synchronized (kilit) {
            *değer = *değer - 1;
        }
    }
}

void main() {
    shared(Kilit) kilit = new shared(Kilit)();
    shared(int) ortak = 0;

    foreach (i; 0 .. 100) {
        spawn(&arttırıcı, &ortak, kilit);
        spawn(&azaltıcı, &ortak, kilit);
    }

    thread_joinAll();
    writeln("son değeri: ", ortak);
}

Bütün iş parçacıkları main() içinde tanımlanmış olan aynı kilidi kullandıklarından belirli bir anda bu iki synchronized bloğundan yalnızca bir tanesi işletilir ve beklenen sonuç elde edilir:

son değeri: 0      ← doğru sonuç

Sınıflar da synchronized olarak tanımlanabilirler. Bunun anlamı, o türün bütün üye işlevlerinin aynı kilidi kullanacaklarıdır:

synchronized class Sınıf {
    void foo() {
        // ...
    }

    void bar() {
        // ...
    }
}

synchronized olarak işaretlenen türlerin bütün üye işlevleri nesnenin kendisini kilit olarak kullanırlar. Yukarıdaki sınıfın eşdeğeri aşağıdaki sınıftır:

class Sınıf {
    void foo() {
        synchronized (this) {
            // ...
        }
    }

    void bar() {
        synchronized (this) {
            // ...
        }
    }
}

Birden fazla nesnenin kilitlenmesi gerektiğinde bütün nesneler aynı synchronized deyimine yazılmalıdırlar. Aksi taktirde farklı iş parçacıkları farklı nesnelerin kilitlerini ele geçirmiş olabileceklerinden sonsuza kadar birbirlerini bekleyerek takılıp kalabilirler.

Bunun tanınmış bir örneği, bir banka hesabından diğerine para aktaran işlevdir. Böyle bir işlemin hatasız gerçekleşmesi için her iki banka hesabının da kilitlenmesinin gerekeceği açıktır. Bu durumda yukarıda gördüğümüz synchronized kullanımını aşağıdaki gibi uygulamak hatalı olur:

void paraAktar(shared(BankaHesabı) kimden,
               shared(BankaHesabı) kime) {
    synchronized (kimden) {           // ← HATALI
        synchronized (kime) {
            // ...
        }
    }
}

Yanlışlığın nedenini şöyle basit bir durumla açıklayabiliriz: Bir iş parçacığının A hesabından B hesabına para aktardığını, başka bir iş parçacığının da B hesabından A hesabına para aktardığını düşünelim. İşletim sisteminin iş parçacıklarını belirsiz zamanlarda duraksatması sonucunda; kimden olarak A hesabını işlemekte olan iş parçacığı A nesnesini, kimden olarak B nesnesini işlemekte olan iş parçacığı da B nesnesini kilitlemiş olabilir. Bu durumda her ikisi de diğerinin elinde tuttuğu nesneyi kilitlemeyi bekleyeceklerinden sonsuza kadar takılıp kalacaklardır.

Bu sorunun çözümü synchronized deyiminde birden fazla nesne belirtmektir:

void paraAktar(shared(BankaHesabı) kimden,
               shared(BankaHesabı) kime) {
    synchronized (kimden, kime) {     // ← doğru
        // ...
    }
}

Derleyici ya nesnelerin ikisinin birden kilitleneceğini ya da hiçbirisinin kilitlenmeyeceğini garanti eder.

Tek ilkleme için shared static this() ve tek sonlandırma için shared static ~this()

static this()'in modül değişkenlerini ilklerken kullanıldığını görmüştük. D'de veri iş parçacıklarına özel olduğundan static this() her iş parçacığı için ayrıca işletilir:

import std.stdio;
import std.concurrency;
import core.thread;

static this() {
    writeln("static this() işletiliyor");
}

void işçi() {
}

void main() {
    spawn(&işçi);

    thread_joinAll();
}

Yukarıdaki programdaki static this() bir kere ana iş parçacığında bir kere de spawn() ile başlatılan iş parçacığında işletilir:

static this() işletiliyor
static this() işletiliyor

Bu durum shared olarak işaretlenmiş olan modül değişkenleri (immutable dahil) için bir sorun oluşturur çünkü aynı değişkenin birden fazla ilklenmesi yarış hali nedeniyle özellikle eş zamanlı programlamada yanlış olacaktır. Bunun çözümü shared static this() bloklarıdır. Bu bloklar bütün programda tek kere işletilirler:

int a;              // her iş parçacığına özel
immutable int b;    // bütün programda paylaşılan

static this() {
    writeln("İş parçacığı değişkeni ilkleniyor; adresi: ", &a);
    a = 42;
}

shared static this() {
    writeln("Program değişkeni ilkleniyor; adresi: ", &b);
    b = 43;
}

Çıktısı:

Program değişkeni ilkleniyor; adresi: 6B0140    ← programda tek
İş parçacığı değişkeni ilkleniyor; adresi: 7F80E22667D0
İş parçacığı değişkeni ilkleniyor; adresi: 7F80E2165670

Benzer biçimde, shared static ~this() de bütün programda tek kere işletilmesi gereken sonlandırma işlemleri içindir.

Kesintisiz işlemler

İşlemlerin başka iş parçacıkları araya girmeden kesintisiz olarak işletilmesini sağlamanın bir yolu; mikro işlemci, derleyici, veya işletim sistemi tarafından sunulmuş olan kesintisiz işlemlerden yararlanmaktır.

Phobos bu olanakları core.atomic modülünde sunar. Bu bölümde bu olanaklardan yalnızca ikisini göstereceğim:

atomicOp

Bu işlev, şablon parametresi olarak belirtilen işleci parametrelerine uygular. Şablon parametresinin +, +=, vs. gibi bir ikili işleç olması şarttır:

import core.atomic;

// ...

        atomicOp!"+="(*değer, 1);    // kesintisiz

Yukarıdaki satır, aşağıdakinin kesintiye uğratılmadan işletilmesinin eşdeğeridir:

        *değer += 1;                 // kesintili

Dolayısıyla, eğer kesintiye uğratılmadan işletilmesi gereken işlem bir ikili işleç ise synchronized bloğuna gerek kalmaz ve kod daha hızlı işleyebilir. Yukarıdaki arttırıcı ve azaltıcı işlevlerinin aşağıdaki eşdeğerleri de programın doğru çalışmasını sağlar. Bu çözümde Kilit türüne de gerek yoktur:

import core.atomic;

//...

void arttırıcı(shared(int) * değer) {
    foreach (i; 0 .. adet) {
        atomicOp!"+="(*değer, 1);
    }
}

void azaltıcı(shared(int) * değer) {
    foreach (i; 0 .. adet) {
        atomicOp!"-="(*değer, 1);
    }
}

atomicOp başka ikili işleçlerle de kullanılabilir.

cas

Bu işlevin ismi "karşılaştır ve değiş tokuş et" anlamına gelen İngilizce compare and swap'ın kısasıdır. İşleyişi, değişkenin hâlâ belirli bir değere eşit ise değiştirilmesi temeline dayanır. Önce değişkenin mevcut değeri okunur ve o değer cas'a yeni değerle birlikte verilir:

    bool değişti_mi = cas(değişken_adresi, mevcutDeğer, yeniDeğer);

Değişkenin mevcut değerinin cas'ın işleyişi sırasında aynı kalmış olması başka bir iş parçacığının araya girmediğinin göstergesidir. Bu durumda cas değişkene yeni değerini atar ve değişimin başarıyla gerçekleştiğini belirtmek için true döndürür. Değişkenin eski değerine eşit olmadığını gördüğünde cas işleyişine devam etmez ve false döndürür.

Aşağıdaki işlevler cas başarısız olduğunda (yani, dönüş değeri false olduğunda) mevcut değeri tekrar okumakta ve işlemi hemen tekrar denemekteler. Bu çağrıların anlamı değeri mevcutDeğer'e eşit ise yeni değerle değiştir diye açıklanabilir:

void arttırıcı(shared(int) * değer) {
    foreach (i; 0 .. adet) {
        int mevcutDeğer;

        do {
            mevcutDeğer = *değer;
        } while (!cas(değer, mevcutDeğer, mevcutDeğer + 1));
    }
}

void azaltıcı(shared(int) * değer) {
    foreach (i; 0 .. adet) {
        int mevcutDeğer;

        do {
            mevcutDeğer = *değer;
        } while (!cas(değer, mevcutDeğer, mevcutDeğer - 1));
    }
}

Yukarıdaki işlevler de synchronized bloğuna gerek kalmadan doğru sonuç üretirler.

core.atomic modülünün olanakları çoğu durumda synchronized bloklarından kat kat hızlıdır. Probleme uygun olduğu sürece öncelikle bu modülden yararlanmanızı öneririm.

Bu olanaklar bu kitabın konusu dışında kalan kilitsiz veri yapılarının gerçekleştirilmelerinde de kullanılırlar.

Klasik eş zamanlı programlamada çok karşılaşılan başka olanakları da core.sync pakedinin modüllerinde bulabilirsiniz:

Özet