Load everything then render, or stream through a channel

1 min read

Rendering a document splits into I/O (loading each page) and CPU (rendering it), as in I/O-bound or CPU-boundI/O-bound or CPU-bound?Async helps most when the work is waiting, and CPU-heavy work needs Task.Run.Opens in a new tab. There are two shapes.

Option A: load all, then render all

C#
// Phase 1: I/O-bound, load all pages concurrentlyvar pageDataTasks = pageNumbers.Select(n => LoadPageDataAsync(n));PageData[] pages = await Task.WhenAll(pageDataTasks); // Phase 2: CPU-bound, render all pages in parallel across coresvar renderedPages = new RenderedPage[pages.Length];Parallel.For(0, pages.Length, i =>{    renderedPages[i] = RenderPage(pages[i]);});

Task.WhenAll loads concurrently without blocking threads, then Parallel.For renders across cores. It suits documents of tens to low hundreds of pages. Every page’s data sits in memory at once, which is costly for thousands of pages with large images.

Option B: a channel

System.Threading.Channels overlaps the phases: a producer loads pages and consumers render them as they arrive.

C#
var channel = Channel.CreateBounded<PageData>(capacity: 8); // backpressure: at most 8 loaded but unrendered pages // Producer: I/O-bound loadingvar producer = Task.Run(async () =>{    foreach (var pageNum in pageNumbers)    {        var data = await LoadPageDataAsync(pageNum); // frees the thread during the I/O wait        await channel.Writer.WriteAsync(data);    }    channel.Writer.Complete();}); // Consumers: CPU-bound rendering, spread across coresvar consumers = Enumerable.Range(0, Environment.ProcessorCount)    .Select(_ => Task.Run(async () =>    {        await foreach (var page in channel.Reader.ReadAllAsync())        {            RenderPage(page); // pure CPU work        }    })); await Task.WhenAll(consumers.Append(producer));

The bound of 8 is backpressure: when rendering is the bottleneck, loading waits, instead of loaded pages piling up. Option A has no equivalent.

Where it matters

Option A is simpler when the whole document fits in memory. Option B suits large or unbounded documents. With either, render each page to its own buffer and assemble them in order afterwards (see fan-out and fan-inFan-out and fan-in with Parallel.ForRender independent items on all cores, keep the results in order, then merge them one at a time.Opens in a new tab).