The Concurrency Model¶
Hardwood is multi-threaded at its core: a single reader, driven by a single consumer thread, keeps a pool of worker threads busy decoding ahead of where the consumer is reading.
Why concurrency is built in¶
The Parquet layout makes parallelism natural: a column chunk is a sequence of independently compressed pages (see How a Parquet File Is Laid Out). Decompressing and decoding a page is CPU-bound work that doesn't depend on its neighbors, so pages can be worked on concurrently. Hardwood exploits this without asking the caller to manage threads: you write an ordinary single-threaded read loop, and the parallelism happens underneath it.
The shared pool: Hardwood and HardwoodContext¶
Worker threads live in a pool owned by a context, not by individual readers.
ParquetFileReader.open(InputFile) creates and owns a context sized to the available processors,
and closing the reader shuts it down. An explicitly created HardwoodContext outlives any one
reader and is shared across all readers opened against it; the caller closes it. See
Read Multiple Files as One Dataset for creating and passing one.
Share one context across many reads. The pool is the expensive, reusable resource; readers are cheap and short-lived. This matters most when reading many files, as described below.
The assembly pipeline¶
For a single read, the reader runs an assembly pipeline ahead of the consumer. While your loop is processing the current batch (or row), pool threads are already decompressing and decoding the pages that feed the next batches, queuing the results. The consumer thread generally pulls ready-to-use decoded data instead of blocking on decode.
Frequent or long BatchWait JFR events
mean decode (or I/O) isn't keeping up with consumption.
Cross-file prefetching¶
When a reader spans multiple files (via Hardwood.openAll(...)), prefetching crosses file
boundaries: as the pages of file N run low, pages of file N+1 are already being fetched and
decoded. This is the main reason to open
a multi-file reader rather than looping over single-file readers yourself, and the reason the
shared pool matters, since all the files draw on the same workers. See
Read Multiple Files as One Dataset.
What this means for your code¶
- Drive one reader from one thread. A
RowReader/ColumnReader/ColumnReadersinstance is a stateful cursor meant to be advanced by a single consumer thread. The concurrency is internal: do not callnext()/nextBatch()on the same reader from multiple threads. The batch arrays aColumnReaderreturns may be handed to other threads while the consumer moves on (see Column-Oriented Reading). - For your own parallelism, read in parallel at the file or row-group grain. To use more
than one consumer thread, give each its own reader over a different file, or partition one file
across readers by byte range with
split-aware reading. Have them share a single
HardwoodContextso they draw worker threads from one pool rather than each spinning up its own. - Size the pool to the workload.
HardwoodContext.create(n)sets the number of threads that decompress and decode pages, which is the bulk of the CPU, sonis the main throughput and CPU dial. The default (available processors) suits a process whose main job is reading Parquet; lower it when Hardwood shares the machine with other heavy work. Two lighter stages, fetching page bytes on demand and assembling decoded values into batches, run on the JVM's virtual-thread carriers rather than this pool. Speculative fetches (the next file's footer, the next row group's fetch plans and first chunk, the chunk after the one being read) run on the commonForkJoinPool.nbounds neither. For strict CPU isolation on a shared machine, also cap the carriers with-Djdk.virtualThreadScheduler.parallelismand the common pool with-Djava.util.concurrent.ForkJoinPool.common.parallelismat a value of 2 or more. Below 2, each speculative fetch runs on a new thread of its own.
Further reading¶
- How a Parquet File Is Laid Out — why pages are independent units of work.
- Configuration — JFR events, plus SIMD and libdeflate acceleration of the decode itself.
- Read Multiple Files as One Dataset — the cross-file API and pool sharing in practice.
- Concurrent Column Consumer — a runnable example that fans each batch's per-value work across a thread pool, with a single-threaded baseline to show the gains.