Mengelola Asynchrony dalam PHP: From Promises to Coroutines


Apa itu asinkron? Singkatnya, asinkron berarti melakukan beberapa tugas selama periode waktu tertentu. PHP berjalan dalam satu utas, yang berarti hanya satu bagian dari kode PHP yang dapat dieksekusi pada waktu tertentu. Ini mungkin tampak seperti batasan, tetapi sebenarnya memberi kita lebih banyak kebebasan. Akibatnya, kami tidak harus menghadapi semua kompleksitas yang terkait dengan pemrograman multithreaded. Namun di sisi lain, ada satu set masalah. Kita harus berurusan dengan asinkron. Kita perlu entah bagaimana mengaturnya dan mengoordinasinya.


Memperkenalkan terjemahan sebuah artikel dari blog pengembang backend Skyeng, Sergey Zhuk.


Misalnya, ketika kami menjalankan dua permintaan HTTP paralel, kami mengatakan bahwa mereka "berjalan secara paralel." Ini biasanya mudah dan sederhana untuk dilakukan, tetapi masalah muncul ketika kita perlu merampingkan respons dari permintaan ini, misalnya, ketika satu permintaan membutuhkan data yang diterima dari permintaan lain. Dengan demikian, dalam manajemen asinkron itulah kesulitan terbesar terletak. Ada beberapa cara berbeda untuk menyelesaikan masalah ini.


Saat ini tidak ada dukungan bawaan untuk abstraksi tingkat tinggi untuk mengelola asinkron di PHP, dan kami harus menggunakan perpustakaan pihak ketiga seperti ReactPHP dan Amp. Dalam contoh di artikel ini, saya menggunakan ReactPHP.

Janji


Untuk lebih memahami gagasan tentang janji, sebuah contoh kehidupan nyata akan berguna. Bayangkan Anda berada di McDonald's dan ingin melakukan pemesanan. Anda membayar uang untuk itu dan dengan demikian memulai transaksi. Menanggapi transaksi ini, Anda berharap mendapatkan hamburger dan kentang goreng. Namun kasir tidak segera mengembalikan makanan. Sebagai gantinya, Anda menerima cek dengan nomor pesanan. Anggap cek ini sebagai janji untuk pesanan di masa mendatang. Sekarang Anda dapat mengambil cek ini dan mulai memikirkan makan siang lezat Anda. Hamburger yang diharapkan dan kentang goreng belum siap, jadi Anda berdiri dan menunggu sampai pesanan Anda selesai. Segera setelah nomornya muncul di layar, Anda akan menukar cek untuk pesanan Anda. Inilah janjinya:


Pengganti nilai masa depan.

Sebuah janji adalah representasi untuk makna masa depan, pembungkus mandiri waktu yang kita bungkus makna. Kami tidak peduli apakah nilainya sudah ada di sini atau belum. Kami terus memikirkannya dengan cara yang sama. Bayangkan bahwa kita memiliki tiga permintaan HTTP asinkron yang dijalankan "secara paralel", sehingga permintaan itu akan diselesaikan pada satu titik waktu. Tetapi kami ingin mengoordinasikan dan mengatur jawaban mereka. Misalnya, kami ingin mencetak jawaban ini segera setelah diterima, tetapi dengan sedikit batasan: jangan cetak jawaban kedua sampai jawaban pertama diterima. Maksud saya di sini adalah jika $ janji1 terpenuhi, maka kita mencetaknya. Tetapi jika $ janji2 dipenuhi terlebih dahulu, kami tidak mencetaknya, karena $ janji1 masih dalam proses. Bayangkan kita mencoba mengadaptasi tiga permintaan kompetitif sedemikian rupa sehingga bagi pengguna akhir mereka terlihat seperti satu permintaan cepat.


Jadi, bagaimana kita bisa menyelesaikan masalah ini dengan janji? Pertama-tama, kita membutuhkan fungsi yang mengembalikan janji. Kita bisa mengumpulkan tiga janji seperti itu dan kemudian menyatukannya. Berikut ini beberapa kode palsu untuk ini:


<?php use React\Promise\Promise; function fakeResponse(string $url, callable $callback) { $callback("response for $url"); } function makeRequest(string $url) { return new Promise(function(callable $resolve) use ($url) { fakeResponse($url, $resolve); }); } 

Di sini saya memiliki dua fungsi:
fakeResponse (string $ url, callable $ callback) berisi respons yang tersandikan dan memungkinkan panggilan balik yang ditentukan dengan jawaban ini;
makeRequest (string $ url) mengembalikan janji yang menggunakan fakeResponse () untuk menunjukkan bahwa permintaan telah selesai.


Dari kode klien, kami cukup memanggil fungsi makeRequest () dan mendapatkan janji:


 <?php $promise1 = makeRequest('url1'); $promise2 = makeRequest('url2'); $promise3 = makeRequest('url3'); 

Itu sederhana, tetapi sekarang kita perlu mengurutkan jawaban ini entah bagaimana. Sekali lagi, kami ingin tanggapan dari janji kedua akan dicetak hanya setelah penyelesaian yang pertama. Untuk mengatasi masalah ini, Anda dapat membangun rantai janji:


 <?php $promise1 ->then('var_dump') ->then(function() use ($promise2) { return $promise2; }) ->then('var_dump') ->then(function () use ($promise3) { return $promise3; }) ->then('var_dump') ->then(function () { echo 'Complete'; }); 

Dalam kode di atas, kita mulai dengan $ janji1 . Setelah selesai, kami mencetak nilainya. Kami tidak peduli berapa lama: kurang dari satu detik atau satu jam. Segera setelah janji itu selesai, kami akan mencetak nilainya. Dan kemudian kita menunggu $ janji2 . Dan di sini kita dapat memiliki dua skenario:


$ janji2 sudah selesai, dan kami segera mencetak nilainya;
$ janji2 masih terpenuhi, dan kami sedang menunggu.


Berkat rantai janji-janji, kita tidak perlu lagi khawatir apakah janji telah dipenuhi atau tidak. Promis tidak tergantung pada waktu, dan karenanya menyembunyikan statusnya dari kami (dalam proses, sudah selesai atau dibatalkan).


Ini adalah bagaimana Anda dapat mengontrol asinkron dengan janji. Dan itu tampak hebat, rantai janji jauh lebih cantik dan lebih bisa dipahami daripada banyak panggilan balik bersarang.


Generator


Di PHP, generator adalah dukungan bahasa bawaan untuk fungsi yang dapat dijeda dan kemudian dilanjutkan. Ketika eksekusi kode di dalam generator berhenti, itu tampak seperti program kecil yang diblokir. Tetapi di luar program ini, di luar generator, semua yang lain terus bekerja. Ini semua keajaiban dan kekuatan generator.


Kami benar-benar dapat menghentikan generator secara lokal untuk menunggu janji untuk menyelesaikan. Ide dasarnya adalah menggunakan janji dan generator bersama. Mereka mengambil alih kendali asynchrony, dan kami hanya memanggil hasil ketika kami perlu menangguhkan generator. Berikut adalah program yang sama, tetapi sekarang kami menghubungkan generator dan janji:


 <?php use Recoil\React\ReactKernel; // ... ReactKernel::start(function () { $promise1 = makeRequest('url1'); $promise2 = makeRequest('url2'); $promise3 = makeRequest('url3'); var_dump(yield $promise1); var_dump(yield $promise2); var_dump(yield $promise3); }); 

Untuk kode ini, saya menggunakan library recoilphp / recoil , yang memungkinkan Anda memanggil ReactKernel :: start () . Recoil memungkinkan untuk menggunakan generator PHP untuk menjalankan janji asinkron ReactPHP.

Di sini, kami masih melakukan tiga kueri secara paralel, tetapi sekarang kami menyortir respons menggunakan kata kunci hasil . Dan lagi, kami menampilkan hasilnya di akhir setiap janji, tetapi hanya setelah yang sebelumnya.


Coroutine


Coroutine adalah cara membagi operasi atau proses menjadi potongan-potongan, dengan beberapa eksekusi di dalam setiap potongan tersebut. Akibatnya, ternyata alih-alih melakukan seluruh operasi pada satu waktu (yang dapat menyebabkan pembekuan aplikasi yang nyata), itu akan dilakukan secara bertahap sampai semua jumlah pekerjaan yang diperlukan telah selesai.


Sekarang kita memiliki generator interruptible dan terbarukan, kita dapat menggunakannya untuk menulis kode asinkron dengan janji-janji dalam bentuk sinkron yang lebih akrab. Menggunakan generator dan janji-janji PHP, Anda dapat sepenuhnya menyingkirkan panggilan balik. Idenya adalah bahwa ketika kita memberikan janji (menggunakan panggilan yield), coroutine berlangganan. Corutin berhenti dan menunggu sampai janji selesai (selesai atau dibatalkan). Begitu janji itu selesai, coroutine akan terus memenuhi. Setelah berhasil diselesaikan, janji coroutine mengirimkan nilai yang diterima kembali ke konteks generator menggunakan panggilan Generator :: send ($ value) . Jika janji gagal, maka Corutin melempar pengecualian melalui generator menggunakan panggilan Generator :: throw () . Dengan tidak adanya panggilan balik, kita dapat menulis kode asinkron yang terlihat hampir seperti yang biasa sinkron.


Eksekusi berurutan


Saat menggunakan coroutine, urutan eksekusi dalam kode asinkron sekarang penting. Kode dieksekusi tepat ke tempat di mana kata kunci hasil dipanggil dan kemudian dijeda sampai janji selesai. Pertimbangkan kode berikut:


 <?php use Recoil\React\ReactKernel; // ... ReactKernel::start(function () { echo 'Response 1: ', yield makeRequest('url1'), PHP_EOL; echo 'Response 2: ', yield makeRequest('url2'), PHP_EOL; echo 'Response 3: ', yield makeRequest('url3'), PHP_EOL; }); 

Promise1: akan ditampilkan di sini , kemudian eksekusi berhenti dan menunggu. Segera setelah janji dari makeRequest ('url1') selesai, kami mencetak hasilnya dan beralih ke baris kode berikutnya.


Menangani kesalahan


Standar Janji / A + Janji menyatakan bahwa masing-masing Janji berisi metode maka () dan menangkap () . Antarmuka ini memungkinkan Anda membangun rantai dari janji dan secara opsional menangkap kesalahan. Pertimbangkan kode berikut:


 <?php operation()->then(function ($result) { return anotherOperation($result); })->then(function ($result) { return yetAnotherOperation($result); })->then(function ($result) { echo $result; }); 

Di sini kita memiliki rantai janji yang meneruskan hasil dari setiap janji sebelumnya ke janji berikutnya. Tetapi tidak ada blok catch () di rantai ini, tidak ada penanganan kesalahan di sini. Ketika janji dalam rantai gagal, eksekusi kode bergerak ke penangan kesalahan terdekat di rantai. Dalam kasus kami, ini berarti bahwa janji yang beredar akan diabaikan, dan kesalahan yang terjadi akan hilang selamanya. Dengan coroutine, penanganan kesalahan muncul kedepan. Jika operasi asinkron gagal, pengecualian akan dilemparkan:


 <?php use Recoil\React\ReactKernel; use React\Promise\RejectedPromise; // ... function failedOperation() { return new RejectedPromise(new RuntimeException('Something went wrong')); } ReactKernel::start(function () { try { yield failedOperation(); } catch (Throwable $error) { echo $error->getMessage() . PHP_EOL; } }); 

Membuat Kode Asinkron Dapat Dibaca


Generator memiliki efek samping yang sangat penting yang dapat kita gunakan untuk mengontrol asinkron dan yang memecahkan masalah keterbacaan kode asinkron. Sulit bagi kita untuk memahami bagaimana kode asinkron akan dieksekusi karena fakta bahwa utas eksekusi terus-menerus berpindah di antara berbagai bagian program. Namun, otak kita pada dasarnya bekerja secara sinkron dan single-threaded. Sebagai contoh, kami merencanakan hari kami dengan sangat konsisten: melakukan satu, lalu yang lain, dan seterusnya. Tetapi kode asinkron tidak berfungsi sebagaimana otak kita terbiasa berpikir. Bahkan rantai janji yang sederhana mungkin tidak terlihat terlalu mudah dibaca:


 <?php $promise1 ->then('var_dump') ->then(function() use ($promise2) { return $promise2; }) ->then('var_dump') ->then(function () use ($promise3) { return $promise3; }) ->then('var_dump') ->then(function () { echo 'Complete'; }); 

Kita harus membongkar secara mental untuk memahami apa yang terjadi di sana. Jadi kita perlu pola yang berbeda untuk mengontrol asinkron. Singkatnya, generator menyediakan cara untuk menulis kode asinkron sehingga terlihat seperti sinkron.


Janji dan generator menggabungkan yang terbaik dari kedua dunia: kita mendapatkan kode asinkron dengan kinerja hebat, tetapi pada saat yang sama itu tampak seperti sinkron, linier, dan berurutan. Coroutine memungkinkan Anda untuk menyembunyikan asinkron, yang sudah menjadi detail implementasi. Dan kode kita pada saat yang sama terlihat seperti otak kita terbiasa berpikir - secara linear dan berurutan.


Jika kita berbicara tentang ReactPHP , maka kita dapat menggunakan pustaka RecoilPHP untuk menulis janji dalam bentuk coroutine. Dalam Amp, coroutine tersedia langsung di luar kotak.

Source: https://habr.com/ru/post/id453296/


All Articles