আমি গত কয়েক বছর ধরে Laravel-এ কাজ করি। পুরোপুরি sync PHP। Request আসে, process হয়, response যায়। Async জিনিসটা কোনোদিন লাগেনি, তাই শিখিওনি।
তারপর একদিন JustSteveKing-এর একটা article পড়লাম PHP 8.6-এর নতুন Polling API নিয়ে। পড়তে গিয়ে ধাক্কা খেলাম দুই জায়গায়:
- epoll আর kqueue কী জিনিস? এই শব্দগুলো আমি Go আর Rust-এর article-এও দেখেছি। PHP-র article-এ এরা কেন?
- আমার ধারণা ছিল ReactPHP চালাতে extension লাগে। এটা ভুল ছিল। কতটা ভুল, সেটা নিচে বলছি।
এই post-টা সেই শেখার journey। আমার মতো যারা কোনোদিন async ছোঁয়নি, তাদের জন্য step by step।
Step 1: Sync PHP-তে "waiting" মানে আসলে কী হয়
এই code আমরা প্রতিদিন লিখি:
$response = Http::get('https://api.shopify.com/...'); // ধরুন 300ms লাগলো
এই 300ms-এ আপনার PHP process কী করছে? কিছুই না।
ভেতরে যা হয়: Http::get() একটা TCP socket খোলে, request লেখে, তারপর fread() call করে। fread() একটা syscall, মানে আপনার process kernel-কে বলছে "এই socket থেকে data দাও।" Kernel দেখে data এখনো আসেনি, তখন সে আপনার process-কে sleep state-এ ফেলে দেয়। CPU থেকে সরিয়ে দেয়। Data আসলে kernel তাকে জাগাবে।
এটাকে বলে blocking I/O। Syscall ততক্ষণ return করে না যতক্ষণ কাজ শেষ না হয়।
এখন math করুন। একটা job-এ তিনটা API call:
Shopify API: 300ms
Google Sheets: 400ms
Webhook ping: 200ms
─────────────────────
Sequential: 900ms
900ms-এর মধ্যে actual CPU কাজ (JSON parse, logic) হয়তো 20ms। বাকি 880ms process ঘুমাচ্ছে, কিন্তু memory ধরে রেখেছে, PHP-FPM-এর একটা worker slot দখল করে আছে। ২০টা worker সবাই এভাবে ঘুমালে ২১তম request queue-তে বসে থাকে, যদিও server-এর CPU idle।
Async-এর পুরো point এটাই: ওই 880ms-এ অন্য কাজ করা। তিনটা call concurrent চালালে total time হয় slowest-টার সমান। 900ms না, 400ms।
কিন্তু concurrent করতে গেলে নতুন problem: তিনটা socket একসাথে খোলা, আপনি জানেন না কোনটার data আগে আসবে। "কোন socket ready?" এই প্রশ্নের উত্তরই হলো epoll।
Step 2: "কোন socket ready?" এই প্রশ্নের তিন যুগের উত্তর
প্রথমে socket-গুলোকে non-blocking mode-এ ফেলা হয়:
stream_set_blocking($socket, false);
এখন fread() আর ঘুম পাড়ায় না। Data না থাকলে সাথে সাথে খালি হাতে return করে। কিন্তু এখন কে বলবে data কখন এসেছে?
Attempt 1: Busy polling (ভুল রাস্তা)
while (true) {
foreach ($sockets as $s) {
$data = fread($s, 8192);
if ($data !== '') { /* process */ }
}
}
কাজ করবে, কিন্তু CPU 100% পুড়বে শুধু বারবার জিজ্ঞেস করতে "এসেছে? এসেছে?" Step 1-এ CPU idle ছিল অপচয়, এখন CPU busy কিন্তু useless কাজে। আরো খারাপ।
Attempt 2: Kernel-কে জিজ্ঞেস করা — select()
সঠিক idea হলো: kernel-ই তো জানে কোন socket-এ data এসেছে। Network card থেকে packet তো kernel-ই receive করে। তাহলে kernel-কেই বলি:
"এই ১০টা socket-এর list নাও। যেকোনো একটাতে data আসা পর্যন্ত আমাকে ঘুম পাড়াও। কিছু আসলে জাগিয়ে বলো কোনগুলো ready।"
এটাই select() syscall। PHP-তে এর wrapper হলো stream_select(), যেটা PHP 4 থেকে core-এ আছে। কোনো extension লাগে না।
কিন্তু select()-এর design-এ দুটো রোগ:
- প্রতিবার পুরো list পাঠাতে হয়। ১০,০০০ socket থাকলে প্রতি call-এ পুরো list kernel-এ copy হয়, kernel পুরোটা scan করে, return-এর পর আপনি আবার scan করেন। প্রতি wake-up-এ O(n) খরচ, যদিও হয়তো মাত্র ২টা ready। আমি একে বলি rescan tax।
- Classic
select()-এ hard limit ~1024 file descriptor।
১০টা socket-এ কোনো সমস্যা নেই। ১০,০০০ connection-এ (WebSocket server ভাবুন) এটা wall-এ ধাক্কা খায়। এটার বিখ্যাত নাম C10K problem।
একটা জিনিস clear করি, কারণ আমি নিজে এখানে ভুল করেছিলাম: stream_select() কিন্তু busy polling করে না। সে kernel-এ ঘুমায়, CPU idle থাকে। তার সমস্যা heat না, সমস্যা হলো rescan tax আর FD limit। দুটো আলাদা failure mode।
Attempt 3: epoll / kqueue
Fix-টা conceptually সহজ। List-টা kernel-এর কাছেই রেখে দাও:
"এই socket-গুলোতে আমার interest আছে, একবার register করলাম। এখন থেকে যখনই জিজ্ঞেস করবো, শুধু ready-গুলোর নাম দিও।"
Kernel নিজেই একটা ready-list maintain করে। Packet আসামাত্র সেই socket ready-list-এ ঢুকে যায়। আপনি জিজ্ঞেস করলে শুধু ছোট list-টা পান। ১০,০০০ scan করা লাগে না।
- epoll = Linux
- kqueue = macOS / FreeBSD
- IOCP = Windows
একই idea, ভিন্ন OS-এর API।
Analogy: হাসপাতালের waiting room ভাবুন। select() হলো নার্স প্রতি মিনিটে পুরো room ঘুরে প্রত্যেক রোগীকে জিজ্ঞেস করছে "আপনার report এসেছে?" আর epoll হলো display board। Report ready হলে board-এ number ওঠে, নার্স শুধু board দেখে। রোগী ১০ জন হোক বা ১০,০০০, নার্সের কাজ একই।
আমার প্রথম "ওহহ" moment
এজন্যই আমি Go আর Rust-এর article-এও epoll দেখেছি। epoll কোনো language feature না, এটা kernel feature। Go-র runtime-এর ভেতরের netpoller Linux-এ epoll call করে। Rust-এ tokio-র নিচে mio একই কাজ করে। Node.js-এর libuv-ও তাই। ভাষা যেটাই হোক, সব async runtime শেষমেশ একই দরজা দিয়ে kernel-এ ঢোকে।
আর PHP? PHP-র core-এ এতদিন শুধু Attempt 2 ছিল (stream_select)। Attempt 3 চাইলে C extension লাগতো (ext-event, ext-ev, ext-uv)। এই gap-টাই PHP 8.6 বন্ধ করছে। সেটা Step 4-এ।
Step 3: Fibers — একটা pause button, আর কিছুই না
PHP 8.1-এ Fibers এসেছে। এখানে আমার সবচেয়ে বড় confusion ছিল, তাই সবচেয়ে জরুরি অংশ এটা।
stream_select() দিয়ে concurrent code লিখতে গেলে প্রতিটা request-এর অবস্থা manually track করতে হয়। কোন data কোন request-এর, buffer-এ কতটুকু জমেছে, header শেষ কিনা। ২০টা request মানে ২০টা আধা-খাওয়া অবস্থার হিসাব। Sync code-এর সৌন্দর্য ছিল গল্পের মতো উপর থেকে নিচে পড়া যায়। Concurrency নিতে গিয়ে সেটা হারায়।
Fiber সেই readability ফেরত দেয়: এমন function যেটা মাঝপথে pause হতে পারে, পরে ঠিক সেখান থেকেই resume হতে পারে। Local variables, call stack, সব অক্ষত।
এই code-টা আমি নিজে run করেছি (PHP 8.1+, zero extension):
<?php
$fiber = new Fiber(function () {
echo "কাজ শুরু\n";
Fiber::suspend();
echo "কাজ শেষ\n";
});
$fiber->start();
echo "৫ সেকেন্ড অপেক্ষা করছি...\n";
sleep(5);
echo "Fiber এখনো ঘুমে। জাগবেও না।\n";
// $fiber->resume(); ← এই লাইন ছাড়া "কাজ শেষ" কোনোদিন print হবে না
আমি প্রথমে ভেবেছিলাম fiber হয়তো কিছুক্ষণ পর নিজে নিজে জেগে ওঠে, বা resume করার জন্য কোনো extension লাগে। দুটোই ভুল। নিজে run করে দেখলাম:
Suspended fiber একটা জমে যাওয়া ছবি। কেউ
$fiber->resume()না ডাকলে সে অনন্তকাল জমে থাকবে।
YouTube video pause করলে যেমন নিজে নিজে play হয় না, কাউকে play চাপতে হয়। Fiber-এ play চাপা মানে অন্য কারো code-এ resume() লেখা থাকা।
আর resume() একটা সাধারণ method call। Core PHP। Extension-এর প্রশ্নই নেই।
তাহলে event loop কী? (এটাই আসল উত্তর)
আমি script-এ নিজের হাতে $fiber->resume() লিখে fiber জাগালাম। তখনই জিনিসটা ধরা পড়লো:
Event loop কোনো জাদু জিনিস না। Event loop = স্রেফ সেই PHP code যেটা সিদ্ধান্ত নেয় কখন কোন fiber-এ resume() ডাকবে।
আমি hardcoded এক লাইনে resume() লিখলে আমি নিজেই একটা এক-লাইনের, বোকা event loop। ReactPHP-র event loop-ও plain PHP code। পার্থক্য শুধু: সে stream_select() জিজ্ঞেস করে সিদ্ধান্ত নেয়।
// আমার version: // ReactPHP-র version (ধারণা):
$fiber->resume(); while (true) {
$ready = stream_select(...); // kernel-কে জিজ্ঞেস
foreach ($ready as $socket) {
$fiberFor[$socket]->resume(); // সেই একই resume()
}
}
"resume() free হলে epoll লাগবে কেন?"
এই প্রশ্নটা আমার মাথায় এসেছিল, আর উত্তরটা পুরো puzzle জোড়া লাগিয়ে দেয়।
resume() হলো পেশি, epoll হলো মগজ। Resume করা কঠিন না। কঠিন প্রশ্ন: কাকে, কখন? ধরুন ৩০টা fiber ৩০টা socket-এ ঘুমাচ্ছে। Kernel-এর খবর ছাড়া আপনার option একটাই: অন্ধভাবে সবাইকে ঘুরে ঘুরে resume করা। Fiber জাগবে, fread() করবে, খালি পাবে, আবার suspend করবে। চিনতে পারছেন? এটা Step 2-এর busy polling, fiber-এর পোশাক পরে ফেরত এসেছে। epoll-এর কাজ resume করা না। কাজ হলো ঘুমিয়ে থেকে জেনে নেওয়া "শুধু socket #7 ready", তারপর শুধু সেই একটা fiber জাগানো।
Fiber vs Generator, এক লাইনে
পার্থক্য speed না। পার্থক্য হলো কোথা থেকে pause করা যায়। Generator-এর yield শুধু generator function-এর নিজের body-তে চলে। Fiber::suspend() call stack-এর যেকোনো গভীরতা থেকে চলে। আপনার function → HTTP client → socket writer, সেখান থেকে suspend, উপরের কেউ টেরও পায় না। এজন্যই fiber-based library-র API দেখতে sync code-এর মতোই থাকে। একটাই শর্ত: suspend() অবশ্যই কোনো চালু fiber-এর ভেতরে হতে হবে, নাহলে FiberError।
Step 4: PHP 8.6-এর Polling API — missing piece
এখন পুরো ছবিটা দেখুন। Async I/O আসলে তিনটা আলাদা কাজ:
- Pause: Fibers (PHP 8.1) ✅ core-এ আছে
- Decide: Event loop, plain PHP code ✅ library লিখতে পারে
-
Know: Kernel polling, "কোন socket ready" ⚠️ core-এ শুধু বুড়ো
stream_select()ছিল
PHP 8.1 দিলো pause primitive, কিন্তু "know" primitive রয়ে গেলো ২০ বছরের পুরনো select, নাহলে C extension। এজন্যই ReactPHP-কে ৪টা আলাদা event loop implementation maintain করতে হতো: StreamSelectLoop (default, extension ছাড়া), আর ExtEventLoop / ExtEvLoop / ExtUvLoop (extension থাকলে)।
PHP 8.6 এই gap বন্ধ করছে। নতুন Io\Poll\Context core-এ ঢুকছে। RFC pass হয়েছে 33-1 vote-এ, implementation master-এ merged, GA target November 2026। ভেতরে PHP নিজেই platform অনুযায়ী backend বাছে: Linux-এ epoll, Mac-এ kqueue, Windows-এ IOCP।
Article-এর mini scheduler-এর core অংশটা এখন আপনি নিজেই পড়তে পারবেন:
public function awaitReadable($stream): void
{
$fiber = Fiber::getCurrent();
$watcher = $this->poll->add($handle, [Event::Read], ['fiber' => $fiber]);
// ↑ kernel-এ register: "এই socket-এ নজর রাখো" (Step 2)
Fiber::suspend();
// ↑ pause button (Step 3)
}
public function run(): void
{
while ($this->fibers !== []) {
foreach ($this->poll->wait(timeoutSeconds: 1) as $watcher) {
// ↑ kernel: "এগুলো ready", শুধু ready-গুলোই
$watcher->getData()['fiber']->resume($watcher);
// ↑ ঠিক সেই fiber-টাকেই জাগাও
}
}
}
Step 2 আর Step 3 এখানে বিয়ে করলো। এটাই async runtime-এর পুরো anatomy।
Article-এর benchmark-এ interesting finding: ছোট scale-এ wall-clock time প্রায় same। আসল পার্থক্য CPU behavior-এ। stream_select-based loop stream সংখ্যা বাড়ার সাথে rescan-এ বেশি কাজ করে, আর Io\Poll version flat থাকে। ৩টা stream আর ৩০টা stream-এ per-wait খরচ প্রায় একই। কেন flat, সেটা আপনি এখন জানেন: kernel ready-list রাখে, rescan লাগে না।
আরেকটা precision: epoll কিন্তু data এনে দেয় না, শুধু readiness জানায়। "Socket #7 ready।" Data আনার কাজ এখনো আপনার fread()। Kernel postman না, kernel হলো doorbell।
যে ভুল ধারণাগুলো ভাঙলো
আমার শুরুর দুটো প্রশ্নে ফিরে যাই।
"ReactPHP চালাতে কি extension লাগে?"
না। কোনোদিন লাগতো না। ReactPHP-র default loop stream_select() চালায়, যেটা PHP 4 থেকে core-এ। ext-event ছিল optional performance upgrade (select → epoll), requirement না। আমার assumption পুরো উল্টো ছিল।
"তাহলে vanilla PHP-তে 8.6-এর benefit কী?"
এতদিন epoll-class performance পেতে compiled PECL extension লাগতো, যেটা বেশিরভাগ shared hosting-এ install করাই যায় না। PHP 8.6-এ যেকোনো সস্তা server-এ vanilla install-ই সেই performance দেবে। ReactPHP/AMPHP-র ৪টা loop implementation ভবিষ্যতে একটা native backend-এ নামবে। Fibers + Polling, async-এর দুটো primitive-ই core-এ। Library-গুলোর কাজ তখন শুধু ergonomics।
মাথায় রাখার মতো তিনটা লাইন
- Async I/O = তিনটা আলাদা কাজ: Pause (Fiber), Decide (event loop, plain PHP), Know (kernel polling)। Fiber নিজে কখনো জাগে না। Event loop কোনো জাদু না, শুধু resume()-এর সিদ্ধান্ত নেওয়া code।
- select vs epoll পার্থক্যটা capability-র না, scale-এর। দুটোই kernel-এ ঘুমায়। select-এর রোগ হলো প্রতি wake-up-এ পুরো list-এর rescan tax + ~1024 FD limit। epoll-এ kernel নিজেই ready-list রাখে।
- epoll language feature না, kernel feature। Go, Rust, Node, PHP, সবাই একই syscall-এর উপর দাঁড়িয়ে।
আমার মতো যদি আপনিও sync-only PHP developer হন: এখনই production-এ কিছু বদলাতে হবে না। PHP-FPM আপনার Laravel app-এর জন্য ঠিকই আছে। কিন্তু এই mental model-টা থাকলে ReactPHP/AMPHP-র documentation আর ভয় লাগবে না, আর PHP 8.6 release হলে বুঝবেন আসলে কী বদলালো।
সবচেয়ে বড় শিক্ষা: নিজে code run করুন। আমি Fiber-এর behavior নিয়ে ভুল ধারণায় ছিলাম, ৫ লাইনের একটা script run করে সেটা ভাঙলো। Build, don't just consume.
Original article: Fibers Plus the Polling API: What Async PHP Actually Looks Like Now by JustSteveKing.
Top comments (0)