채널

채널은 단일 스레드 환경보다 멀티스레드 환경에서의 통신에 더 유용합니다. 채널은 한 코루틴에서 다른 코루틴으로 안전하게 데이터를 전송하는 데 사용됩니다. 공유 데이터를 수정해야 하는 경우, 단일 스레드 환경에서는 채널을 만드는 것보다 객체를 여러 코루틴에 전달하는 것이 더 간단합니다.

그러나 채널은 다음 시나리오에서 유용합니다:

예를 들어, 크롤링할 URL이 많지만 동시 연결은 N개 이하여야 하는 경우:

use Async\Channel;
use Async\Scope;

const MAX_CONNECTIONS = 10;
const MAX_QUEUE = 100;

$tasks = new Scope();
$channel = new Channel(MAX_QUEUE);

for($i = 0; $i < MAX_CONNECTIONS; $i++) {
    $tasks->spawn(function() use ($channel) {
        while (!$channel->isClosed()) {
            $url = $channel->recv();
            $content = file_get_contents($url);
            echo "Fetched page {$url}, length: " . strlen($content) . "\n";
        }
    });
}

// 채널을 값으로 채웁니다
for($i = 0; $i < MAX_CONNECTIONS * 2; $i++) {
    $channel->send("https://example.com/{$i}");
}

이 예제에서 MAX_QUEUE 상수는 생산자의 제한자로 작동하여 배압(backpressure)을 만듭니다 – 소비자가 채널의 공간을 확보할 때까지 생산자가 데이터를 보낼 수 없는 상황입니다.

버퍼 없는 채널 (랑데부)

버퍼 크기가 0인 채널은 랑데부 모드로 작동합니다: send()는 다른 코루틴이 recv()를 호출할 때까지 차단되고, 그 반대도 마찬가지입니다. 이는 엄격한 동기화를 보장합니다:

use Async\Channel;

$ch = new Channel(0); // 랑데부 채널

spawn(function() use ($ch) {
    echo "송신자: send 전\n";
    $ch->send("hello");
    echo "송신자: send 완료\n"; // recv() 이후에만
});

spawn(function() use ($ch) {
    echo "수신자: recv 전\n";
    $value = $ch->recv();
    echo "수신자: $value 수신됨\n";
});

작업 취소

recv()send() 메서드는 선택적 취소 토큰(Completable)을 받아 임의의 조건에 따라 대기를 중단할 수 있습니다. 이는 고정된 타임아웃보다 유연합니다 — 다른 코루틴에서, 시그널로, 이벤트로, 또는 시간으로 연산을 취소할 수 있습니다:

use Async\Channel;
use Async\CancelledException;

$ch = new Channel(0);

// 타임아웃으로 취소
spawn(function() use ($ch) {
    try {
        $ch->recv(Async\timeout(50)); // 최대 50ms 대기
    } catch (CancelledException $e) {
        echo "50ms 내에 아무도 데이터를 보내지 않았습니다\n";
    }
});

// 임의의 조건으로 취소
spawn(function() use ($ch) {
    $cancel = new \Async\Future();

    spawn(function() use ($cancel) {
        // 50ms 후에 취소
        Async\delay(50);
        $cancel->complete(null);
    });

    try {
        $ch->send("data", $cancel);
    } catch (CancelledException $e) {
        echo "아무도 데이터를 수신하지 않았습니다 — 연산이 취소되었습니다\n";
    }
});

경쟁 수신자

여러 코루틴이 같은 채널에서 recv()를 대기하는 경우, 각 값은 그 중 하나만 수신합니다. 값은 복제되지 않습니다:

use Async\Channel;

$ch = new Channel(0);

// 송신자
spawn(function() use ($ch) {
    for ($i = 1; $i <= 3; $i++) {
        $ch->send($i);
    }
    $ch->close();
});

// 수신자 A
spawn(function() use ($ch) {
    try {
        while (true) {
            $v = $ch->recv();
            echo "A 수신: $v\n";
        }
    } catch (\Async\ChannelException) {}
});

// 수신자 B
spawn(function() use ($ch) {
    try {
        while (true) {
            $v = $ch->recv();
            echo "B 수신: $v\n";
        }
    } catch (\Async\ChannelException) {}
});

// 각 값(1, 2, 3)은 A 또는 B 중 하나만 수신하며, 둘 다 수신하지 않습니다

이 패턴은 여러 코루틴이 공유 큐에서 작업을 경쟁하는 워커 풀을 구현하는 데 유용합니다.