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— данные, привязанные к выполнению, а не к процессу.
Заметьте, чему нам не пришлось учиться: мьютексам, семафорам, гонкам данных, коллбэкам и ключевым словам async/await на каждой строчке. Код остался обычным последовательным PHP, который просто перестал простаивать.
Дальше два пути. За практикой — в документацию, там каждый компонент разобран до винтика. За пониманием, как это устроено изнутри, — в архитектуру. А лучше всего просто возьмите свой самый медленный скрипт и посмотрите, что с ним сделает один spawn.