PHP 8.1 ile gelen Fiber sınıfı, çalışmasını istediğiniz noktada duraklatıp daha sonra kaldığı yerden sürdürebileceğiniz bir fonksiyonu temsil eder. Kılavuz fiber'ları "tam yığınlı, kesilebilir fonksiyonlar" olarak tanımlar: her fiber'ın kendi çağrı yığını vardır, bu yüzden Fiber::suspend() çağrısı fiber'ın içinden çağrılan, hatta çok derinde kalmış bir fonksiyondan bile yapılabilir. Fiber'ı start() ile başlatırsınız, fiber kendini Fiber::suspend() ile askıya alır, siz de resume() ile bir değer vererek ya da throw() ile bir istisna fırlatarak onu devam ettirirsiniz.

Bu yazıda fiber'lar arasında değerlerin nasıl taşındığını, durum sorgulama metotlarını, üreteçlerden (generator) temel farkını, birkaç fiber'ı sırayla çalıştıran basit bir zamanlayıcıyı, istisnaların fiber sınırını nasıl geçtiğini ve FiberError fırlatan hatalı kullanımları PHP 8.5.12 ile çalıştırılmış örneklerle görüyorsunuz. Fiber'lar üreteçlerle birlikte düşünüldüğünde daha kolay anlaşılır; önce PHP generator ve yield yazısını okumanız faydalı olabilir.

Temel kullanım: start, suspend, resume

Temel kullanım: start, suspend, resume: start(...$args), Fiber::suspend($deger), resume($deger), throw($istisna)

Bir fiber, kurucuya verilen bir callable ile oluşturulur. Oluşturmak hiçbir kodu çalıştırmaz; callable ancak start() çağrıldığında, start() fonksiyonuna verilen argümanlarla çalışmaya başlar.

php
<?php
$fiber = new Fiber(function (string $ad): string {
    echo "Fiber başladı: $ad\n";
    $gelen = Fiber::suspend('ilk durak');
    echo "Fiber devam ediyor, gelen: $gelen\n";
    $gelen2 = Fiber::suspend('ikinci durak');
    echo "Fiber son kez devam: $gelen2\n";
    return 'bitti';
});

$deger = $fiber->start('işçi-1');
echo "Ana kod aldı: $deger\n";

$deger = $fiber->resume('merhaba');
echo "Ana kod aldı: $deger\n";

$deger = $fiber->resume('güle güle');
var_dump($deger);
echo 'Dönüş: ', $fiber->getReturn(), "\n";
text
Fiber başladı: işçi-1
Ana kod aldı: ilk durak
Fiber devam ediyor, gelen: merhaba
Ana kod aldı: ikinci durak
Fiber son kez devam: güle güle
NULL
Dönüş: bitti

Değerlerin akışı başlangıçta kafa karıştırabilir, ama kural simetriktir. Fiber içinde Fiber::suspend($x) çağrıldığında $x, fiber'ı o an çalıştıran start() veya resume() çağrısının dönüş değeri olur. Ana kod resume($y) çağırdığında da $y, fiber içinde bekleyen Fiber::suspend() çağrısının dönüş değeri olur. Fiber callable'ı return ile bittiğinde son resume() çağrısı null döndürür; asıl dönüş değeri getReturn() ile okunur.

ÇağrıNeredeNe döndürür
start(...$args)Ana kodİlk Fiber::suspend() değerini ya da fiber bittiyse null
Fiber::suspend($deger)Fiber içiFiber'ı devam ettiren resume() argümanını
resume($deger)Ana kodBir sonraki Fiber::suspend() değerini ya da fiber bittiyse null
throw($istisna)Ana kodresume() gibi; istisna suspend() noktasından fırlar
getReturn()Ana kodFiber callable'ının return değerini (yalnızca bittikten sonra)
Fiber::getCurrent()Her yerÇalışan Fiber nesnesini ya da fiber dışındaysa null

Burada önemli bir gözlem var: start() ve resume() çağrıları, fiber askıya alınana ya da bitene kadar geri dönmez. Yani aynı anda yalnızca bir kod parçası çalışır; fiber'lar iş parçacığı (thread) değildir ve kodu paralel çalıştırmaz. Kontrolün ne zaman el değiştireceğine tamamen siz karar verirsiniz. Bu işbirlikçi (cooperative) model, kilitler ve yarış durumlarıyla uğraşmadan birden çok işi aynı süreç içinde dönüşümlü ilerletmenizi sağlar.

Fiber'ın durumunu sorgulamak

Bir fiber yaşamı boyunca birkaç durumdan geçer. Dört metot bu durumu sorgular: isStarted(), isSuspended(), isRunning() ve isTerminated(). Aşağıdaki örnek her aşamada dördünü birden yazdırıyor:

php
<?php
function durum(Fiber $f): string
{
    return sprintf('başladı=%s askıda=%s çalışıyor=%s bitti=%s',
        var_export($f->isStarted(), true), var_export($f->isSuspended(), true),
        var_export($f->isRunning(), true), var_export($f->isTerminated(), true));
}

$f = new Fiber(function () use (&$f): void {
    echo 'içeride:  ', durum($f), "\n";
    Fiber::suspend();
});

echo 'önce:     ', durum($f), "\n";
$f->start();
echo 'askıda:   ', durum($f), "\n";
$f->resume();
echo 'sonra:    ', durum($f), "\n";
text
önce:     başladı=false askıda=false çalışıyor=false bitti=false
içeride:  başladı=true askıda=false çalışıyor=true bitti=false
askıda:   başladı=true askıda=true çalışıyor=false bitti=false
sonra:    başladı=true askıda=false çalışıyor=false bitti=true

Kılavuzdaki tanımlar çıktıyla birebir örtüşüyor. isStarted() fiber başlatıldıktan sonra hep true kalır. isRunning() yalnızca henüz geri dönmemiş bir start(), resume() veya throw() çağrısı varken true olur; bu yüzden onu yalnızca fiber'ın içinden true olarak görebilirsiniz. isTerminated() ise fiber normal şekilde döndüğünde ya da bir istisna fırlatarak bittiğinde true olur. Bir zamanlayıcı yazarken en çok kullanacağınız ikili isSuspended() ve isTerminated() olacaktır.

Derindeki bir fonksiyondan askıya almak

Fiber'ların asıl gücü burada ortaya çıkar. Kılavuza göre Fiber::suspend() çağrı yığınının herhangi bir yerinden yapılabilir; askıya alma noktasını tanımlayan fonksiyonun dönüş tipini değiştirmesi gerekmez. Fiber'lar, array_map() gibi PHP'nin kendi fonksiyonlarına verilen callable'ların ya da bir Iterator nesnesi üzerinde foreach tarafından çağrılan metotların içinden bile askıya alınabilir.

php
<?php
function logla(string $mesaj): void
{
    // Derindeki bir fonksiyon, imzası değişmeden fiber'ı askıya alır.
    echo "log: $mesaj\n";
    Fiber::suspend($mesaj);
}

function isle(array $kayitlar): int
{
    $adet = 0;
    array_walk($kayitlar, function (string $k) use (&$adet) {
        logla("$k işlendi");
        $adet++;
    });
    return $adet;
}

$f = new Fiber(fn() => isle(['A', 'B', 'C']));
$durak = $f->start();
while (!$f->isTerminated()) {
    echo "  ana kod araya girdi ($durak)\n";
    $durak = $f->resume();
}
echo 'Toplam: ', $f->getReturn(), "\n";
text
log: A işlendi
  ana kod araya girdi (A işlendi)
log: B işlendi
  ana kod araya girdi (B işlendi)
log: C işlendi
  ana kod araya girdi (C işlendi)
Toplam: 3

logla() sıradan bir void fonksiyon. array_walk() tarafından çağrılan bir closure'ın içinden çağrılıyor ve her seferinde tüm yığını, yani isle(), array_walk() ve closure çerçevelerini birlikte donduruyor. Ana kod her durakta araya girip bir şey yapabiliyor, sonra resume() ile fiber'ı kaldığı yerden sürdürüyor. Aynı şeyi bir üreteçle yapmak mümkün olmazdı: logla() içine yield yazsaydınız o fonksiyon bir üretece dönüşür ve çağrıldığında hiç çalışmazdı.

Generator ile Fiber arasındaki fark

Generator ile Fiber arasındaki fark: Duraklatma noktası, Fonksiyon imzasına etkisi, Dışarıya değer verme, İçeriye değer gönderme

İki yapı da bir fonksiyonu duraklatıp sürdürmeye yarar, ama kılavuzun vurguladığı temel fark yığındır: üreteçler yığınsızdır (stackless), fiber'ların ise kendi çağrı yığını vardır. Aşağıdaki örnek bu farkın pratikte ne anlama geldiğini gösteriyor:

php
<?php
function uretec(): Generator
{
    yield 1;
    yardimci();   // yield içermeyen fonksiyondan askıya alma yapılamaz
    yield 2;
}

function yardimci(): void
{
    echo "yardımcı çalıştı, ama burada yield kullanılamaz\n";
}

foreach (uretec() as $v) {
    echo "üreteç: $v\n";
}

$f = new Fiber(function (): void {
    echo "fiber: 1\n";
    fiberYardimci();
    echo "fiber: 2\n";
});

function fiberYardimci(): void
{
    Fiber::suspend();   // derindeki fonksiyon da fiber'ı durdurabilir
}

$f->start();
echo "ana kod: fiber yardımcıda durdu\n";
$f->resume();
text
üreteç: 1
yardımcı çalıştı, ama burada yield kullanılamaz
üreteç: 2
fiber: 1
ana kod: fiber yardımcıda durdu
fiber: 2

Üreteçte yardimci() fonksiyonu duraklatma yapamadı; yalnızca çalışıp bitti. Duraklatma ancak üretecin kendi gövdesindeki yield noktalarında olabilir. Fiber örneğinde ise duraklatma, fiber'ın çağırdığı ayrı bir fonksiyonun içinde gerçekleşti ve ana kod araya girebildi.

ÖzellikGeneratorFiber
Duraklatma noktasıYalnızca kendi gövdesindeki yieldÇağrı yığınının herhangi bir yeri
Fonksiyon imzasına etkisiDönüş değeri Generator olurİmza değişmez
Dışarıya değer vermeyield $degerFiber::suspend($deger)
İçeriye değer göndermesend($deger)resume($deger)
İçeriye istisna fırlatmathrow($istisna)throw($istisna)
foreach ile dolaşmaEvet (Iterator uygular)Hayır
Örnek kullanımTembel veri akışı, yineleyicilerİşleri dönüşümlü ilerleten zamanlayıcılar
SürümPHP 5.5PHP 8.1

Basit bir kural olarak: değer üreten ve foreach ile tüketilen bir veri kaynağı istiyorsanız üreteç daha uygun ve daha okunaklıdır. Var olan, imzası değiştirilemeyen kodun ortasından kontrolü geri almanız gerekiyorsa fiber gerekir.

Basit bir zamanlayıcı: işleri sırayla ilerletmek

Fiber'larla en sık kurulan yapı, birkaç işi dönüşümlü olarak ilerleten bir zamanlayıcıdır (scheduler). Aşağıda üç iş farklı sayıda adımdan oluşuyor ve her adımın sonunda kontrolü geri veriyor. Ana döngü askıdaki her fiber'ı sırayla devam ettiriyor, biteni listeden çıkarıyor.

php
<?php
$isler = [];
foreach (['A' => 3, 'B' => 2, 'C' => 1] as $ad => $adim) {
    $isler[$ad] = new Fiber(function () use ($ad, $adim): string {
        for ($i = 1; $i <= $adim; $i++) {
            echo "$ad: adım $i/$adim\n";
            Fiber::suspend();
        }
        return "$ad tamam";
    });
}

foreach ($isler as $f) {
    $f->start();
}
while ($isler) {
    foreach ($isler as $ad => $f) {
        if ($f->isTerminated()) {
            echo $f->getReturn(), "\n";
            unset($isler[$ad]);
        } else {
            $f->resume();
        }
    }
}
text
A: adım 1/3
B: adım 1/2
C: adım 1/1
A: adım 2/3
B: adım 2/2
A: adım 3/3
C tamam
B tamam
A tamam

Çıktıdaki sıra, işlerin aynı anda değil dönüşümlü ilerlediğini gösteriyor: önce her iş birinci adımını atıyor, sonra kalanlar ikinci adımlarını atıyor. En kısa iş olan C, ikinci turda bittiği için tamamlanma mesajı ilk onun için yazılıyor. Bu fikri dışarıdan veri bekleyen işlere uygulamak isterseniz, verinin hazır olup olmadığını denetleyen ve uygun anda resume() çağıran bir döngüyü sizin (ya da kullandığınız kütüphanenin) yazması gerekir. Fiber kendi başına hiçbir işlemi beklemesiz hale getirmez; örneğin fiber içinde çağrılan sleep() tüm süreci bekletir.

Uygulamalı örnek: veriyi dışarıdan parça parça beslemek

Fiber'ların zamanlayıcı dışındaki bir kullanımı da kontrolün yönünü tersine çevirmektir. Normalde bir ayrıştırıcı veriyi kendisi okur. Aşağıdaki örnekte ayrıştırıcı (topla()) her yeni satıra ihtiyaç duyduğunda sonrakiSatir() fonksiyonunu çağırıyor; bu fonksiyon fiber'ı askıya alıyor ve satırın dışarıdan, resume() ile gelmesini bekliyor. Ayrıştırıcının kodu, veriyi sanki sıradan bir fonksiyondan okuyormuş gibi düz bir döngü olarak kalıyor.

php
<?php
function sonrakiSatir(): string
{
    return Fiber::suspend('veri bekleniyor');
}

function topla(): int
{
    $toplam = 0;
    while (($satir = sonrakiSatir()) !== '') {
        $toplam += (int) $satir;
        echo "  ayrıştırıcı: $satir eklendi, toplam $toplam\n";
    }
    return $toplam;
}

$f = new Fiber(topla(...));
echo 'Fiber diyor ki: ', $f->start(), "\n";

foreach (['10', '20', '12', ''] as $parca) {
    echo "Ana kod gönderiyor: '$parca'\n";
    $f->resume($parca);
}
echo 'Sonuç: ', $f->getReturn(), "\n";
text
Fiber diyor ki: veri bekleniyor
Ana kod gönderiyor: '10'
  ayrıştırıcı: 10 eklendi, toplam 10
Ana kod gönderiyor: '20'
  ayrıştırıcı: 20 eklendi, toplam 30
Ana kod gönderiyor: '12'
  ayrıştırıcı: 12 eklendi, toplam 42
Ana kod gönderiyor: ''
Sonuç: 42

Akışı izleyelim. start() fiber'ı başlattı; ayrıştırıcı ilk satırı istedi ve fiber 'veri bekleniyor' değeriyle askıya alındı, bu değer de start() çağrısının sonucu oldu. Ana kod her resume() çağrısında bir parça gönderdi; bu parça fiber içindeki Fiber::suspend() çağrısının dönüş değeri, dolayısıyla sonrakiSatir() fonksiyonunun sonucu oldu. Boş metin gönderildiğinde döngü bitti, fiber 42 değerini döndürerek sonlandı ve son resume() çağrısı null döndürdü. Fiber'a verilen callable burada topla(...) biçimindeki first-class callable sözdizimiyle oluşturuldu; bu sözdizimi PHP 8.1 ile geldi.

Aynı ayrıştırıcıyı bir üreteçle yazmak isteseydiniz sonrakiSatir() çağrısının yerine doğrudan yield yazmanız gerekirdi; yardımcı fonksiyonu korumanın yolu olmazdı ve topla() artık bir tamsayı değil bir Generator döndürürdü. Fiber'ın sağladığı şey tam olarak budur: ayrıştırıcının imzası ve iç yapısı değişmeden veri kaynağı dışarıya taşınır.

Fiber ve istisnalar

İstisnalar fiber sınırını iki yönde de geçebilir. Ana kod throw() ile fiber'ı bir istisnayla devam ettirebilir; istisna fiber içindeki Fiber::suspend() çağrısından fırlar ve fiber onu yakalayabilir. Ters yönde, fiber içinde yakalanmayan bir istisna fiber'ı sonlandırır ve onu çalıştıran start(), resume() veya throw() çağrısından dışarı fırlar.

php
<?php
$f = new Fiber(function (): void {
    try {
        Fiber::suspend('bekliyorum');
    } catch (RuntimeException $e) {
        echo 'Fiber içinde yakalandı: ', $e->getMessage(), "\n";
    }
    echo "Fiber temizlik yapıp bitiyor\n";
});

$f->start();
$f->throw(new RuntimeException('zaman aşımı'));
var_dump($f->isTerminated());

$g = new Fiber(function (): void {
    Fiber::suspend();
    throw new LogicException('fiber içinde patladı');
});
$g->start();
try {
    $g->resume();
} catch (LogicException $e) {
    echo 'Ana kodda yakalandı: ', $e->getMessage(), "\n";
}
text
Fiber içinde yakalandı: zaman aşımı
Fiber temizlik yapıp bitiyor
bool(true)
Ana kodda yakalandı: fiber içinde patladı

Birinci fiber zaman aşımı istisnasını kendi içinde yakaladı, temizliğini yaptı ve normal şekilde bitti. İkinci fiber istisnayı yakalamadı; istisna ana koddaki resume() satırından fırladı ve oradaki try/catch tarafından yakalandı. Bu durumda fiber sonlanmış sayılır ve bir dönüş değeri yoktur; getReturn() çağrısı FiberError fırlatır. İstisnaların genel kuralları için PHP try/catch ve exception yazısına bakabilirsiniz.

FiberError: geçersiz işlemler

FiberError, Error sınıfından türeyen final bir sınıftır ve fiber üzerinde geçersiz bir işlem yapıldığında fırlatılır. Kurucusu doğrudan örnek oluşturmayı engellemek için vardır; bu sınıfı siz fırlatmazsınız, PHP fırlatır. En sık karşılaşılan durumlar aşağıdaki örnekte:

php
<?php
$f = new Fiber(function (): int {
    Fiber::suspend();
    return 7;
});

$denemeler = [
    'start öncesi getReturn' => fn() => $f->getReturn(),
    'start öncesi resume'    => fn() => $f->resume(),
    'fiber dışında suspend'  => fn() => Fiber::suspend(),
];
foreach ($denemeler as $ad => $is) {
    try {
        $is();
    } catch (FiberError $e) {
        echo "$ad: ", $e->getMessage(), "\n";
    }
}

$f->start();
try {
    $f->start();
} catch (FiberError $e) {
    echo 'ikinci start: ', $e->getMessage(), "\n";
}
$f->resume();
echo 'dönüş: ', $f->getReturn(), "\n";
var_dump(Fiber::getCurrent());
text
start öncesi getReturn: Cannot get fiber return value: The fiber has not been started
start öncesi resume: Cannot resume a fiber that is not suspended
fiber dışında suspend: Cannot suspend outside of a fiber
ikinci start: Cannot start a fiber that has already been started
dönüş: 7
NULL

Mesajlar kendi kendini açıklıyor: başlamamış bir fiber'ın dönüş değeri okunamaz, askıda olmayan bir fiber devam ettirilemez, fiber dışında Fiber::suspend() çağrılamaz ve bir fiber iki kez başlatılamaz. Son satır da Fiber::getCurrent() fonksiyonunun ana kodda null döndürdüğünü gösteriyor; bir fonksiyonun bir fiber içinde çalışıp çalışmadığını bu yolla anlayabilirsiniz. FiberError bir Error olduğu için catch (Exception $e) bloğu onu yakalamaz.

Fiber::getCurrent() özellikle hem fiber içinde hem dışında çağrılabilecek yardımcı fonksiyonlarda işe yarar. Örneğin yukarıdaki logla() gibi bir fonksiyon, Fiber::getCurrent() !== null kontrolüyle yalnızca bir fiber içinde çalışıyorsa askıya alma yapabilir, ana koddan çağrıldığında ise FiberError fırlatmak yerine işini doğrudan bitirebilir. Böylece aynı fonksiyon iki bağlamda da güvenle kullanılır. Bu kontrolü atlamak, fiber dışında Fiber::suspend() çağrısının neden olduğu "Cannot suspend outside of a fiber" hatasının en yaygın kaynağıdır.

Sınırlar ve sürüm notları

Kılavuz ve sürüm geçiş notları iki önemli ayrıntı veriyor. Birincisi, PHP 8.4'ten önce bir nesnenin yıkıcı metodu (__destruct()) çalışırken fiber değiştirmek yasaktı; 8.4 ile bu kısıt kaldırıldı. 8.4 notlarına göre çöp toplayıcının (garbage collector) çağırdığı yıkıcılar artık ayrı bir fiber içinde çalıştırılabiliyor. İkincisi, Fiber sınıfı final olduğu için genişletilemez; davranış eklemek istiyorsanız fiber'ı kendi sınıfınızın içinde bir özellik olarak tutun.

Kılavuzun belirttiği gibi askıya alma noktası içeren bir fonksiyonun imzası değişmez. Bunun bir yan etkisi var: kullandığınız bir kütüphane arka planda fiber'larla çalışıyorsa bunu fonksiyon imzalarından anlayamazsınız. Mekanizmayı bilmek, bir istisnanın neden beklenmedik bir resume() satırından fırladığını ya da bir işin neden başka bir işin ortasında ilerlediğini anlamanıza yardım eder. Fiber'a verilen callable genellikle bir closure ya da arrow function olur; bunların use ile dış değişken yakalama kuralları için PHP anonim fonksiyon ve arrow function yazısına bakabilirsiniz.

Sık yapılan hatalar

Sık yapılan hatalar: Fiber'ın paralel çalıştığını sanmak., Bitmemiş fiber'da getReturn() çağırmak., Askıda olmayan fiber'ı devam ettirmek., FiberError'ı Exception ile yakalamaya...
  • Fiber'ın paralel çalıştığını sanmak. Aynı anda yalnızca bir fiber ya da ana kod çalışır; resume() fiber durana kadar geri dönmez.
  • Bitmemiş fiber'da getReturn() çağırmak. Önce isTerminated() ile kontrol edin; fiber istisnayla bittiyse dönüş değeri yoktur.
  • Askıda olmayan fiber'ı devam ettirmek. Bir zamanlayıcıda resume() öncesinde isSuspended() kontrolü yapın.
  • FiberError'ı Exception ile yakalamaya çalışmak. FiberError bir Error alt sınıfıdır; gerekiyorsa Throwable ya da FiberError yazın.
  • Fiber'dan engelleyen bir çağrının beklenmesiz olmasını beklemek. sleep() veya senkron bir dosya okuması fiber içinde de süreci bekletir.
  • Askıdaki fiber'ları yarım bırakmak. Hiç devam ettirilmeyen bir fiber'ın askıya alma noktasından sonraki kodu çalışmaz. PHP 8.5.12 ile yaptığımız denemede askıdaki fiber nesnesi yok edilirken finally bloğu çalıştı, ama kalan iş mantığı yarım kaldı; zamanlayıcınızın her fiber'ı sonuna kadar götürdüğünden emin olun.

Sık Sorulan Sorular

Fiber hangi PHP sürümünde geldi?

Fiber desteği PHP 8.1 ile eklendi. Destekleyen bir sürümde ek bir eklenti kurmanız gerekmez; Fiber sınıfı çekirdekte bulunur.

Fiber ile çok iş parçacıklı (multithread) programlama aynı şey mi?

Hayır. Fiber'lar aynı süreç ve aynı iş parçacığı içinde çalışır; kontrolü bilinçli olarak Fiber::suspend() ile devredersiniz. Bu yüzden iki fiber aynı anda aynı değişkeni değiştiremez ve kilit mekanizmasına ihtiyaç duyulmaz, ama işlemci çekirdeklerinden aynı anda yararlanılmaz.

Fiber içinde Fiber oluşturabilir miyim?

Evet. Bir fiber içinden başka bir fiber başlatıp devam ettirebilirsiniz. Fiber::suspend() her zaman o anda çalışan fiber'ı askıya alır ve kontrolü onu en son başlatan ya da devam ettiren çağrıya geri verir.

Generator mı kullanmalıyım, Fiber mı?

Bir veri akışını foreach ile tüketmek, büyük veriyi parça parça işlemek gibi ihtiyaçlarda üreteç daha basit ve yeterlidir. Var olan fonksiyonların imzasını değiştirmeden derinden duraklatma gerekiyorsa fiber kullanın. Karşılaştırmanın ayrıntısı yukarıdaki tabloda; üreteçler için generator yazısına bakabilirsiniz.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: PHP öğrenme rehberi · PHP generator ve yield · Anonim fonksiyon ve arrow function · PHP try/catch · TypeError ve diğer Error türleri