Tutorial 11: Where to go from here
On this page
What you have built
blockpack started as a sequential loop and ended as a task graph: a stream that reads ahead, a
fork task that starts the encoding of every block as soon as it is loaded, encode tasks on every
core, and a writer that appends in order while the disk writes behind it – with bounded memory, a
clean shutdown, and output that is byte for byte the same as the sequential reference.
The method
The building blocks change from program to program. The method does not:
- Write or keep a sequential reference whose output is correct by definition.
- Measure where the time goes. Split the largest stage first.
- Introduce one building block at a time, because of a problem you can see – a time, a memory size, an idle thread – not because it might help.
- Compare the output with the reference after every step, on all cores. A test on one core misses ordering mistakes.
- Bound everything: blocks in flight, memory, runnable tasks. Size the scheduler for the bound, not for the input.
- Split serial state into a parallel part and a short serial part; a lock does not make it parallel.
- Give the critical path priority, and fork the parallel work at Normal explicitly.
- Shut down the way it runs: stop the source, drain, then destroy.
- Use the ready-made stages once you know what they do.
The building blocks of this tutorial
| Building block | Chapter | What it is for |
|---|---|---|
| Scheduler, ConvertThreadToTask | 2 | Run tasks on all cores; let main wait like a task |
| WaitableTask | 2, 3 | A task that runs once and can be waited for |
| SchedulerInfo | 4 | Threads and task queue capacity |
| AsyncFile | 5 | File I/O that suspends the task, not the thread |
| Semaphore_s, TaskGroup_s | 6 | Counting free and ready slots; a reusable join |
| CriticalSection | 7 | Mutual exclusion – where the order does not matter |
| TaskPriority | 8 | Prefer the critical path |
| The shutdown contract | 9 | Stop, drain, destroy |
| StreamedFileReader, OrderedFileWriter | 10 | Ready-made read and write stages |
What the tutorial did not need
These building blocks did not solve a problem of blockpack, so it did not use them. Each has its
own guide:
- Events – AutoResetEvent and ManualResetEvent, for a task that waits for a signal: Events.
- Time – waits with a timeout, SleepTask, tasks started later: Timeouts, Timers.
- Sockets – a task per connection that waits without a thread: TCP sockets.
- Pipes and queues – a byte stream between two tasks, lock-free queues and stacks: Pipes, Queues and stacks.
- Yielding – long-running tasks that give way to more urgent work: Yielding.
- Your own primitives – Custom primitives.
Real-world task graphs
The same method, applied to complete encoders. Each case study shows step by step how a sequential reference became a task graph that writes byte-identical files:
- ctLame – an MP3 encoder in which every frame depends on the state of the frame before it.
- ctJpeg – a JPEG encoder built as a sequence of fork/join stages.
- ctOpus – an Opus encoder that computes future frames ahead on speculation.
- ctDeflate – a gzip compressor whose block boundaries depend on everything before them.
Their sources are in the ThinkMeta.ConcurrentTasks.Samples repository on GitHub, and the Task graphs guide collects the patterns they use.
Room for improvement
blockpack is a teaching program; a real encoder would go further:
- Measure the stages while they run. A profiler's threading view shows where tasks wait and where threads are idle – the next thing to split.
- Write the encoded blocks without a copy with
WriteDeferredand copy tasks, keeping each slot until the writer has copied it. - Size the ring by measurement instead of four slots per thread.
- Write the output with unbuffered I/O for very large files.