0Pricing
C# Academy · 강의

System.Threading.Channels

백프레셔를 제어하기 위해 Channel, ChannelWriter, ChannelReader로 생산자-소비자 파이프라인을 구축합니다.

System.Threading.Channels은(는) CoddyKit의 무료 C# Academy 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 C# Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. C# Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

채널이란 무엇인가요

System.Threading.Channels(.NET Core 3에서 도입됨)은 고성능의 스레드 안전 생산자-소비자 큐를 제공합니다. BlockingCollection과 달리 채널은 완전히 비동기적이고 할당 효율이 높아 프로세스 내부 파이프라인에 적합합니다.

채널 만들기

채널은 팩토리를 사용해 만듭니다. unbounded(제한 없음) 또는 bounded(역압이 적용되는 제한된 용량)를 선택할 수 있습니다. 팩토리는 Channel<T>과 Writer 및 Reader를 반환합니다.

using System.Threading.Channels;

// Unbounded: unlimited capacity, no backpressure
var unbounded = Channel.CreateUnbounded<string>();

// Bounded: max 100 items; writer waits when full
var bounded = Channel.CreateBounded<string>(100);

// Bounded with drop-oldest strategy:
var dropping = Channel.CreateBounded<string>(new BoundedChannelOptions(50)
{
    FullMode = BoundedChannelFullMode.DropOldest
});

채널에 쓰기

생산자는 WriteAsync(bounded 채널이 가득 차면 기다림) 또는 TryWrite(가득 찬 경우 즉시 false 반환)를 사용해 항목을 씁니다. Writer.Complete()로 완료를 알립니다.

var channel = Channel.CreateUnbounded<int>();

// Producer task
var producer = Task.Run(async () =>
{
    for (int i = 0; i < 100; i++)
    {
        await channel.Writer.WriteAsync(i);
        await Task.Delay(10);
    }
    channel.Writer.Complete(); // signal no more items
});

채널에서 읽기

소비자는 ReadAllAsync()(가장 간단한 방법) 또는 더 세밀하게 제어할 수 있는 ReadAsync/TryRead를 사용해 읽습니다. 작성자가 Complete()을 호출하면 ReadAllAsync가 완료됩니다.

// Consumer task
var consumer = Task.Run(async () =>
{
    await foreach (var item in channel.Reader.ReadAllAsync())
    {
        Console.WriteLine($"Processed: {item}");
        // Naturally paced — waits for next item
    }
    Console.WriteLine("Channel completed");
});

await Task.WhenAll(producer, consumer);

여러 생산자

채널은 스레드 안전합니다. 여러 생산자가 동시에 쓸 수 있으므로 잠금이 필요하지 않습니다. 모든 생산자가 작업을 마친 후에만 Writer.Complete()을 호출합니다.

var channel = Channel.CreateUnbounded<WorkItem>();

var producers = Enumerable.Range(0, 4).Select(id =>
    Task.Run(async () =>
    {
        for (int i = 0; i < 25; i++)
            await channel.Writer.WriteAsync(new WorkItem(id, i));
    }));

// Wait for all producers before completing the writer
await Task.WhenAll(producers);
channel.Writer.Complete();

여러 소비자(팬아웃)

같은 채널 리더에서 여러 소비자 작업을 실행하면 처리를 병렬화할 수 있습니다. 각 항목은 정확히 하나의 소비자에게 전달되며, 모든 소비자에게 브로드캐스트되지는 않습니다.

var channel = Channel.CreateBounded<WorkItem>(100);

// 4 parallel consumers
var consumers = Enumerable.Range(0, 4).Select(id =>
    Task.Run(async () =>
    {
        await foreach (var item in channel.Reader.ReadAllAsync())
        {
            await ProcessItemAsync(item);
            Console.WriteLine($"Consumer {id} processed {item.Id}");
        }
    }));

await Task.WhenAll(consumers);

파이프라인 패턴

채널을 연결해 처리 파이프라인을 구성합니다. 각 단계는 한 채널에서 읽고, 항목을 처리한 다음, 다음 채널에 씁니다. 각 단계가 동시에 실행되며 자연스럽게 역압이 적용됩니다.

// Stage 1: raw data
var stage1 = Channel.CreateBounded<string>(50);
// Stage 2: parsed
var stage2 = Channel.CreateBounded<ParsedRecord>(50);
// Stage 3: enriched
var stage3 = Channel.CreateBounded<EnrichedRecord>(50);

var parse   = ParseStageAsync(stage1.Reader, stage2.Writer);
var enrich  = EnrichStageAsync(stage2.Reader, stage3.Writer);
var persist = PersistStageAsync(stage3.Reader);

await Task.WhenAll(parse, enrich, persist);

bounded 채널을 사용한 역압

bounded 채널은 자동으로 역압을 적용합니다. 채널이 가득 차면 소비자가 항목을 꺼낼 때까지 WriteAsync가 생산자를 일시 중단합니다. 수동으로 조절하는 코드를 작성할 필요가 없습니다.

// Bounded channel: max 10 items
var channel = Channel.CreateBounded<string>(10);

// Fast producer
var producer = Task.Run(async () =>
{
    for (int i = 0; i < 1000; i++)
    {
        // WriteAsync waits when channel has 10 items
        await channel.Writer.WriteAsync($"item-{i}");
        // Producer is naturally slowed to consumer speed
    }
    channel.Writer.Complete();
});

// Slow consumer
var consumer = Task.Run(async () =>
{
    await foreach (var item in channel.Reader.ReadAllAsync())
    {
        await Task.Delay(50); // simulate slow processing
        Console.WriteLine(item);
    }
});

채널의 오류 처리

Writer.Complete(exception)에 예외를 전달하면 대기 중인 모든 리더에게 오류를 전파할 수 있습니다. 리더는 다음에 채널을 읽을 때 예외를 확인합니다.

var channel = Channel.CreateUnbounded<int>();

var producer = Task.Run(async () =>
{
    try
    {
        for (int i = 0; i < 100; i++)
        {
            if (i == 50) throw new Exception("Producer failed at 50");
            await channel.Writer.WriteAsync(i);
        }
        channel.Writer.Complete();
    }
    catch (Exception ex)
    {
        channel.Writer.Complete(ex); // propagate to reader
    }
});

try
{
    await foreach (var item in channel.Reader.ReadAllAsync())
        Console.WriteLine(item);
}
catch (Exception ex)
{
    Console.Error.WriteLine($"Channel error: {ex.Message}");
}

실전: 백그라운드 작업 큐

호스팅된 서비스와 채널을 사용하는 백그라운드 작업 큐의 예입니다. HTTP 요청이 작업을 큐에 넣으면 백그라운드 작업자가 작업을 한 번에 하나씩 처리합니다.

public class JobQueue
{
    private readonly Channel<Func<CancellationToken, Task>> _queue
        = Channel.CreateBounded<Func<CancellationToken, Task>>(100);

    public ChannelWriter<Func<CancellationToken, Task>> Writer => _queue.Writer;
    public ChannelReader<Func<CancellationToken, Task>> Reader => _queue.Reader;
}

public class JobProcessor : BackgroundService
{
    private readonly JobQueue _queue;
    public JobProcessor(JobQueue q) => _queue = q;

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        await foreach (var job in _queue.Reader.ReadAllAsync(ct))
            await job(ct);
    }
}

간단 확인

생산자가 가득 찬 bounded 채널에서 WriteAsync를 호출하면 어떻게 되나요?

복습: System.Threading.Channels

핵심 요점:

  • 채널은 고성능의 비동기 중심 생산자-소비자 큐를 제공합니다
  • unbounded: 제한 없음, bounded: 역압 또는 삭제 전략이 적용되는 유한한 용량
  • 여러 생산자와 소비자를 잠금 없이 안전하게 사용할 수 있습니다
  • ReadAllAsync() + await foreach는 가장 깔끔한 소비 패턴입니다
  • 채널을 파이프라인으로 연결하면 단계별 동시 처리를 구현할 수 있습니다
  • 스트림의 끝을 알리려면 Writer.Complete()(또는 Complete(exception))을 호출합니다

자주 묻는 질문

“System.Threading.Channels” 강의는 무료인가요?

네 — “System.Threading.Channels” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 C# Academy 강의 전체를 잠금 해제할 수 있습니다. C# Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“System.Threading.Channels”에서 뭘 배우나요?

백프레셔를 제어하기 위해 Channel, ChannelWriter, ChannelReader로 생산자-소비자 파이프라인을 구축합니다. 브라우저에서 직접 실행하는 실습 코드로 C# Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

C# Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 C# Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“System.Threading.Channels” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 C# Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 C# Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. IAsyncEnumerable과 await foreach
  2. System.Threading.Channels
  3. ValueTask와 할당 방지
  4. ConfigureAwait와 동기화 컨텍스트
← C# Academy(으)로 돌아가기