Async\Coroutine 클래스

(PHP 8.6+, True Async 1.0)

TrueAsync의 코루틴

일반 함수가 freadfwrite 같은 I/O 작업(파일 읽기 또는 네트워크 요청)을 호출하면, 제어가 운영 체제 커널로 전달되고 작업이 완료될 때까지 PHP가 차단됩니다.

그러나 코루틴 내에서 함수가 실행되고 I/O 작업을 호출하면, 전체 PHP 프로세스가 아닌 코루틴만 차단됩니다. 그동안 다른 코루틴이 있다면 제어가 전달됩니다.

이런 의미에서 코루틴은 운영 체제 스레드와 매우 유사하지만, OS 커널이 아닌 사용자 공간에서 관리됩니다.

또 다른 중요한 차이점은 코루틴은 CPU 시간을 번갈아 사용하며 자발적으로 제어를 양보하는 반면, 스레드는 언제든지 선점될 수 있다는 것입니다.

TrueAsync 코루틴은 단일 스레드 내에서 실행되며 병렬이 아닙니다. 이는 몇 가지 중요한 결과를 초래합니다:

코루틴 생성

코루틴은 spawn() 함수를 사용하여 생성합니다:

use function Async\spawn;

// 코루틴 생성
$coroutine = spawn(function() {
    echo "Hello from a coroutine!\n";
    return 42;
});

// $coroutine은 Async\Coroutine 타입의 객체입니다
// 코루틴은 이미 실행 예약되어 있습니다

spawn이 호출되면 함수는 스케줄러에 의해 가능한 빨리 비동기적으로 실행됩니다.

매개변수 전달

spawn 함수는 callable과 해당 함수가 시작될 때 전달될 모든 매개변수를 받습니다.

function fetchUser(int $userId) {
    return file_get_contents("https://api/users/$userId");
}

// 함수와 매개변수 전달
$coroutine = spawn(fetchUser(...), 123);

결과 가져오기

코루틴의 결과를 얻으려면 await()를 사용합니다:

$coroutine = spawn(function() {
    sleep(2);
    return "Done!";
});

echo "Coroutine started\n";

// 결과 대기
$result = await($coroutine);

echo "Result: $result\n";

중요: await()현재 코루틴의 실행을 차단하지만, 전체 PHP 프로세스를 차단하지는 않습니다. 다른 코루틴은 계속 실행됩니다.

코루틴 수명 주기

코루틴은 여러 상태를 거칩니다:

  1. 대기 중(Queued)spawn()으로 생성됨, 스케줄러에 의해 시작 대기 중
  2. 실행 중(Running) – 현재 실행 중
  3. 일시 중단(Suspended) – 일시 정지, I/O 또는 suspend() 대기 중
  4. 완료(Completed) – 실행 완료 (결과 또는 예외와 함께)
  5. 취소됨(Cancelled)cancel()로 취소됨

상태 확인

$coro = spawn(longTask(...));

var_dump($coro->isQueued());     // true - 시작 대기 중
var_dump($coro->isStarted());   // false - 아직 시작되지 않음

suspend(); // 코루틴이 시작되도록 함

var_dump($coro->isStarted());    // true - 코루틴이 시작됨
var_dump($coro->isRunning());    // false - 현재 실행 중이 아님
var_dump($coro->isSuspended());  // true - 일시 중단, 무언가를 기다리는 중
var_dump($coro->isCompleted());  // false - 아직 완료되지 않음
var_dump($coro->isCancelled());  // false - 취소되지 않음

일시 중단: suspend

suspend 키워드는 코루틴을 멈추고 제어를 스케줄러에 전달합니다:

spawn(function() {
    echo "Before suspend\n";

    suspend(); // 여기서 멈춤

    echo "After suspend\n";
});

echo "Main code\n";

// 출력:
// Before suspend
// Main code
// After suspend

코루틴이 suspend에서 멈추고, 제어가 메인 코드로 돌아갔습니다. 나중에 스케줄러가 코루틴을 재개했습니다.

대기와 함께 suspend

일반적으로 suspend는 어떤 이벤트를 기다리는 데 사용됩니다:

spawn(function() {
    echo "Making an HTTP request\n";

    $data = file_get_contents('https://api.example.com/data');
    // file_get_contents 내부에서 suspend가 암시적으로 호출됩니다
    // 네트워크 요청이 진행되는 동안 코루틴은 일시 중단됩니다

    echo "Got data: $data\n";
});

PHP는 I/O 작업에서 자동으로 코루틴을 일시 중단합니다. 수동으로 suspend를 작성할 필요가 없습니다.

코루틴 취소

$coro = spawn(function() {
    try {
        echo "Starting long work\n";

        for ($i = 0; $i < 100; $i++) {
            Async\sleep(100); // 100ms 대기
            echo "Iteration $i\n";
        }

        echo "Finished\n";
    } catch (Async\AsyncCancellation $e) {
        echo "I was cancelled during iteration\n";
    }
});

// 코루틴이 1초간 작업하도록 함
Async\sleep(1000);

// 취소
$coro->cancel();

// 코루틴은 다음 await/suspend에서 AsyncCancellation을 받습니다

중요: 취소는 협력적으로 작동합니다. 코루틴은 (await, sleep 또는 suspend를 통해) 취소를 확인해야 합니다. 코루틴을 강제로 종료할 수 없습니다.

다중 코루틴

원하는 만큼 시작할 수 있습니다:

$tasks = [];

for ($i = 0; $i < 10; $i++) {
    $tasks[] = spawn(function() use ($i) {
        $result = file_get_contents("https://api/data/$i");
        return $result;
    });
}

// 모든 코루틴 대기
$results = array_map(fn($t) => await($t), $tasks);

echo "Loaded " . count($results) . " results\n";

10개의 요청이 모두 동시에 실행됩니다. 10초(각각 1초) 대신 약 1초 만에 완료됩니다.

오류 처리

코루틴의 오류는 일반 try-catch로 처리됩니다:

$coro = spawn(function() {
    throw new Exception("Oops!");
});

try {
    $result = await($coro);
} catch (Exception $e) {
    echo "Caught error: " . $e->getMessage() . "\n";
}

오류가 잡히지 않으면 상위 스코프로 전파됩니다:

$scope = new Async\Scope();

$scope->spawn(function() {
    throw new Exception("Error in coroutine!");
});

try {
    $scope->awaitCompletion();
} catch (Exception $e) {
    echo "Error bubbled up to scope: " . $e->getMessage() . "\n";
}

코루틴 = 객체

코루틴은 완전한 PHP 객체입니다. 어디에든 전달할 수 있습니다:

function startBackgroundTask(): Async\Coroutine {
    return spawn(function() {
        // 긴 작업
        Async\sleep(10000);
        return "Result";
    });
}

$task = startBackgroundTask();

// 다른 함수에 전달
processTask($task);

// 또는 배열에 저장
$tasks[] = $task;

// 또는 객체 속성에 저장
$this->backgroundTask = $task;

중첩 코루틴

코루틴은 다른 코루틴을 실행할 수 있습니다:

spawn(function() {
    echo "Parent coroutine\n";

    $child1 = spawn(function() {
        echo "Child coroutine 1\n";
        return "Result 1";
    });

    $child2 = spawn(function() {
        echo "Child coroutine 2\n";
        return "Result 2";
    });

    // 두 자식 코루틴 대기
    $result1 = await($child1);
    $result2 = await($child2);

    echo "Parent received: $result1 and $result2\n";
});

Finally: 보장된 정리

코루틴이 취소되더라도 finally는 실행됩니다:

spawn(function() {
    $file = fopen('data.txt', 'r');

    try {
        while ($line = fgets($file)) {
            processLine($line);
            suspend(); // 여기서 취소될 수 있음
        }
    } finally {
        // 어떤 경우에도 파일이 닫힙니다
        fclose($file);
        echo "File closed\n";
    }
});

코루틴 디버깅

콜 스택 가져오기

$coro = spawn(function() {
    doSomething();
});

// 코루틴의 콜 스택 가져오기
$trace = $coro->getTrace();
print_r($trace);

코루틴이 생성된 위치 확인

$coro = spawn(someFunction(...));

// spawn()이 호출된 위치
echo "Coroutine created at: " . $coro->getSpawnLocation() . "\n";
// 출력: "Coroutine created at: /app/server.php:42"

// 또는 배열 [filename, lineno]
[$file, $line] = $coro->getSpawnFileAndLine();

코루틴이 일시 중단된 위치 확인

$coro = spawn(function() {
    file_get_contents('https://api.example.com/data'); // 여기서 일시 중단
});

suspend(); // 코루틴이 시작되도록 함

echo "Suspended at: " . $coro->getSuspendLocation() . "\n";
// 출력: "Suspended at: /app/server.php:45"

[$file, $line] = $coro->getSuspendFileAndLine();

대기 정보

$coro = spawn(function() {
    Async\delay(5000);
});

suspend();

// 코루틴이 무엇을 기다리고 있는지 확인
$info = $coro->getAwaitingInfo();
print_r($info);

디버깅에 매우 유용합니다 – 코루틴이 어디서 왔고 어디서 멈췄는지 즉시 확인할 수 있습니다.

코루틴 vs 스레드

코루틴 스레드
경량 중량
빠른 생성 (<1us) 느린 생성 (~1ms)
단일 OS 스레드 여러 OS 스레드
협력적 멀티태스킹 선점형 멀티태스킹
경쟁 상태 없음 경쟁 상태 가능
await 지점 필요 어디서든 선점 가능
I/O 작업용 CPU 집약적 연산용

protect()를 사용한 지연 취소

코루틴이 protect()를 통해 보호된 섹션 내에 있는 경우, 보호된 블록이 완료될 때까지 취소가 지연됩니다:

$coro = spawn(function() {
    $result = protect(function() {
        // 크리티컬 작업 -- 취소가 지연됨
        $db->beginTransaction();
        $db->execute('INSERT INTO logs ...');
        $db->commit();
        return "saved";
    });

    // protect()를 빠져나온 후 여기서 취소가 발생합니다
    echo "Result: $result\n";
});

suspend();

$coro->cancel(); // 취소가 지연됨 -- protect()가 완전히 완료됩니다

isCancellationRequested() 플래그는 즉시 true가 되고, isCancelled()는 코루틴이 실제로 종료된 후에만 true가 됩니다.

클래스 개요

final class Async\Coroutine implements Async\Completable {

    /* 식별 */
    public getId(): int

    /* 우선순위 */
    public asHiPriority(): Coroutine

    /* 컨텍스트 */
    public getContext(): Async\Context

    /* 결과와 오류 */
    public getResult(): mixed
    public getException(): mixed

    /* 상태 */
    public isStarted(): bool
    public isQueued(): bool
    public isRunning(): bool
    public isSuspended(): bool
    public isCompleted(): bool
    public isCancelled(): bool
    public isCancellationRequested(): bool

    /* 제어 */
    public cancel(?Async\AsyncCancellation $cancellation = null): void
    public finally(\Closure $callback): void

    /* 디버깅 */
    public getTrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT, int $limit = 0): ?array
    public getSpawnFileAndLine(): array
    public getSpawnLocation(): string
    public getSuspendFileAndLine(): array
    public getSuspendLocation(): string
    public getAwaitingInfo(): array
}

목차

다음 단계