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

bağlam değiştirme: [context switching], başka iş parçacığına geçilmesi
çağrı yığıtı: [call stack], belleğin kısa ömürlü değişkenler ve işlev çağrıları için kullanılan bölgesi
çerçeve: [frame], işlev çağrısının yerel durumunu barındıran alan
çoklu görev: [multi-tasking], birden fazla görevin etkin olması
erişici: [iterator], elemanlara erişim sağlayan yapı
geçişli çoklu görev: [preemptive multi-tasking], görevlerin bilinmeyen zamanlarda duraksatıldığı çoklu görev yöntemi
görev: [task], programın geri kalanıyla koşut işletilebilen işlem birimi
hevesli: [eager], işlemlerin, ürettikleri sonuçların kullanılacaklarından emin olunmadan gerçekleştirilmeleri
iş parçacığı: [thread], işletim sisteminin program işletme birimi
işbirlikli çoklu görev: [cooperative multi-tasking], görevlerin kendilerini duraksattıkları çoklu görev yöntemi
mikro işlemci çekirdeği: [CPU core], başlı başına mikro işlemci olarak kullanılabilen işlemci birimi
ortak işlev: [coroutine], aynı zamanda işletilen görevlerden birisi
ön bellek: [cache], hızlı veri veya kod erişimi için kullanılan mikro işlemci iç belleği
özyineleme: [recursion], bir işlevin doğrudan veya dolaylı olarak kendisini çağırması
tembel değerlendirme: [lazy evaluation], işlemlerin gerçekten gerekene kadar geciktirilmesi
yarış hali: [race condition], verinin yazılma ve okunma sırasının kesin olmaması
yığıt çözülmesi: [stack unwinding], atılan hata nedeniyle çerçevelerin çağrı yığıtından çıkartılmaları
zaman uyumsuz: [asynchronous], önceden bilinmeyen zaman aralıklarında gerçekleşen
... bütün sözlük



İngilizce Kaynaklar


Diğer




Fiberler

Fiber, tek iş parçacığının birden fazla görev yürütmesini sağlayan bir işlem birimidir. Koşut işlemlerde ve eş zamanlı programlamada normalde yararlanılan iş parçacıklarıyla karşılaştırıldığında bir fiberin duraksatılması ve tekrar başlatılması çok daha hızlıdır. Fiberler ortak işlevlere (coroutines) ve yeşil iş parçacıklarına (green threads) çok benzerler ve bu terimler bazen aynı anlamda kullanılır.

Fiberler temelde iş parçacıklarının birden fazla çağrı yığıtı kullanmalarını sağlarlar. Bu yüzden, fiberlerin yararını tam olarak görebilmek için önce çağrı yığıtının getirdiği kolaylığı anlamak gerekir.

Çağrı yığıtı

Bir işlevin parametreleri, static olmayan yerel değişkenleri, dönüş değeri, geçici ifadeleri, ve işletilmesi sırasında gereken başka her türlü bilgi o işlevin yerel durumudur (local state). Yerel durumu oluşturan değişkenler için kullanılan alan her işlev çağrısında otomatik olarak ayrılır ve bu değişkenler otomatik olarak ilklenirler.

Her çağrı için ayrılan bu alan o çağrının çerçevesi olarak adlandırılır. İşlevler başka işlevleri çağırdıkça bu çerçeveler kavramsal olarak yığıt biçiminde üst üste yerleştirilirler. Belirli bir andaki bütün etkin işlev çağrılarının çerçevelerinden oluşan alana o işlevleri işletmekte olan iş parçacığının çağrı yığıtı denir.

Örneğin, aşağıdaki programın ana iş parçacığında main'in foo'yu çağırmasının ardından foo'nun da bar'ı çağırdığı durumda toplam üç etkin işlev çağrısı vardır:

void main() {
    int a;
    int b;

    int c = foo(a, b);
}

int foo(int x, int y) {
    bar(x + y);
    return 42;
}

void bar(int parametre) {
    string[] dizi;
    // ...
}

O çağrılar sonucunda bar'ın işletilmesi sırasında çağrı yığıtı üç çerçeveden oluşur:

Çağrı yığıtı işlev çağrıları
derinleştikçe yukarıya doğru büyür.
                               ▲  ▲
                               │  │
Çağrı yığıtının tepesi → ┌───────────────┐
                         │ int parametre │ ← bar'ın çerçevesi
                         │ string[] dizi │
                         ├───────────────┤
                         │ int x         │
                         │ int y         │ ← foo'nun çerçevesi
                         │ dönüş değeri  │
                         ├───────────────┤
                         │ int a         │
                         │ int b         │ ← main'in çerçevesi
  Çağrı yığıtının dibi → └───────────────┘

İşlevlerin başka işlevleri çağırarak daha derine dallanmalarına ve bu işlevlerden üst düzeylere dönülmelerine bağlı olarak çağrı yığıtının büyüklüğü buna uygun olarak artar veya azalır. Örneğin, bar'dan dönüldüğünde artık çerçevesine gerek kalmadığından o alan ilerideki başka bir çağrı için kullanılmak üzere boş kalır:

                         ┌───────────────┐
                         │ int parametre │
                         │ string[] dizi │
Çağrı yığıtının tepesi → ├───────────────┤
                         │ int x         │
                         │ int y         │ ← foo'nun çerçevesi
                         │ dönüş değeri  │
                         ├───────────────┤
                         │ int a         │
                         │ int b         │ ← main'in çerçevesi
  Çağrı yığıtının dibi → └───────────────┘

Bu kitapta yazdığımız her programda üzerinde durmasak da hep çağrı yığıtından yararlandık. Özyinelemeli işlevlerin basit olabilmelerinin nedeni de çağrı yığıtıdır.

Özyineleme

Özyineleme, bir işlevin doğrudan veya dolaylı olarak kendisini çağırması durumudur. Özyineleme, aralarında böl ve fethet (divide-and-conquer) diye tanımlananlar da bulunan bazı algoritmaları büyük ölçüde kolaylaştırır.

Bunun bir örneğini görmek için bir dilimin elemanlarının toplamını döndüren aşağıdaki işleve bakalım. Bu işlev görevini yerine getirirken yine kendisini, ama farklı parametre değerleriyle çağırmaktadır. Her çağrı, parametre olarak alınan dilimin bir eksik elemanlısını kullanmaktadır. Bu özyineleme dilim boş kalana kadar devam eder. Belirli bir ana kadar hesaplanmış olan toplam değer ise işlevin ikinci parametresi olarak geçirilmektedir:

import std.array;

int topla(int[] dilim, int anlıkToplam = 0) {
    if (dilim.empty) {
        /* Ekleyecek eleman yok. Bu ana kadar hesaplanmış olan
         * toplamı döndürelim. */
        return anlıkToplam;
    }

    /* Baştaki elemanın değerini bu andaki toplama ekleyelim
     * ve kendimizi dilimin geri kalanı ile çağıralım. */
    return topla(dilim[1..$], anlıkToplam + dilim.front);
}

void main() {
    assert(topla([1, 2, 3]) == 6);
}

Not: Yukarıdaki işlev yalnızca gösterim amacıyla yazılmıştır. Bir aralıktaki elemanların toplamı gerektiğinde std.algorithm.sum işlevini kullanmanızı öneririm. O işlev kesirli sayıları toplarken özel algoritmalardan yararlanır ve daha doğru sonuçlar üretir.

topla'nın yukarıdaki gibi [1, 2, 3] dilimiyle çağrıldığını düşünürsek, özyinelemenin son adımında çağrı yığıtı aşağıdaki çerçevelerden oluşacaktır. Her parametrenin değerini == işlecinden sonra gösteriyorum. Çağrı sırasına uygun olması için çerçeveleri aşağıdan yukarıya doğru okumanızı öneririm:

    ┌──────────────────────────┐
    │ dilim       == []        │ ← topla'nın son çağrılışı
    │ anlıkToplam == 6         │
    ├──────────────────────────┤
    │ dilim       == [3]       │ ← topla'nın üçüncü çağrılışı
    │ anlıkToplam == 3         │
    ├──────────────────────────┤
    │ dilim       == [2, 3]    │ ← topla'nın ikinci çağrılışı
    │ anlıkToplam == 1         │
    ├──────────────────────────┤
    │ dilim       == [1, 2, 3] │ ← topla'nın ilk çağrılışı
    │ anlıkToplam == 0         │
    ├──────────────────────────┤
    │             ...          │ ← main'in çerçevesi
    └──────────────────────────┘

Not: Eğer özyinelemeli işlev topla'da olduğu gibi kendisini çağırmasının sonucunu döndürüyorsa, derleyiciler "kuyruk özyinelemesi" denen bir eniyileştirme (tail-call optimization) yönteminden yararlanırlar ve her çağrı için ayrı çerçeve kullanımını önlerler.

Birden fazla iş parçacığı kullanılan durumda her iş parçacığı diğerlerinden bağımsızca kendi görevini yürüttüğünden, her iş parçacığı için ayrı çağrı yığıtı vardır.

Bir fiberin gücü, kendisi iş parçacığı olmadığı halde kendi çağrı yığıtına sahip olmasından kaynaklanır. Fiberler, normalde tek çağrı yığıtına sahip olan iş parçacıklarının birden fazla çağrı yığıtı kullanabilmelerini sağlarlar. Tek çağrı yığıtı ancak tek görevin durumunu saklayabildiğinden birden fazla çağrı yığıtı bir iş parçacığının birden fazla görev yürütmesini sağlar.

Kullanım

Fiberlerin genel kullanımı aşağıdaki işlemlerden oluşur. Bunların örneklerini biraz aşağıda göreceğiz.

Fiberlerin aralıklara yararı

Hemen hemen her aralık en son nerede kaldığı bilgisini saklamak üzere üye değişkenlerden yararlanır. Bu bilgi, aralık nesnesi popFront ile ilerletilirken kullanılır. Hem Aralıklar bölümünde hem de daha sonraki bölümlerde gördüğümüz çoğu aralığın da üye değişkenleri bulunuyordu.

Örneğin, daha önce tanımlamış olduğumuz FibonacciSerisi, serinin iki sonraki sayısını hesaplamak için iki üye değişkenden yararlanıyordu:

struct FibonacciSerisi {
    int baştaki = 0;
    int sonraki = 1;

    enum empty = false;

    int front() const {
        return baştaki;
    }

    void popFront() {
        const ikiSonraki = baştaki + sonraki;
        baştaki = sonraki;
        sonraki = ikiSonraki;
    }
}

İlerleme durumu için böyle değişkenler tanımlamak FibonacciSerisi gibi bazı aralıklar için basit olsa da, ikili ağaç gibi bazı özyinelemeli veri yapılarında şaşırtıcı derecede güçtür. Şaşırtıcılığın nedeni, aynı algoritmaların özyinelemeli olarak yazıldıklarında ise çok basit olmalarıdır.

Örneğin, özyinelemeli olarak tanımlanmış olan aşağıdaki ekle ve yazdır işlevleri hiç değişken tanımlamaları gerekmeden ve ağaçtaki eleman sayısından bağımsız olarak çok basitçe yazılabilmişlerdir. Özyinelemeli çağrıları işaretli olarak gösteriyorum. (Dikkat ederseniz, ekle'nin özyinelemesi ekleVeyaİlkle üzerindendir.)

import std.stdio;
import std.string;
import std.conv;
import std.random;
import std.range;
import std.algorithm;

/* İkili ağacın düğümlerini temsil eder. Aşağıdaki Ağaç
 * yapısının gerçekleştirilmesinde kullanılmak üzere
 * tanımlanmıştır. */
struct Düğüm {
    int eleman;
    Düğüm * sol;    // Sol alt ağaç
    Düğüm * sağ;    // Sağ alt ağaç

    void ekle(int eleman) {
        if (eleman < this.eleman) {
            /* Küçük elemanlar sol alt ağaca */
            ekleVeyaİlkle(sol, eleman);

        } else if (eleman > this.eleman) {
            /* Büyük elemanlar sağ alt ağaca */
            ekleVeyaİlkle(sağ, eleman);

        } else {
            throw new Exception(format("%s mevcut", eleman));
        }
    }

    void yazdır() const {
        /* Önce sol alt ağacı yazdırıyoruz. */
        if (sol) {
            sol.yazdır();
            write(' ');
        }

        /* Sonra bu düğümün elemanını yazdırıyoruz. */
        write(eleman);

        /* En sonunda da sağ alt ağacı yazdırıyoruz. */
        if (sağ) {
            write(' ');
            sağ.yazdır();
        }
    }
}

/* Elemanı belirtilen alt ağaca ekler. Eğer 'null' ise düğümü
 * ilkler. */
void ekleVeyaİlkle(ref Düğüm * düğüm, int eleman) {
    if (!düğüm) {
        /* Bu alt ağacı ilk elemanıyla ilkliyoruz. */
        düğüm = new Düğüm(eleman);

    } else {
        düğüm.ekle(eleman);
    }
}

/* Ağaç veri yapısını temsil eder. 'kök' üyesi 'null' ise ağaç
 * boş demektir. */
struct Ağaç {
    Düğüm * kök;

    /* Elemanı bu ağaca ekler. */
    void ekle(int eleman) {
        ekleVeyaİlkle(kök, eleman);
    }

    /* Elemanları sıralı olarak yazdırır. */
    void yazdır() const {
        if (kök) {
            kök.yazdır();
        }
    }
}

/* '10 * n' sayı arasından rasgele seçilmiş olan 'n' sayı ile
 * bir ağaç oluşturur. */
Ağaç rasgeleAğaç(size_t n) {
    /* '10 * n' sayı arasından 'n' tane seç. */
    auto sayılar = iota((n * 10).to!int)
                   .randomSample(n, Random(unpredictableSeed))
                   .array;

    /* 'n' sayıyı karıştır. */
    randomShuffle(sayılar);

    /* Ağacı o sayılarla doldur. */
    auto ağaç = Ağaç();
    sayılar.each!(e => ağaç.ekle(e));

    return ağaç;
}

void main() {
    auto ağaç = rasgeleAğaç(10);
    ağaç.yazdır();
}

Not: Yukarıdaki program aşağıdaki Phobos olanaklarından da yararlanmaktadır:

Her toplulukta olduğu gibi, aralık algoritmalarıyla kullanılabilmesi için bu ağaç topluluğunun da bir aralık arayüzü sunmasını isteriz. Bunu opSlice üye işlevini tanımlayarak gerçekleştirebileceğimizi biliyoruz:

struct Ağaç {
// ...

    /* Ağacın elemanlarına sıralı erişim sağlar. */
    struct SıralıAralık {
        ... Gerçekleştirmesi nasıl olmalıdır? ...
    }

    SıralıAralık opSlice() const {
        return SıralıAralık(kök);
    }
}

Yukarıda tanımlanan yazdır üye işlevi de temelde elemanlara sırayla eriştiği halde, bir ağacın elemanlarına erişim sağlayan bir InputRange tanımlamak göründüğünden çok daha güç bir iştir. Ben burada SıralıAralık yapısını tanımlamaya çalışmayacağım. Ağaç erişicilerinin nasıl gerçekleştirildiklerini kendiniz araştırmanızı ve geliştirmeye çalışmanızı öneririm. (Bazı erişici gerçekleştirmeleri sol ve sağ üyelerine ek olarak üstteki (parent) düğümü gösteren Node* türünde bir üye daha olmasını gerektirirler.)

yazdır gibi özyinelemeli ağaç algoritmalarının o kadar basit yazılabilmelerinin nedeni çağrı yığıtıdır. Çağrı yığıtı, belirli bir andaki elemanın hangisi olduğunun yanında o elemana hangi alt ağaçlar izlenerek erişildiği (hangi düğümlerde sola veya sağa dönüldüğü) bilgisini de otomatik olarak saklar.

Örneğin, özyinelemeli sol.yazdır() çağrısından soldaki elemanlar yazdırılıp dönüldüğünde, şu anda işlemekte olan yazdır işlevi sırada boşluk karakteri olduğunu zaten bilir:

    void yazdır() const {
        if (sol) {
            sol.yazdır();
            write(' '); // ← Çağrı yığıtına göre sıra bundadır
        }

        // ...
    }

Fiberler özellikle çağrı yığıtının büyük kolaylık sağladığı bu gibi durumlarda yararlıdır.

Fiberlerin sağladığı kolaylık Fibonacci serisi gibi basit türler üzerinde gösterilemese de, fiber işlemlerini özellikle böyle basit bir yapı üzerinde tanıtmak istiyorum. Daha aşağıda bir ikili ağaç aralığı da tanımlayacağız.

import core.thread;

/* Elemanları üretir ve 'ref' parametresine atar. */
void fibonacciSerisi(ref int baştaki) {                 // (1)
    baştaki = 0;    // Not: 'baştaki' parametrenin kendisidir
    int sonraki = 1;

    while (true) {
        Fiber.yield();                                  // (2)
        /* Bir sonraki call() çağrısı tam bu noktadan
         * devam eder. */                               // (3)

        const ikiSonraki = baştaki + sonraki;
        baştaki = sonraki;
        sonraki = ikiSonraki;
    }
}

void main() {
    int baştaki;                                        // (1)
                         // (4)
    Fiber fiber = new Fiber(() => fibonacciSerisi(baştaki));

    foreach (_; 0 .. 10) {
        fiber.call();                                   // (5)

        import std.stdio;
        writef("%s ", baştaki);
    }
}
  1. Yukarıdaki fiber işlevi parametre olarak int türünde bir değişken referansı almakta ve ürettiği elemanları kendisini çağırana bu parametre aracılığıyla iletmektedir. (Bu parametre ref yerine out olarak da tanımlanabilir.)
  2. Fiber, yeni eleman hazır olduğunda kendisini Fiber.yield() ile duraksatır.
  3. Bir sonraki call() çağrısı, fiberi en son duraksatmış olan Fiber.yield()'in hemen sonrasından devam ettirir. (İlk call() ise fiber işlevini başlatır.)
  4. Fiber işlevleri parametre almadıklarından fibonacciSerisi() doğrudan kullanılamaz. O yüzden, Fiber nesnesi kurulurken parametresiz bir isimsiz işlev kullanılmıştır.
  5. Çağıran, fiberi call() üye işlevi ile başlatır ve devam ettirir.

Sonuçta, main eleman değerlerini baştaki değişkeni üzerinden elde eder ve yazdırır:

0 1 1 2 3 5 8 13 21 34 
Fiberlerin std.concurrency.Generator ile aralık olarak kullanılmaları

Fibonacci serisinin yukarıdaki fiber gerçekleştirmesinin bazı yetersizlikleri vardır:

std.concurrency.Generator sınıfı bu yetersizliklerin hepsini giderir. Aşağıdaki fibonacciSerisi'nin nasıl basit bir işlev olarak yazılabildiğine dikkat edin. Tek farkı, işlevden tek eleman döndürmek yerine yield ile birden fazla eleman üretmesidir. (Bu örnekte sonsuz sayıda eleman üretilmektedir.)

Ek olarak, aşağıdaki yield daha önce kullandığımız Fiber.yield üye işlevi değil, std.concurrency modülündeki yield işlevidir.

import std.stdio;
import std.range;
import std.concurrency;

/* Bu alias std.range.Generator ile olan bir isim çakışmasını
 * gidermek içindir. */
alias FiberAralığı = std.concurrency.Generator;

void fibonacciSerisi() {
    int baştaki = 0;
    int sonraki = 1;

    while (true) {
        yield(baştaki);

        const ikiSonraki = baştaki + sonraki;
        baştaki = sonraki;
        sonraki = ikiSonraki;
    }
}

void main() {
    auto seri = new FiberAralığı!int(&fibonacciSerisi);
    writefln("%(%s %)", seri.take(10));
}

Sonuçta, bir fiber işlevinin ürettiği elemanlar kolayca bir InputRange aralığı olarak kullanılabilmektedir:

0 1 1 2 3 5 8 13 21 34

Ağaç elemanlarına InputRange arayüzü vermek için de Generator'dan yararlanılabilir. Dahası, InputRange arayüzü bulunan bir ağacın yazdır işlevine de artık gerek kalmaz. Aşağıdaki düğümleri işlevinin sonrakiDüğüm'ü çağıran bir isimsiz işlev oluşturduğuna ve Generator'a o isimsiz işlevi verdiğine dikkat edin:

import std.concurrency;

alias FiberAralığı = std.concurrency.Generator;

struct Düğüm {
// ...

    /* Not: Gerekmeyen yazdır() işlevi çıkartılmıştır. */

    auto opSlice() const {
        return düğümleri(&this);
    }
}

/* Bu fiber işlevi eleman değerine göre sıralı olarak bir
 * sonraki düğümü üretir. */
void sonrakiDüğüm(const(Düğüm) * düğüm) {
    if (!düğüm) {
        /* Bu düğümün kendisinde veya altında eleman yok */
        return;
    }

    sonrakiDüğüm(düğüm.sol);    // Önce soldaki elemanlar
    yield(düğüm);               // Şimdi bu eleman
    sonrakiDüğüm(düğüm.sağ);    // Sonra sağdaki elemanlar
}

/* Ağacın düğümlerinden oluşan bir InputRange döndürür. */
auto düğümleri(const(Düğüm) * düğüm) {
    return new FiberAralığı!(const(Düğüm)*)(
        () => sonrakiDüğüm(düğüm));
}

// ...

struct Ağaç {
// ...

    /* Not: Gerekmeyen yazdır() işlevi çıkartılmıştır. */

    auto opSlice() const {
        /* Düğümlerden eleman değerlerine dönüşüm. */
        return düğümleri(this).map!(d => d.eleman);
    }
}

/* Ağacın düğümlerinden oluşan bir InputRange döndürür. Ağaçta
 * eleman bulunmadığında (yani, 'kök' 'null' olduğunda) boş
 * aralık döndürür. */
auto düğümleri(const(Ağaç) ağaç) {
    if (ağaç.kök) {
        return düğümleri(ağaç.kök);

    } else {
        alias AralıkTürü = typeof(return);
        return new AralıkTürü(() {});    // ← Boş aralık
    }
}

Artık Ağaç nesneleri [] işleciyle dilimlenebilirler ve InputRange olarak kullanılabilirler:

    writefln("%(%s %)", ağaç[]);
Fiberlerin zaman uyumsuz giriş/çıkış işlemlerinde kullanılmaları

Fiberlerin çağrı yığıtları zaman uyumsuz giriş/çıkış işlemlerini de kolaylaştırır.

Bunun bir örneğini görmek için kullanıcıların sırayla isim, e-posta, ve yaş bilgilerini girerek kayıt oldukları bir servis düşünelim. Bu örneği bir internet sitesinin üye kayıt iş akışına (flow) benzetebiliriz. Örneği kısa tutmak için bir internet sunucusu yerine kullanıcılarla komut satırı üzerinden etkileşen bir program yazalım. Bu etkileşim girilen bilgilerin işaretli olarak gösterildikleri aşağıdaki protokolü kullanıyor olsun:

Örneğin, Ayşe ve Barış adlı iki kullanıcının etkileşimleri aşağıdaki gibi olabilir. Kullanıcıların girdikleri veriler işaretli olarak gösterilmiştir. Her kullanıcı bağlandıktan sonra isim, e-posta, ve yaş bilgisini girmektedir:

> merhaba                   ← Ayşe bağlanır
0 numaralı akış başladı.
> 0 Ayşe
> 0 ayse@example.com
> 0 20                      ← Ayşe kaydını tamamlar
Akış 0 tamamlandı.
'Ayşe' eklendi.
> merhaba                   ← Barış bağlanır
1 numaralı akış başladı.
> 1 Barış
> 1 baris@example.com
> 1 30                      ← Barış kaydını tamamlar
Akış 1 tamamlandı.
'Barış' eklendi.
> son
Güle güle.
Kullanıcılar:
  Kullanıcı("Ayşe", "ayse@example.com", 20)
  Kullanıcı("Barış", "baris@example.com", 30)

Bu programı merhaba komutunu bekleyen ve kullanıcı verileri için bir işlev çağıran bir tasarımla gerçekleştirebiliriz:

    if (giriş == "merhaba") {
        yeniKullanıcıKaydet();  // ← UYARI: Giriş tıkayan tasarım
    }

Eğer program eş zamanlı programlama yöntemleri kullanmıyorsa, yukarıdaki gibi bir tasarım girişi tıkayacaktır (block) çünkü bağlanan kullanıcının verileri tamamlanmadan program başka kullanıcı kabul edemez. Verilerini dakika mertebesinde giren kullanıcılar fazla yüklü olmayan bir sunucuyu bile kullanışsız hale getirecektir.

Böyle bir servisin tıkanmadan işlemesini (yani, birden fazla kullanıcının kayıt işlemlerinin aynı anda sürdürülebilmesini) sağlayan çeşitli tasarımlar düşünülebilir:

Her kayıt akışı için ayrı fiber kullanan bir yöntem de düşünülebilir. Bunun yararı, akışın doğrusal olarak ve kullanıcı protokolüne tam uygun olarak yazılabilmesidir: önce isim, sonra e-posta, ve son olarak yaş. Aşağıdaki başlangıç işlevinin akışın durumunu saklamak için değişken tanımlamak zorunda kalmadığına dikkat edin. Her call çağrısı bir önceki Fiber.yield'in kaldığı yerden devam eder; bir sonra işletilecek olan işlem, çağrı yığıtı tarafından üstü kapalı olarak saklanmaktadır.

Önceki örneklerden farklı olarak, aşağıdaki programdaki fiber, Fiber'in alt sınıfı olarak tanımlanmıştır:

import std.stdio;
import std.string;
import std.format;
import std.exception;
import std.conv;
import std.array;
import core.thread;

struct Kullanıcı {
    string isim;
    string eposta;
    uint yaş;
}

/* Bu alt sınıf kullanıcı kayıt akışını temsil eder. */
class KayıtAkışı : Fiber {
    /* Bu akış için en son okunmuş olan veri. */
    string veri_;

    /* Kullanıcı nesnesi kurmak için gereken bilgi. */
    string isim;
    string eposta;
    uint yaş;

    this() {
        /* Fiberin başlangıç noktası olarak 'başlangıç' üye
         * işlevini belirtiyoruz. */
        super(&başlangıç);
    }

    void başlangıç() {
        /* İlk girilen veri isimdir. */
        isim = veri_;
        Fiber.yield();

        /* İkinci girilen veri e-postadır. */
        eposta = veri_;
        Fiber.yield();

        /* Sonuncu veri yaştır. */
        yaş = veri_.to!uint;

        /* Bu noktada Kullanıcı nesnesi oluşturacak bütün
         * veriyi toplamış bulunuyoruz. 'Fiber.yield()' ile
         * duraksamak yerine artık işlevin sonlanmasını
         * istiyoruz. (Burada açıkça 'return' deyimi de
         * olabilirdi.) Bunun sonucunda bu fiberin durumu
         * Fiber.State.TERM değerini alır. */
    }

    /* Bu nitelik işlevi çağıranın veri girmesi içindir. */
    void veri(string yeniVeri) {
        veri_ = yeniVeri;
    }

    /* Bu nitelik işlevi kurulan nesneyi çağırana vermek
     * içindir. */
    Kullanıcı kullanıcı() const {
        return Kullanıcı(isim, eposta, yaş);
    }
}

/* Belirli bir akış için girişten okunmuş olan veriyi temsil
 * eder. */
struct AkışVerisi {
    size_t numara;
    string yeniVeri;
}

/* Belirtilen satırdan akış verisi okur. */
AkışVerisi akışVerisiOku(string satır) {
    size_t numara;
    string yeniVeri;

    const adet =
        satır.formattedRead!" %s %s"(numara, yeniVeri);

    enforce(adet == 2,
            format("Geçersiz veri: '%s'.", satır));

    return AkışVerisi(numara, yeniVeri);
}

void main() {
    Kullanıcı[] kullanıcılar;
    KayıtAkışı[] akışlar;

    bool bitti_mi = false;

    while (!bitti_mi) {
        write("> ");
        string satır = readln.strip;

        switch (satır) {
        case "merhaba":
            /* Yeni bağlanan kullanıcı için yeni akış
             * oluşturalım. */
            akışlar ~= new KayıtAkışı();

            writefln("%s numaralı akış başladı.",
                     akışlar.length - 1);
            break;

        case "son":
            /* Programdan çıkalım. */
            bitti_mi = true;
            break;

        default:
            /* Girilen satırı akış verisi olarak kullanmaya
             * çalışalım. */
            try {
                auto kullanıcı = veriİşle(satır, akışlar);

                if (!kullanıcı.isim.empty) {
                    kullanıcılar ~= kullanıcı;
                    writefln("'%s' eklendi.", kullanıcı.isim);
                }

            } catch (Exception hata) {
                writefln("Hata: %s", hata.msg);
            }
            break;
        }
    }

    writeln("Güle güle.");
    writefln("Kullanıcılar:\n%(  %s\n%)", kullanıcılar);
}

/* Girilen verinin ait olduğu fiberi belirler, yeni verisini
 * bildirir, ve o fiberin işleyişini kaldığı yerden devam
 * ettirir. Eğer girilen son veri üzerine akış sonlanmışsa,
 * üyeleri geçerli değerlerden oluşan bir Kullanıcı nesnesi
 * döndürür. */
Kullanıcı veriİşle(string satır, KayıtAkışı[] akışlar) {
    const akışVerisi = akışVerisiOku(satır);
    const numara = akışVerisi.numara;

    enforce(numara < akışlar.length,
            format("Geçersiz numara: %s.", numara));

    auto akış = akışlar[numara];

    enforce(akış.state == Fiber.State.HOLD,
            format("Akış %s işletilebilir durumda değil.",
                   numara));

    /* Akışa yeni verisini bildir. */
    akış.veri = akışVerisi.yeniVeri;

    /* Akışı kaldığı yerden devam ettir. */
    akış.call();

    Kullanıcı kullanıcı;

    if (akış.state == Fiber.State.TERM) {
        writefln("Akış %s tamamlandı.", numara);

        /* Dönüş değerine yeni oluşturulan kullanıcıyı ata. */
        kullanıcı = akış.kullanıcı;

        /* Sonrası için fikir: 'akışlar' dizisinin artık işi
         * bitmiş olan bu elemanı yeni bağlanacak olan
         * kullanıcılar için kullanılabilir. Ancak, önce
         * 'akış.reset()' ile tekrar başlatılabilir duruma
         * getirilmesi gerekir. */
    }

    return kullanıcı;
}

main işlevi girişten satırlar okur, onları ayrıştırır, ve veriyi işlenmek üzere ilgili akışa bildirir. Her akışın durumu kendi çağrı yığıtı tarafından otomatik olarak bilinmektedir. Yeni kullanıcılar bilgileri tamamlandıkça sisteme eklenirler.

Yukarıdaki programı çalıştırdığınızda kullanıcıların bilgi girme hızlarından bağımsız olarak sistemin her zaman için yeni kullanıcı kabul ettiğini göreceksiniz. Aşağıdaki örnekte Ayşe'nin etkileşimi işaretlenmiştir:

> merhaba                   ← Ayşe bağlanır
0 numaralı akış başladı.
> 0 Ayşe
> merhaba                   ← Barış bağlanır
1 numaralı akış başladı.
> merhaba                   ← Can bağlanır
2 numaralı akış başladı.
> 0 ayse@example.com
> 1 Barış
> 2 Can
> 2 can@example.com
> 2 40                      ← Can kaydını tamamlar
Akış 2 tamamlandı.
'Can' eklendi.
> 1 baris@example.com
> 1 30                      ← Barış kaydını tamamlar
Akış 1 tamamlandı.
'Barış' eklendi.
> 0 20                      ← Ayşe kaydını tamamlar
Akış 0 tamamlandı.
'Ayşe' eklendi.
> son
Güle güle.
Kullanıcılar:
  Kullanıcı("Can", "can@example.com", 40)
  Kullanıcı("Barış", "baris@example.com", 30)
  Kullanıcı("Ayşe", "ayse@example.com", 20)

Önce Ayşe, sonra Barış, ve en son Can bağlandıkları halde kayıt işlemlerini farklı sürelerde tamamlamışlardır. Sonuçta kullanıcılar dizisinin elemanları tamamlanan akış sırasına göre eklenmiştir.

Fiberlerin bu programa bir yararı, KayıtAkışı.başlangıç işlevinin kullanıcı giriş hızlarından bağımsız olarak basitçe yazılabilmiş olmasıdır. Ek olarak, başka akışlardan bağımsız olarak her zaman için yeni kullanıcı kabul edilebilmektedir.

vibe.d gibi çok sayıdaki zaman uyumsuz giriş/çıkış çatısı da fiberler üzerine kurulu tasarımlardan yararlanır.

Fiberler ve hata yönetimi

Hata Yönetimi bölümünde "alt düzey bir işlevden atılan bir hatanın teker teker o işlevi çağıran üst düzey işlevlere geçtiğini" görmüştük. Hiçbir düzeyde yakalanmayan bir hatanın ise "main'den de çıkılmasına ve programın sonlanmasına" neden olduğunu görmüştük. O bölümde hiç çağrı yığıtından bahsedilmemiş olsa da hata atma düzeneği de çağrı yığıtından yararlanır.

Bu bölümün ilk örneğinden devam edersek, bar içinde bir hata atıldığında çağrı yığıtından önce bar'ın çerçevesi çıkartılır, ondan sonra foo'nunki, ve en sonunda da main'inki. İşlevler sonlanırken çerçevelerinin çağrı yığıtından çıkartılması sırasında o işlevlerin yerel değişkenlerinin sonlandırıcı işlevleri de işletilir. İşlevlerden hata atılması üzerine çıkılması ve sonlandırıcıların işletilmesine yığıt çözülmesi denir.

Fiberlerin kendi çağrı yığıtları olduğundan, atılan hata da fiberin kendi çağrı yığıtını etkiler, fiberi çağıran kodun çağrı yığıtını değil. Hata yakalanmadığında ise fiber işlevinden de çıkılmış olur ve fiberin durumu Fiber.State.TERM değerini alır.

Bu, bazı durumlarda tam da istenen davranış olabileceği gibi, bazen fiberin kaldığı yeri kaybetmeden hata durumunu bildirmesi istenebilir. Fiber.yieldAndThrow, fiberin kendisini duraksatmasını ve hemen ardından çağıranın kapsamında bir hata atmasını sağlar.

Bundan nasıl yararlanılabileceğini görmek için yukarıdaki kayıt programına geçersiz yaş bilgisi verelim:

> merhaba
0 numaralı akış başladı.
> 0 Ayşe
> 0 ayse@example.com
> 0 selam                ← kullanıcı geçersiz yaş bilgisi girer
Hata: Unexpected 's' when converting from type string to type uint
> 0 20                   ← hatasını düzeltmeye çalışır
Hata: Akış 0 işletilebilir durumda değil. ← ama fiber sonlanmıştır

Fiberin sonlanması nedeniyle bütün kullanıcı akışının kaybedilmesi yerine, fiber atılan dönüşüm hatasını yakalayabilir ve kendisini çağırana yieldAndThrow ile bildirebilir. Bunun için yaş bilgisinin dönüştürüldüğü aşağıdaki satırın değiştirilmesi gerekir:

        yaş = veri_.to!uint;

O satırın koşulsuz bir döngüdeki bir try-catch deyimi içine alınması, uint'e dönüştürülebilecek veri gelene kadar fiberi canlı tutacaktır:

        while (true) {
            try {
                yaş = veri_.to!uint;
                break;  // ← Dönüştürüldü; döngüden çıkalım

            } catch (ConvException hata) {
                Fiber.yieldAndThrow(hata);
            }
        }

Bu sefer, geçerli veri gelene kadar döngü içinde kalınır:

> merhaba
0 numaralı akış başladı.
> 0 Ayşe
> 0 ayse@example.com
> 0 selam                ← kullanıcı geçersiz yaş bilgisi girer
Hata: Unexpected 's' when converting from type string to type uint
> 0 dünya                ← tekrar geçersiz yaş bilgisi girer
Hata: Unexpected 'd' when converting from type string to type uint
> 0 20                   ← sonunda doğru bilgi girer
Akış 0 tamamlandı.
'Ayşe' eklendi.
> son
Güle güle.
Kullanıcılar:
  Kullanıcı("Ayşe", "ayse@example.com", 20)

Programın çıktısında görüldüğü gibi, artık akış hata nedeniyle sonlanmaz ve kullanıcı sisteme eklenmiş olur.

İşbirlikli çoklu görevler

İşletim sisteminin sunduğu çoklu görev olanağı iş parçacıklarını belirsiz zamanlarda duraksatmaya ve tekrar başlatmaya dayanır. Fiberler ise kendilerini istedikleri zaman duraksatırlar ve çağıranları tarafından tekrar başlatılırlar. Bu ayrıma göre, işletim sisteminin sunduğu çoklu görev sistemine geçişli çoklu görev, fiberlerin sunduğuna ise işbirlikli çoklu görev denir.

Geçişli çoklu görev sistemlerinde işletim sistemi başlattığı her iş parçacığına belirli bir süre ayırır. O süre dolduğunda iş parçacığı duraksatılır ve başka bir iş parçacığına geçilir. Bir iş parçacığından başkasına geçmeye bağlam değiştirme denir. Bağlam değiştirme göreceli olarak masraflı bir işlemdir.

Sistemler genelde çok sayıda iş parçacığı işlettiklerinden bağlam değiştirme hem kaçınılmazdır hem de programların kesintisiz işlemeleri açısından istenen bir durumdur. Ancak, bazı iş parçacıkları ayrılan süreleri daha dolmadan kendilerini duraksatma gereği duyarlar. Bu durum, bir iş parçacığının başka bir iş parçacığından veya bir cihazdan veri beklediği zamanlarda oluşabilir. Bir iş parçacığı kendisini durdurduğunda işletim sistemi başka bir iş parçacığına geçmek için yeniden bağlam değiştirmek zorundadır. Sonuçta, mikro işlemcinin iş gerçekleştirmek amacıyla ayırdığı sürenin bir bölümü bağlam değiştirmek için harcanmıştır.

Fiberlerde ise fiber ve onu çağıran kod aynı iş parçacığı üzerinde işletilirler. (Fiber ve çağıranının aynı anda işletilmemelerinin nedeni budur.) Bunun bir yararı, ikisi arasındaki geçişlerde bağlam değiştirme masrafının bulunmamasıdır. (Yine de işlev çağırma masrafı kadar küçük olan bir masraf vardır.)

İşbirlikli çoklu görevlerin başka bir yararı, fiberle çağıranı arasında iletilen verinin mikro işlemcinin önbelleğinde bulunma olasılığının daha yüksek olmasıdır. Önbelleğe erişmek sistem belleğine erişmekten yüzlerce kat hızlı olduğundan, fiberler iş parçacıklarından çok daha hızlı işleyebilirler.

Dahası, fiber ve çağıranı aynı anda işlemediklerinden, veri erişiminde yarış hali de söz konusu değildir. Dolayısıyla, synchronized gibi olanaklar kullanılması da gerekmez. Ancak, programcı yine de fiberin gereğinden erken duraksatılmadığından emin olmalıdır. Örneğin, aşağıdaki işlev() çağrısı sırasında Fiber.yield çağrılmamalıdır çünkü paylaşılanVeri'nin değeri o sırada henüz ikiye katlanmamıştır:

void fiberİşlevi() {
    // ...

        işlev();              // ← fiberi duraksatmamalıdır
        paylaşılanVeri *= 2;
        Fiber.yield();        // ← istenen duraksatma noktası

    // ...
}

Fiberlerin bariz bir yetersizliği, fiber ve çağıranının tek çekirdek üzerinde işliyor olmalarıdır. Mikro işlemcinin boşta bekleyen çekirdekleri olduğunda bu durum kaynak savurganlığı anlamına gelir. Bunun önüne geçmek için M:N iş parçacığı modeli (M:N threading model) gibi çeşitli yöntemlere başvurulabilir. Bu yöntemleri kendiniz araştırmanızı öneririm.

Özet