RFC مربوط به Polling API بومی که یک API Polling بومی را تعریف می‌کند، تایید و در شاخه master زبان ادغام شده است؛ این امر به توسعه‌دهندگان روشی سریع و در سطح سیستم‌عامل برای بررسی (poll) توصیف‌گرهای فایل (file descriptors) می‌دهد. در کنار Fibers که اخیراً اضافه شده‌اند، PHP اکنون دارای یک زیربنای داخلی و کارآمد برای کدهای ناهمگام (asynchronous) است—شکافی که مدت‌ها توسعه‌دهندگان را مجبور می‌کرد تا راهکارهای جایگزین خود را به صورت دستی سرهم کنند.

فیبرها (Fibers) و حلقه رویداد (event loop)، تفکیک شده

فیبرها (Fibers) یک ابزار اولیه (primitive) برای کنترل جریان (control-flow) در سطح پایین هستند. آن‌ها به یک تابع اجازه می‌دهند تا اجرای خود را در نقطه‌ای مشخص متوقف (suspend) کرده و بعداً دقیقاً از همان‌جا که رها شده بود، از سر بگیرد (resume). نکته حیاتی این است که یک فیبر هیچ اطلاعی از سوکت‌ها، تایمرها یا هر منبع I/O دیگری ندارد؛ فیبر صرفاً بنا به درخواست، متوقف و دوباره شروع می‌شود.

حلقه رویداد (event loop) زمان‌بندی‌کننده‌ای (scheduler) است که تصمیم می‌گیرد چه زمانی یک فیبر متوقف‌شده باید از سر گرفته شود. در یک پشته (stack) ناهمگام معمولی، حلقه مجموعه‌ای از توصیف‌گرهای فایل را زیر نظر می‌گیرد، منتظر می‌ماند تا آن‌ها خواندنی یا نوشتنی شوند و سپس فیبر مربوطه را بیدار می‌کند.

PHP پیش از این Fibers را داشت اما فاقد مکانیزم بومی برای پرسش از سیستم‌عامل درباره اینکه کدام توصیف‌گرها آماده هستند بود. نتیجه این بود که توسعه‌دهندگان به stream_select() یا افزونه‌های خارجی متکی بودند و هر کتابخانه ناهمگام در نهایت مجبور می‌شد بک‌اندهای (back-ends) اختصاصی خود را برای epoll در لینوکس، kqueue در BSD، IOCP در ویندوز و غیره بنویسد.

Polling API جدید این قطعه گم‌شده را پر می‌کند. این API یک پوشش (wrapper) نازک دور قابلیت‌های Polling سیستم‌عامل ایجاد می‌کند (epoll در لینوکس، kqueue در BSD/macOS و غیره). این API جایگزین Fibers نمی‌شود؛ بلکه صرفاً سرعت و مقیاس‌پذیری مورد نیاز حلقه رویداد را فراهم می‌کند.

چرا این تغییر برای ReactPHP، Amp و دوستانشان اهمیت دارد

ReactPHP و Amp v3 لایه‌های انتزاعی (abstraction layers) خود را بر روی مکانیزم‌های Polling سیستم‌عامل ساخته‌اند. آن لایه‌ها شامل مسیرهای کد (code paths) متعددی هستند که هر کدام برای یک پلتفرم خاص بهینه شده‌اند و باید با تغییرات هسته (kernel) همگام نگه داشته شوند. با وجود Polling API بومی، این کتابخانه‌ها می‌توانند بیشتر آن زیرساخت‌های پیچیده (plumbing) را حذف کرده و بر یک فراخوانی واحد که توسط هسته (core) ارائه می‌شود، تکیه کنند.

  • نگهداری (Maintenance) – شاخه‌های کمترِ وابسته به پلتفرم به معنای باگ‌های کمتر و سطح حمله کوچک‌تر برای بررسی‌های امنیتی است.
  • عملکرد (Performance) – فراخوانی بومی مستقیماً با epoll/kqueue صحبت می‌کند.
  • قابلیت حمل (Portability) – کدی که روی PHP خام (vanilla) اجرا می‌شود، اکنون بدون نیاز به افزونه‌های اختیاری، همان عملکرد پایه را در تمام سیستم‌عامل‌های پشتیبانی‌شده دریافت می‌کند.

در مقابل، Swoole همچنان یک جایگزین کامل برای زمان اجرا (runtime) باقی می‌ماند که حلقه رویداد و سیستم کوروتین (coroutine) مخصوص به خود را ارائه می‌دهد. Polalling API بر انتخاب‌های Swoole تأثیری نمی‌گذارد؛ توسعه‌دهندگانی که به تأخیر (latency) بسیار کم یا مدیریت حافظه سفارشی نیاز دارند، همچنان آن را به عنوان یک گزینه مجزا در نظر خواهند گرفت.

یک بنچمارک سریع گویای ماجراست

یک زمان‌بندی‌کننده (scheduler) حداقلی چندین URL را از طریق سوکت‌های خام (raw sockets) فراخوانی کرد و از یک Io\Poll\Context واحد برای مدیریت چندین فیبر استفاده نمود. دو مشاهده حاصل شد:

  1. سرعت (Speed) – اضافه کردن درخواست‌های همزمان (concurrent) بیشتر، زمان کل سپری‌شده را افزایش نمی‌دهد؛ دسته درخواست‌ها به سرعتِ کندترین درخواستِ واحد به پایان می‌رسد. به عبارت دیگر، سربار همزمانی (concurrency overhead) عملاً صفر است.
  2. هزینه CPU (CPU cost) – با استفاده از stream_select()، با افزایش تعداد استریم‌ها، میزان استفاده از CPU به وضوح بالا می‌رود، زیرا این تابع باید در هر فراخوانی روی تک‌تک توصیف‌گرها پیمایش کند. اما هزینه Polling API از سه استریم تا سی استریم ثابت می‌ماند، که این امر مدیون توانایی هسته سیستم‌عامل در نظارت بر تعداد زیادی توصیف‌گر در یک فراخوانی سیستمی واحد است.

توسعه‌دهندگان اکنون باید چه کنند

  • از Amp v3 استفاده کنید اگر سبک مبتنی بر فیبر (fiber-native) را ترجیح می‌دهید که مستقیماً با API جدید همسو است. رابط عمومی آن در حال حاضر با poller زیرساختی مطابقت دارد، بنابراین بدون نیاز به بازنویسی کد خود، از این مزیت بهره‌مند می‌شوید.
  • به ReactPHP پایبند بمانید اگر کنترل صریح بر چرخه حیات (lifecycle) حلقه را دوست دارید.
  • از زمان‌بندی‌کننده‌های سفارشی خودداری کنید برای بارهای کاری عملیاتی (production workloads). برای محیط‌های عملیاتی، زمان‌بندی‌کننده خودتان را ننویسید.