Scope: управление жизнью корутин

Проблема: корутины-зомби

Представьте, что вы запустили кучу корутин и забыли про них:

function processUser($userId) {
    spawn(sendEmail(...), $userId);
    spawn(updateCache(...), $userId);
    spawn(logActivity(...), $userId);

    return "OK";
}

processUser(123);
// Функция вернулась, но три корутины еще работают!
// Кто за ними следит? Когда они закончатся?
// Что если они упадут с ошибкой?

Корутины превратились в зомби — они работают где-то там, но никто не знает где и когда они закончатся.

Решение: Scope

Scope — это родитель для корутин. Пока родитель жив, дети работают. Родитель ушел — дети тоже завершаются.

function processUser($userId) {
    $scope = new Async\Scope();

    spawn with $scope sendEmail($userId);
    spawn with $scope updateCache($userId);
    spawn with $scope logActivity($userId);

    // Ждем, пока все корутины в scope завершатся
    $scope->awaitCompletion();

    return "OK";
}

// Теперь функция вернется только когда ВСЕ корутины закончат работу

Никаких зомби. Все под контролем.

Scope = купол для корутин

Думайте о scope как о стеклянном куполе:

class UserService {
    private Scope $scope;

    public function __construct() {
        // Создаем купол для всех корутин сервиса
        $this->scope = new Async\Scope();
    }

    public function sendNotification($userId) {
        // Запускаем корутину внутрь нашего купола
        spawn with $this->scope function() use ($userId) {
            // Эта корутина привязана к UserService
            sendEmail($userId);
        };
    }

    public function __destruct() {
        // Когда объект удаляется, убираем купол
        // Все корутины внутри автоматически отменяются
        $this->scope->dispose();
    }
}

$service = new UserService();
$service->sendNotification(123);
$service->sendNotification(456);

// Удаляем сервис - все его корутины автоматически отменятся
unset($service);

Иерархия scope

Scope может содержать другие scope. Как матрешки.

$mainScope = new Async\Scope();

spawn with $mainScope function() {
    echo "Главная задача\n";

    // Создаем дочерний scope
    $childScope = Async\Scope::inherit();

    spawn with $childScope function() {
        echo "Подзадача 1\n";
    };

    spawn with $childScope function() {
        echo "Подзадача 2\n";
    };

    // Ждем завершения подзадач
    $childScope->awaitCompletion();

    echo "Все подзадачи готовы\n";
};

$mainScope->awaitCompletion();

Если отменить $mainScope, отменятся и все дочерние scope. Вся иерархия.

Отмена всех корутин в scope

$scope = new Async\Scope();

spawn with $scope function() {
    try {
        while (true) {
            echo "Работаю...\n";
            Async\sleep(1000);
        }
    } catch (Async\CancellationException $e) {
        echo "Меня отменили!\n";
    }
};

spawn with $scope function() {
    try {
        while (true) {
            echo "Тоже работаю...\n";
            Async\sleep(1000);
        }
    } catch (Async\CancellationException $e) {
        echo "И меня тоже!\n";
    }
};

// Работает 3 секунды
Async\sleep(3000);

// Отменяем ВСЕ корутины в scope
$scope->cancel();

// Обе корутины получат CancellationException

Обработка ошибок в scope

Когда корутина внутри scope падает с ошибкой, scope может это поймать:

$scope = new Async\Scope();

// Устанавливаем обработчик ошибок
$scope->onException(function(Throwable $e) {
    echo "Ошибка в scope: " . $e->getMessage() . "\n";
    // Можно залогировать, отправить в Sentry, etc
});

spawn with $scope function() {
    throw new Exception("Что-то сломалось!");
};

spawn with $scope function() {
    echo "Я работаю нормально\n";
};

$scope->awaitCompletion();

// Вывод:
// Ошибка в scope: Что-то сломалось!
// Я работаю нормально

Finally: гарантированная очистка

Даже если scope отменяется, finally-блоки выполнятся:

$scope = new Async\Scope();

spawn with $scope function() {
    try {
        echo "Начинаю работу\n";
        Async\sleep(10000); // Долгая операция
        echo "Закончил\n"; // Не выполнится
    } finally {
        // Это ГАРАНТИРОВАННО выполнится
        echo "Очистка ресурсов\n";
        closeConnection();
    }
};

Async\sleep(1000);
$scope->cancel(); // Отменяем через секунду

// Вывод:
// Начинаю работу
// Очистка ресурсов

TaskGroup: scope с результатами

TaskGroup — это специальный scope, который собирает результаты всех корутин:

$taskGroup = new Async\TaskGroup(captureResults: true);

spawn with $taskGroup function() {
    return "Результат 1";
};

spawn with $taskGroup function() {
    return "Результат 2";
};

spawn with $taskGroup function() {
    return "Результат 3";
};

// Получаем массив результатов
$results = await($taskGroup);
// ["Результат 1", "Результат 2", "Результат 3"]

Global Scope: всегда есть родитель

Если вы не указали scope явно, корутина создается в global scope:

// Без указания scope
spawn(function() {
    echo "Я в global scope\n";
});

// То же самое, что:
spawn with Async\Scope::global() function() {
    echo "Я в global scope\n";
};

Global scope живет весь запрос. Когда PHP завершается, все корутины в global scope отменяются gracefully.

Реальный пример: HTTP-клиент

class HttpClient {
    private Scope $scope;

    public function __construct() {
        $this->scope = new Async\Scope();
    }

    public function get(string $url): Async\Awaitable {
        return spawn with $this->scope function() use ($url) {
            $ch = curl_init($url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

            try {
                return curl_exec($ch);
            } finally {
                curl_close($ch);
            }
        };
    }

    public function cancelAll(): void {
        // Отменяем все активные запросы
        $this->scope->cancel();
    }

    public function __destruct() {
        // При удалении клиента все запросы автоматически отменяются
        $this->scope->dispose();
    }
}

$client = new HttpClient();

$req1 = $client->get('https://api1.com/data');
$req2 = $client->get('https://api2.com/data');
$req3 = $client->get('https://api3.com/data');

// Отменяем все запросы
$client->cancelAll();

// Или просто удаляем клиент - эффект тот же
unset($client);

Это называется Structured Concurrency

Идея простая: каждая корутина должна иметь родителя. Не должно быть бесхозных корутин.

Как с памятью: в PHP есть garbage collector, который убирает неиспользуемые объекты. Scope — это как GC для корутин.

Дальше что?