ОсновыContext

Context

Классический PHP прожил всю жизнь по простому правилу: один процесс, один запрос. Из него выросла целая культура: глобальные переменные, статические свойства, синглтоны. Текущий пользователь? Статическое свойство Auth::$user. Идентификатор запроса для логов? Глобальная переменная. Работало безотказно, потому что «текущее» и правда было одно на весь процесс.

TrueAsync это правило отменил. Теперь в одном процессе вперемешку живут сотни корутин, обслуживающих разных пользователей. Присвоили Auth::$user в одной корутине, а прочитает его следующая проснувшаяся, и хорошо, если это тот же запрос. Знакомая история: так же в девятой главе десять воркеров делили один PDO. Гонки без потоков, теперь в глобальном состоянии.

Передавать всё параметрами? Честно, но беспощадно: токен авторизации придётся протащить через двадцать сигнатур, из которых он нужен одной. Хочется хранилище, привязанное не к процессу, а к логическому потоку выполнения. У нас как раз есть подходящая структура: корутины и scope уже выстроены в дерево.

Хранилище на дереве

Async\Context — это хранилище ключ-значение, привязанное к scope или к корутине. Контекст scope доступен всем его корутинам:

php
use function Async\current_context;

// middleware, начало обработки запроса
current_context()
    ->set('request_id', bin2hex(random_bytes(8)))
    ->set('user_id', $userId);

А дальше в любом месте, на любой глубине вызовов, без единого дополнительного параметра:

php
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, и ни одной глобальной переменной.

Три этажа

Контекст есть на трёх уровнях, от широкого к узкому:

php
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: не нашёл у себя, спросил у родителя.

И ещё одна приятная деталь:

php
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.