Context
Класичний PHP усе своє життя прожив за одним простим правилом: один процес, один запит. З цього правила виросла ціла культура: глобальні змінні, статичні властивості, синглтони. Поточний користувач? Статична властивість Auth::$user. Ідентифікатор запиту для логування? Глобальна змінна. Це працювало бездоганно, бо «поточне» справді було одним-єдиним на весь процес.
TrueAsync скасував це правило. Тепер сотні корутин живуть перемішані в одному процесі, обслуговуючи різних користувачів. Присвоїш Auth::$user в одній корутині, а наступна, що прокинеться, прочитає його назад, і тобі ще пощастить, якщо це взагалі той самий запит. Знайома історія: те саме сталося в дев'ятій главі, коли десять воркерів ділили один PDO. Гонки без потоків, тільки тепер у глобальному стані.
Передавати все як параметри натомість? Чесно, але безжально: тобі довелося б протягувати токен авторизації крізь двадцять сигнатур функцій, коли насправді він потрібен лише одній із них. Що тобі справді потрібно, так це сховище, прив'язане не до процесу, а до логічної нитки виконання. А в нас уже є структура, придатна для цієї роботи: корутини й scope вже утворюють дерево.
Сховище на дереві
Async\Context це сховище ключ-значення, приєднане до scope або до корутини. Контекст scope доступний усім його корутинам:
use function Async\current_context;
// middleware, початок обробки запиту
current_context()
->set('request_id', bin2hex(random_bytes(8)))
->set('user_id', $userId);І звідти, будь-де, на будь-якій глибині виклику, без жодного зайвого параметра:
function logInfo(string $message): void
{
$requestId = current_context()->find('request_id');
error_log("[$requestId] $message");
}Зверни увагу на find: він шукає ключ у поточному контексті, а якщо не знаходить, дереться вгору деревом scope. Дочірні scope автоматично бачать дані своїх батьків, тож корутина, запущена глибоко всередині обробника, знайде request_id, встановлений на самому початку запиту. Це той самий механізм, що й context.Context у Go, тільки тобі не доводиться знаходити його і передавати з рук у руки: дерево вже на місці.
Запити більше не заважають одне одному: кожен має власний scope, а отже, і власний контекст. Тисяча конкурентних запитів, тисяча незалежних request_id, і жодної глобальної змінної.
Три рівні
Контекст існує на трьох рівнях, від найширшого до найвужчого:
use function Async\root_context;
use function Async\current_context;
use function Async\coroutine_context;
root_context(); // весь процес: конфігурація, спільна для всіх
current_context(); // поточний scope: запит, користувач, локаль
coroutine_context(); // лише ця корутина: приватні даніroot_context це законна домівка для всього, що раніше по праву було глобальною змінною: назва застосунку, налаштування. Ти звертаєшся до нього явно: root_context()->find('app_name'). coroutine_context це протилежний полюс: його дані невидимі для будь-кого, крім самої корутини, навіть для її сусідів у тому самому scope. Між цими двома find зшиває рівні scope докупи: не знайшов локально, спитай батька.
І ще одна приємна деталь:
current_context()->set('user_id', 42);
current_context()->set('user_id', 7); // AsyncException: ключ уже існуєПерезапис заборонено, доки ти явно про це не попросиш (replace: true). Контекст оберігає сам себе від тієї самої хвороби глобальних змінних, з якої почалася ця глава: хтось, десь, тихо перезаписує значення.
Кінець подорожі
П'ятнадцять глав тому ми запустили дві функції «водночас» і здивувалися, побачивши, як їхній вивід переплітається. Відтоді склалася ціла система, і кожен її рівень має власну турботу:
- Корутини,
spawn,await— одиниця конкурентності й обіцянка результату. - Скасування, тайм-аути, винятки — контракт переривання: ніщо не працює вічно, і ніщо не вмирає мовчки.
Futureі канали — з'єднання: одинокий результат і потік значень із синхронізацією.Scope,TaskGroup,TaskSet,iterate— структура: кожна корутина має власника, а кожна група має стратегію очікування.- Пули — дисципліна ресурсів: мало з'єднань, багато корутин.
- Потоки — паралелізм для обчислень, без спільної пам'яті.
Context— дані, прив'язані до виконання, а не до процесу.
Зверни увагу, чого нам ніколи не довелося вчити: м'ютекси, семафори, гонки даних, колбеки чи розфарбовування функцій в асинхронні проти синхронних. Код залишався звичайним, послідовним PHP, який просто перестав сидіти без діла.
Звідси два шляхи. Для практики руками прямуй до документації, де кожен компонент розібрано до останнього гвинтика. Для розуміння, як усе це працює під капотом, прямуй до архітектури. Або, найкраще, просто візьми свій найповільніший скрипт і подивись, що з ним зробить один-єдиний spawn.