Ders 14 / 21
Kuyruklar ve İşler
İş sınıfı, kuyruğa koyma ve işçi; kuyruğa konan işin yanıt anında ertelenmesi, eşzamanlı sürücüde aynı işin koşması; ilk denemede başarısız olan işin yeniden denenmesi ve deneme hakkı bitince başarısız işler listesine düşmesi.
Yayın Güncelleme
Bu derste
- İş sınıfını, kuyruğa koymayı ve işçiyi birbirinden ayırır
- Kuyruğa konan işin yanıt anında ertelendiğini, eşzamanlı sürücüde aynı işin koştuğunu ölçer
- Yeniden deneme hakkının nasıl tüketildiğini ve başarısız işlerin nereye düştüğünü gösterir
İçindekiler
Kimlik Doğrulama ve Yetkilendirme dersinde kapı eylemi atladı ve yanıt hemen döndü. Kapı geçirdiğinde ise eylem koşar ve eylemin içinde yanıtı geciktirecek bir iş olabilir: bir görselin küçük resmini üretmek, bir raporu hazırlamak. Bu ders şunu sorar: bu iş yanıtı bekletmeli mi ve beklemiyorsa kim, ne zaman koşturur?
Önce adlandırma. Bu derste model aynı süreçtedir: ayrı bir sunucu, ayrı bir süreç ya da ağ yoktur. “Kuyruk” bir dizidir ve işçi (worker) bu diziden işleri alıp çalıştıran isle() çağrısının adıdır. İsteğin yanıtı döndükten sonra bu çağrı yapılır. Gerçek bir sistemde işçi ayrı bir süreç olurdu; burada ayrım yalnız yanıtın işi bekleyip beklemediğidir. Benzer bir soruyu JVM, Eşzamanlılık ve Başarım kursunda (M08/K04) görmüştük, ama orada adımı kimin yürüttüğü arandı: hangi iş parçacığının çalıştırdığı ölçüldü. Burada soru o değil; yanıtın işi bekleyip beklemediği.
US2. Kuyruk bir dizidir, işçi senkron bir döngüdür. Zaman aşımı, eşzamanlı işçiler, kilitleme ve iş sırasının kalıcılığı modellenmez; kuyruk sürücüsü adı kullanılmaz. Her deneme bir sayaçla izlenir ve sleep çağrılmaz: yeniden deneme aralığı gerçek zaman değil, deneme numarasıdır. İş içinde fırlatılan istisna işçide yakalanır ve yanıta ulaşmaz.
Ölçüm Çekirdeği
Kurs boyunca her derste aynı ölçüm kullanılır. kostu.php beklenen bir parçanın bu istekte koşup koşmadığını kaydeder; dosya her derste birebir verilir ve çıktı bloğu yoktur, yalnız diske yazılır. Gunluk üç liste tutar: isteğin yoluna konanlar, istek içinde çağrılanlar ve isteğin dışına bırakılanlar. sinifla() bunlara sırayla bakar. Karar sırası sinifla()’nın kendisidir: koştu, ertelendi, atlandı, bağlanmadı. Parçayı geliştirici adlandırır; ölçülen, geliştiricinin bu istekte koşmasını beklediği parçalardır.
<?php // kostu.php — beklenen bir parça bu istekte koştu mu? declare(strict_types=1); final class Gunluk { public static array $yol = []; // isteğin yoluna konan parçalar public static array $kostu = []; // istek içinde çağrılanlar public static array $ertelenen = []; // isteğin dışına bırakılanlar public static function yeniIstek(): void { self::$yol = self::$kostu = self::$ertelenen = []; } public static function yola(string $parca): void { self::$yol[] = $parca; } public static function kostu(string $parca): void { self::$kostu[] = $parca; } public static function ertele(string $parca): void { self::$ertelenen[] = $parca; } } function sinifla(string $parca): string { return match (true) { in_array($parca, Gunluk::$kostu, true) => 'koştu', in_array($parca, Gunluk::$ertelenen, true) => 'ertelendi', in_array($parca, Gunluk::$yol, true) => 'atlandı', default => 'bağlanmadı', }; }
Bu ders çekirdeğin üç çağrısını kullanır. Küçük resim işi yola konur (yola()), kuyruğa gönderilirken ertele() ile isteğin dışına bırakıldığı işaretlenir, eşzamanlı sürücüde ise kostu() ile çağrıldığı işaretlenir.
İş Sınıfı, Sürücü ve İşçi
isler.php dilimin tamamıdır. IsSinifi bir işin sözleşmesidir: bir ad(), bir calistir() ve kaç kez denendiğini sayan deneme alanı. KucukResimIsi tek bir yazı kimliği alır ve calistir() çağrıldığında kimliği bir diziye ekler; diziye yazılmış olması işin koştuğunun kanıtıdır.
İşin nasıl gönderildiğine sürücü (driver) karar verir. Surucu arayüzünün tek yöntemi gonder() vardır. EszamanliSurucu işi kuyruğa koymaz, Gunluk::kostu() ile işaretleyip hemen çalıştırır. BellekSurucusu ise Gunluk::ertele() ile işaretleyip işi bir diziye ekler ve yanıtı döndürür; iş orada bekler. Bekleyen işleri isle() çalıştırır: her işi diziden çeker, denemeyi bir artırır, calistir() der ve istisnayı yakalarsa deneme hakkı kalmışsa işi dizinin sonuna geri koyar, kalmamışsa basarisiz listesine atar.
YazilarDenetleyicisi::kaydet() ise sürücüyü bilmez. Yalnız Surucu arayüzüne konuşur: bir iş nesnesi üretir, gonder() der ve 201 döndürür. Denetleyicinin kodu iki sürücüde de aynıdır; değişen, sürücünün işi ne yaptığıdır.
<?php // isler.php — iş sınıfı, iki sürücü ve işçi declare(strict_types=1); require_once 'kostu.php'; abstract class IsSinifi { public int $deneme = 0; abstract public function ad(): string; abstract public function calistir(): void; } final class KucukResimIsi extends IsSinifi { public static array $uretilen = []; public function __construct(private int $yaziId) {} public function ad(): string { return 'kucukResim'; } public function calistir(): void { self::$uretilen[] = $this->yaziId; } } interface Surucu { public function gonder(IsSinifi $is): void; } final class EszamanliSurucu implements Surucu { public function gonder(IsSinifi $is): void { Gunluk::kostu($is->ad()); $is->calistir(); } } final class BellekSurucusu implements Surucu { private array $bekleyen = []; public array $basarisiz = []; public function __construct(private int $denemeHakki = 3) {} public function gonder(IsSinifi $is): void { Gunluk::ertele($is->ad()); $this->bekleyen[] = $is; } public function bekleyen(): int { return count($this->bekleyen); } public function isle(): array { // işçi: istekten bağımsız koşar $gunluk = []; while ($is = array_shift($this->bekleyen)) { $is->deneme++; try { $is->calistir(); $gunluk[] = "{$is->ad()} deneme {$is->deneme}: koştu"; } catch (Throwable $hata) { $gunluk[] = "{$is->ad()} deneme {$is->deneme}: " . get_class($hata); if ($is->deneme < $this->denemeHakki) $this->bekleyen[] = $is; else $this->basarisiz[] = $is; } } return $gunluk; } } final class YazilarDenetleyicisi { public function __construct(private Surucu $surucu) {} public function kaydet(int $yaziId): array { Gunluk::yeniIstek(); Gunluk::yola('kaydet'); Gunluk::yola('kucukResim'); Gunluk::kostu('kaydet'); $this->surucu->gonder(new KucukResimIsi($yaziId)); return [201, 'kaydedildi']; } }
Kuyruğa Konan İş
İlk ölçüm iki parça sorar: kaydet (denetleyicinin kendi adımı) ve kucukResim (kuyruğa konan iş). Sürücü kuyruktur. Yanıt döndükten sonra isle() çağrılır ve işçi çalışır. Karşıt ölçüm aynı denetleyiciyi eşzamanlı sürücüyle koşturur.
<?php // kuyruk.php — aynı iş iki sürücüyle require_once 'isler.php'; function olc(string $ad, Surucu $surucu): void { KucukResimIsi::$uretilen = []; $yanit = (new YazilarDenetleyicisi($surucu))->kaydet(7); printf("%s -> %d %s\n", $ad, $yanit[0], $yanit[1]); foreach (['kaydet', 'kucukResim'] as $parca) printf(" %-11s %s\n", $parca, sinifla($parca)); echo ' yolda mı: ', in_array('kucukResim', Gunluk::$yol, true) ? 'evet' : 'hayır', "\n"; echo ' yanıt anında üretilen: ', count(KucukResimIsi::$uretilen), "\n"; } $kuyruk = new BellekSurucusu(); olc('kuyruk', $kuyruk); echo ' bekleyen iş: ', $kuyruk->bekleyen(), "\n"; foreach ($kuyruk->isle() as $satir) echo ' işçi: ', $satir, "\n"; echo ' işçiden sonra üretilen: ', count(KucukResimIsi::$uretilen), "\n"; olc('eşzamanlı', new EszamanliSurucu());
kuyruk -> 201 kaydedildi kaydet koştu kucukResim ertelendi yolda mı: evet yanıt anında üretilen: 0 bekleyen iş: 1 işçi: kucukResim deneme 1: koştu işçiden sonra üretilen: 1 eşzamanlı -> 201 kaydedildi kaydet koştu kucukResim koştu yolda mı: evet yanıt anında üretilen: 1
Bu dersin gözlemi ilk blokta. Yanıt 201 olarak döndü, ama kucukResim ertelendi: yola konmuştu, isteğin içinde çağrılmadı, isteğin dışına bırakıldı. “Yanıt anında üretilen” sayısı 0’dır, yani küçük resim henüz yoktur; bekleyen iş sayısı 1’dir. İşçi isle() ile çalıştığında iş ilk denemede koştu ve üretilen sayı 1 oldu. Yanıtı alan istemci küçük resmi beklemedi.
Karar sırası burada işe yarar. kucukResim yolda olduğu için yolda mı satırı evet der; bu satır tek başına işin koştuğunu söylemez. sinifla() önce kostu listesine bakar, orada bulamaz; sonra ertelenen listesine bakar ve bulur. “Yolda” olmak ile “koşmak” ayrı gözlemlerdir ve ertelenen parça tam aralarındadır.
Karşıt: Eşzamanlı Sürücü
İkinci blok aynı denetleyiciyi EszamanliSurucu ile koşturur. Aynı iş, aynı gonder() çağrısı; kucukResim bu kez koştu ve yanıt anında üretilen sayı 1’dir. İstemci yanıtını işin bitmesinden sonra alır. Kod aynıdır, tek değişen sürücüdür. Bu, kuyruğun işi küçültmediğini, yalnız işin koştuğu anı isteğin dışına taşıdığını gösterir.
Ayrımı bir cümleyle koymak gerekirse: M08/K04’te adımı kimin yürüttüğünü aradık; burada adımı kimin yürüttüğü değil, yanıtın onu bekleyip beklemediği önemlidir. Eşzamanlı sürücüde yanıt bekler, kuyrukta beklemez.
Sınırlayıcı Ölçüm
Kuyruğun işi ertelemesi bir bedeli de getirir: iş yanıttan sonra koştuğu için hata istemciye ulaşmaz, işçide kalır. Bu yüzden yeniden deneme ve başarısız işler listesi gerekir. Aşağıdaki ölçüm iki iş kurar. Birincisi yalnız ilk denemede istisna fırlatır; ikincisi ilk beş denemede fırlatır. Deneme hakkı 3’tür.
<?php // deneme.php — ilk denemede başarısız olan iş require_once 'isler.php'; final class KirilganIs extends IsSinifi { public function __construct(private int $basarisizDeneme) {} public function ad(): string { return 'kirilgan'; } public function calistir(): void { if ($this->deneme <= $this->basarisizDeneme) throw new RuntimeException('geçici hata'); } } foreach ([[1, 3], [5, 3]] as [$basarisiz, $hak]) { Gunluk::yeniIstek(); $kuyruk = new BellekSurucusu($hak); $kuyruk->gonder(new KirilganIs($basarisiz)); echo "ilk $basarisiz deneme başarısız, hak $hak\n"; echo ' istekte: ', sinifla('kirilgan'), "\n"; foreach ($kuyruk->isle() as $satir) echo ' işçi: ', $satir, "\n"; echo ' başarısız işler: ', count($kuyruk->basarisiz), "\n"; }
ilk 1 deneme başarısız, hak 3 istekte: ertelendi işçi: kirilgan deneme 1: RuntimeException işçi: kirilgan deneme 2: koştu başarısız işler: 0 ilk 5 deneme başarısız, hak 3 istekte: ertelendi işçi: kirilgan deneme 1: RuntimeException işçi: kirilgan deneme 2: RuntimeException işçi: kirilgan deneme 3: RuntimeException başarısız işler: 1
İki iş de istekte ertelendi: yanıt hiçbir durumda hatayı görmedi. İlk iş birinci denemede RuntimeException fırlattı, işçi onu yakaladı, hakkı bitmediği için dizinin sonuna geri koydu ve ikinci denemede iş koştu. Başarısız işler listesi boş kaldı. İkinci iş üç denemede de başarısız oldu. Üçüncü denemeden sonra hak kalmadığı için geri konmadı ve basarisiz listesine düştü: listede 1 iş vardır. Beşinci deneme hiç yapılmadı.
Sayılar şunu söyler: yeniden deneme geçici hatalar içindir ve hata sürerse sonsuza dek denemez; sınırlı bir hak vardır ve hak bitince iş görünür bir yere, başarısız işler listesine düşer. O liste boş değilse birinin bakması gerekir. Yanıtın 201 dönmesi yalnız yazının kaydedildiğini söyler; küçük resim işinin başarılı olduğunu göstermez, o iş yalnız kuyruğa konmuştur.
Özet
- Bu dersin ölçü sınıfı ertelendi: denetleyici küçük resim işini kuyruğa koydu, yanıt 201 döndü, iş yanıt anında çağrılmadı ve işçide koştu.
- Model aynı süreçtedir; işçi
isle()çağrısının adıdır ve yanıttan sonra çalışır. - Karşıt: eşzamanlı sürücüde aynı iş koştu ve yanıt onu bekledi; denetleyici kodu değişmedi, yalnız sürücü değişti.
- Sınırlayıcı ölçümde ilk denemede başarısız olan iş ikinci denemede koştu; üç denemede de başarısız olan iş başarısız işler listesine düştü.
- Kuyruğa konmuş olmak başarılı olmak değildir: 201 yazının kaydedildiğini söyler, kuyruktaki işin sonucunu söylemez.
Sonraki Adım
Küçük resim işi kuyruğa girdi, ama denetleyici onu adıyla bildi: new KucukResimIsi(...). Yeni bir yan etki gerektiğinde, örneğin yazı kaydedilince bir arama dizinini güncellemek, denetleyiciye yeni bir satır eklemek gerekir. Olaylar ve Dinleyiciler dersi bu bağı gevşetir: denetleyici bir olay yayar, dinleyici onu işler. Peki olay adı dinleyicinin kayıtlı olduğu adla uyuşmazsa ne olur?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.