First-Class Callable dan Fibers

Sintaks first-class callable dan pengenalan fibers.

Daftar isi

Topik ini membahas dua fitur PHP 8.1 yang menyangkut arah eksekusi program. Sintaks first-class callable mengubah fungsi dan metode apa pun menjadi objek closure (fungsi yang dapat disimpan dalam variabel) dengan penulisan paling singkat. Fiber menyediakan eksekusi yang dapat dijeda dan dilanjutkan tanpa menghentikan alur utama. Contoh memakai pengurutan kata dan pekerjaan pengindeksan arsip sederhana. Setelah membacanya, fungsi dapat diperlakukan sebagai nilai secara alami, dan konsep pemrograman asinkron pada PHP dapat dipahami mulai dari fondasinya.

Sintaks first-class callable#

Sebelum PHP 8.1, callable dalam bentuk apa pun dibungkus dengan Closure::fromCallable(). PHP 8.1 menyediakan bentuk yang lebih pendek, yaitu menuliskan ... pada posisi daftar argumen, sehingga strlen(...) menghasilkan closure yang setara. Bentuk ini berlaku untuk fungsi global, metode milik objek melalui $objek->metode(...), dan metode statis melalui Kelas::metode(...).

php callable-pertama.php
<?php

declare(strict_types=1);

$panjang = strlen(...);

echo get_class($panjang), PHP_EOL;
echo $panjang('Arsip nasional'), PHP_EOL;

$nama = array_map(strtoupper(...), ['ali', 'budi', 'citra']);
print_r($nama);

class PembandingKata
{
    public function urutkan(array $kata): array
    {
        usort($kata, $this->bandingkan(...));

        return $kata;
    }

    private function bandingkan(string $a, string $b): int
    {
        return strlen($a) <=> strlen($b);
    }
}

print_r((new PembandingKata())->urutkan(['ki', 'kira', 'kira-kira']));
text Keluaran
Closure
14
Array
(
    [0] => ALI
    [1] => BUDI
    [2] => CITRA
)
Array
(
    [0] => ki
    [1] => kira
    [2] => kira-kira
)

Hasil strlen(...) adalah objek Closure sebagaimana tercetak pada baris pertama keluaran, dan objek itu dipanggil kembali dengan $panjang('Arsip nasional'). Sintaks strtoupper(...) menunjukkan bahwa closure siap pakai dapat dijadikan argumen langsung untuk array_map tanpa variabel perantara. Di dalam metode urutkan(), bentuk $this->bandingkan(...) menyelesaikan masalah pemanggilan metode privat sebagai callable, karena closure yang dihasilkan tetap terikat pada $this sehingga usort dapat memanggilnya.

Sintaks ini murni pembentuk objek, dan tanda ... di situ bukan variadic function (fungsi dengan daftar argumen panjang), melainkan penanda bahwa seluruh daftar argumen diserahkan kepada pemanggil closure.

Fiber, eksekusi yang dapat dijeda#

Fiber adalah blok eksekusi lengkap dengan tumpukan panggilannya sendiri yang dapat dijeda di tengah jalan dan dilanjutkan kemudian. Jeda dan lanjutan itu berlangsung secara cooperative (sukarela), yaitu fiber memutuskan sendiri kapan menyerahkan kendali melalui Fiber::suspend(), dan kode utama melanjutkannya dengan resume(). Program tetap berjalan pada satu alur tunggal, sehingga tidak ada dua kode yang dieksekusi pada saat bersamaan.

Diagram berikut memperlihatkan pergantian kendali antara kode utama dan fiber sepanjang satu siklus hidupnya.

Kode utamaFiber1. start()2. suspend() menyerahkan kendali3. resume() memberi nilai4. return lalu getReturn()
Alur kendali antara kode utama dan fiber

Pergantian kendali inilah yang membuat fiber menjadi fondasi pustaka asinkron. Pada saat fiber menunggu sesuatu yang lambat, fiber menyerahkan kendali secara sukarela, dan pengatur jadwal milik pustaka menjalankan fiber lain. Tanpa fiber, pustaka semacam itu harus membagi pekerjaannya menjadi banyak fungsi kecil yang terhubung callback, yaitu fungsi yang dipanggil balik oleh pustaka, sehingga alurnya sulit dibaca.

Alur suspend dan resume#

Contoh berikut memuat satu fiber yang menjalankan tiga tahap pekerjaan pengindeksan dan berhenti dua kali untuk melapor. Method start() menjalankan fiber hingga Fiber::suspend() pertama, dan nilai yang diserahkan suspend() dikembalikan kepada pemanggil. Method resume() melanjutkan fiber, dan argumennya menjadi nilai kembalian dari Fiber::suspend() di dalam fiber.

php fiber-dasar.php
<?php

declare(strict_types=1);

$fiber = new Fiber(function (): string {
    echo 'Fiber: membaca daftar berkas', PHP_EOL;
    $nilai = Fiber::suspend('berkas terbaca');
    echo 'Fiber: melanjutkan dengan ', $nilai, PHP_EOL;
    echo 'Fiber: menghitung ringkasan', PHP_EOL;
    Fiber::suspend('ringkasan selesai');
    echo 'Fiber: menutup pekerjaan', PHP_EOL;

    return 'pekerjaan tuntas';
});

echo 'Utama: memulai fiber', PHP_EOL;
$catatan = $fiber->start();
echo 'Utama: fiber menyerah pada ', $catatan, PHP_EOL;

$catatan = $fiber->resume('data sensor');
echo 'Utama: fiber menyerah pada ', $catatan, PHP_EOL;

$fiber->resume();
echo 'Utama: hasil akhir ', $fiber->getReturn(), PHP_EOL;
echo 'Utama: fiber selesai? ', $fiber->isTerminated() ? 'ya' : 'belum', PHP_EOL;
text Keluaran
Utama: memulai fiber
Fiber: membaca daftar berkas
Utama: fiber menyerah pada berkas terbaca
Fiber: melanjutkan dengan data sensor
Fiber: menghitung ringkasan
Utama: fiber menyerah pada ringkasan selesai
Fiber: menutup pekerjaan
Utama: hasil akhir pekerjaan tuntas
Utama: fiber selesai? ya

Urutan baris pada keluaran memperlihatkan pergantian giliran antara kode utama dan fiber. Nilai data sensor yang diberikan melalui resume() diterima $nilai pada titik suspend, sehingga jalur dua arah antara kedua pihak berlangsung tanpa variabel global. Setelah fiber mengembalikan nilainya dengan return, method getReturn() membaca hasil akhir tersebut dan isTerminated() menegaskan bahwa eksekusinya sudah selesai. Kelas Fiber juga menyediakan isStarted() dan isRunning() untuk membaca keadaan di antara tahap-tahap tersebut.

Exception yang dilempar di dalam fiber dan tidak ditangkap akan muncul pada pemanggil start() atau resume() yang menjalankannya, seolah-olah exception itu dilempar dari titik panggilan tersebut. Fiber yang berhenti karena exception dianggap berakhir, sehingga isTerminated() mengembalikan nilai true, sedangkan getReturn() tidak lagi menyediakan nilai kembalian dan melempar FiberError dengan keterangan bahwa fiber melempar exception. Perilaku ini menegaskan bahwa penanganan error tetap menjadi tanggung jawab kode utama di sekitar pemanggilan fiber.

Kesalahan umum#

Kesalahan pertama adalah memanggil Fiber::suspend() dari luar fiber. Tanpa fiber yang sedang berjalan, tidak ada eksekusi yang dapat dijeda, dan PHP melempar FiberError sebagaimana contoh berikut.

php luar-fiber.php
<?php

declare(strict_types=1);

try {
    Fiber::suspend('di luar fiber');
} catch (FiberError $e) {
    echo 'Error: ', $e->getMessage(), PHP_EOL;
}
text Keluaran
Error: Cannot suspend outside of a fiber

Kesalahan kedua adalah menganggap fiber seperti thread (alur eksekusi paralel milik sistem operasi). Fiber tidak menjalankan dua pekerjaan pada saat bersamaan dan tidak mempercepat perhitungan yang padat CPU, melainkan menata giliran eksekusi agar penantian yang lama tidak memblokir pekerjaan lain. Manfaatnya muncul pada pemrograman asinkron yang didukung pustaka I/O nonblokir, bukan pada pembagian beban hitung.

Kesalahan ketiga adalah memakai fiber tanpa pengatur jadwal. Fiber mentah menuntut pemanggil resume() yang eksplisit, sehingga menulis fiber sendiri untuk pekerjaan sekuensial biasa hanya menambah kompleksitas. Fiber berada di lapisan bawah, sedangkan pustaka asinkron di atasnya menyembunyikan pengaturan giliran dari kode aplikasi.

Ringkasan#

  • Sintaks fungsi(...) menghasilkan Closure dari fungsi, metode, maupun metode statis, dan menggantikan Closure::fromCallable().
  • Fiber adalah eksekusi dengan tumpukan sendiri yang dapat dijeda lewat Fiber::suspend() dan dilanjutkan lewat resume().
  • Nilai mengalir dua arah, yaitu suspend() menyerahkan nilai kepada pemanggil dan resume() memberi nilai kepada fiber.
  • Fiber bukan thread, tidak mengeksekusi paralel, dan Fiber::suspend() di luar fiber melempar FiberError.

Referensi#