Vix.cpp v2.8.5 is here Read the blog
Skip to content

Parallel Pipeline ​

parallel_pipeline runs several independent stages concurrently through a ThreadPool.

cpp
#include <vix/threadpool/all.hpp>

int main()
{
  vix::threadpool::ThreadPool pool(4);

  vix::threadpool::parallel_pipeline(
    pool,
    [](){
      load_config();
    },
    [](){
      warm_cache();
    },
    [](){
      prepare_metrics();
    }
  );

  return 0;
}

All stages are submitted before the function begins waiting for their results.

The call returns only after every submitted stage has reached a terminal asynchronous result.

Independent stages ​

The most important property of parallel_pipeline is that its stages are independent.

This:

cpp
vix::threadpool::parallel_pipeline(
  pool,
  first_stage,
  second_stage,
  third_stage
);

means:

text
first_stage  ──► ThreadPool
second_stage ──► ThreadPool
third_stage  ──► ThreadPool
       │
       └──── caller waits for all

It does not mean:

text
first_stage
    ↓
second_stage
    ↓
third_stage

There is no automatic data flow or dependency between stages.

Not a sequential data pipeline ​

The word pipeline here describes a group of independent stages launched together.

It does not provide automatic stage-to-stage value passing.

For example:

cpp
vix::threadpool::parallel_pipeline(
  pool,
  [](){
    return 42;
  },
  [](){
    consume_result();
  }
);

does not pass 42 from the first callable to the second callable.

Return values are discarded.

If one operation requires another operation's result, express that dependency explicitly.

Stage order is not guaranteed ​

Stages are submitted in argument order.

For:

cpp
vix::threadpool::parallel_pipeline(
  pool,
  stageA,
  stageB,
  stageC
);

submission occurs as:

text
stage A
stage B
stage C

but execution can occur in any order allowed by the scheduler.

For example:

text
stage B starts
stage A starts
stage C starts

or several stages can execute simultaneously.

Submission order must not be interpreted as dependency order.

Basic execution model ​

The direct API follows this path:

text
stages
  ↓
submit stage 1
submit stage 2
submit stage 3
  ↓
Future<void>
Future<void>
Future<void>
  ↓
consume every Future
  ↓
exception?
 ┌────┴────┐
yes        no
 │          │
rethrow    return

Every stage is submitted through ThreadPool::submit().

The stages therefore use the normal ThreadPool scheduling and worker model.

Stage signature ​

Stages are intended to be callable without arguments:

cpp
[](){
  perform_work();
}

Required state can be captured:

cpp
const int value = 42;

vix::threadpool::parallel_pipeline(
  pool,
  [value](){
    consume(value);
  }
);

The pipeline does not inject arguments into stage callables.

Return values are discarded ​

A stage can technically return a value:

cpp
vix::threadpool::parallel_pipeline(
  pool,
  [](){
    return 42;
  }
);

but the pipeline wrapper calls the stage only for its execution:

cpp
stage();

The return value is not stored.

The generated asynchronous result is:

cpp
vix::threadpool::Future<void>

Use ordinary ThreadPool::submit() or another result-producing abstraction when stage values must be collected.

No stages ​

Calling the direct algorithm with no stage is a no-op:

cpp
vix::threadpool::parallel_pipeline(pool);

The function returns immediately.

Conceptually:

text
number of stages = 0
        ↓
return

No task is submitted.

All stages are submitted before waiting ​

For several stages, the implementation first builds the complete Future collection.

Conceptually:

text
submit A
submit B
submit C
submit D
   ↓
only then
   ↓
get Future A
get Future B
get Future C
get Future D

The function does not:

text
submit A
wait A
submit B
wait B

This allows independent stages to overlap in execution.

ParallelPipelineOptions ​

Stage submission can be configured with:

cpp
vix::threadpool::ParallelPipelineOptions options;

The type currently contains one field:

cpp
vix::threadpool::TaskOptions task_options;

A default options object therefore uses default TaskOptions:

cpp
vix::threadpool::ParallelPipelineOptions options;

The same task_options value is used for every stage.

Pass explicit options ​

The overload with options places them immediately after the pool:

cpp
vix::threadpool::ParallelPipelineOptions options;

options.task_options.set_priority(
  vix::threadpool::TaskPriority::high
);

vix::threadpool::parallel_pipeline(
  pool,
  options,
  [](){
    perform_first_operation();
  },
  [](){
    perform_second_operation();
  }
);

Both stages are submitted with high priority.

TaskOptions are shared by configuration ​

Every generated stage submission receives:

cpp
options.task_options

Conceptually:

text
ParallelPipelineOptions
          │
          ▼
      TaskOptions
      ┌───┼───┐
      ▼   ▼   ▼
   stage stage stage

The same configuration can therefore apply:

text
priority
worker affinity
cancellation
deadline
timeout

to every stage.

Normal ThreadPool task semantics still apply.

Priority ​

Set a stage priority with:

cpp
vix::threadpool::ParallelPipelineOptions options;

options.task_options.set_priority(
  vix::threadpool::TaskPriority::high
);

Every stage receives the same priority.

Priority controls ordering inside each selected worker's local queue.

It does not establish ordering between pipeline stages.

For example:

text
stage A = high
stage B = high
stage C = high

does not mean A must execute before B.

See Priorities.

Worker affinity ​

Affinity can be applied to all stages:

cpp
options.task_options.set_affinity(
  vix::threadpool::WorkerId{2}
);

Because the same TaskOptions are used for every submission:

text
stage A ──┐
stage B ──┼──► Worker 2
stage C ──┘

One worker executes one task at a time.

Applying one affinity to every stage can therefore serialize the stages and remove the concurrency that parallel_pipeline normally provides.

Leave affinity unset unless same-worker placement is intentional.

Cancellation ​

A shared cancellation token can be attached:

cpp
vix::threadpool::CancellationSource source;

vix::threadpool::ParallelPipelineOptions options;

options.task_options.set_cancellation(
  source.token()
);

Every stage then observes the same cancellation state through the normal result-producing submission path.

Conceptually:

text
CancellationSource
       │
       ▼
shared cancellation state
    ┌────┼────┐
    ▼    ▼    ▼
 stage stage stage

A cancellation request can prevent stages that have not started from invoking their user callable.

Cancellation after a stage starts ​

Cancellation does not forcibly terminate a stage already running.

For long-running stage code, capture the token explicitly:

cpp
vix::threadpool::CancellationSource source;
auto token = source.token();

vix::threadpool::ParallelPipelineOptions options;
options.task_options.set_cancellation(token);

vix::threadpool::parallel_pipeline(
  pool,
  options,
  [token](){
    while (has_more_work())
    {
      if (token.stop_requested())
      {
        return;
      }

      process_next_item();
    }
  },
  [token](){
    while (has_more_background_work())
    {
      if (token.stop_requested())
      {
        return;
      }

      process_background_item();
    }
  }
);

The stage itself chooses where it is safe to stop.

See Cancellation.

Deadline ​

A common deadline can be applied to every stage:

cpp
options.task_options.set_deadline(
  vix::threadpool::Deadline::after(
    std::chrono::seconds{1}
  )
);

All stages receive the same absolute deadline value.

A stage that remains queued until after that deadline can be skipped by ThreadPool::submit().

Its Future then contains a timeout error.

The complete pipeline eventually throws after consuming all stage Futures.

See Deadlines.

Timeout ​

A timeout can also be configured:

cpp
options.task_options.set_timeout(
  vix::threadpool::Timeout::milliseconds(100)
);

Each stage receives its own low-level execution-duration observation.

Conceptually:

text
stage A → timeout observation
stage B → timeout observation
stage C → timeout observation

This is not one global timeout for the complete pipeline.

The current submit() timeout semantics can also produce a successful Future while the low-level task is recorded as timed out.

See Timeouts.

Stage callables are individually owned ​

The direct variadic parallel_pipeline overload decays and captures each stage separately into its generated task.

Conceptually:

text
stage A object
    ↓
move or copy into stage A task

stage B object
    ↓
move or copy into stage B task

The direct API does not create one shared callable object for all stages.

Each stage is its own callable.

This differs from chunk algorithms such as parallel_for, where one callable can be shared across generated chunks.

Move-only stages ​

Because each direct stage is captured from the forwarded callable into its task wrapper, the direct parallel_pipeline API can work with move-only stage objects when they otherwise satisfy the submission requirements.

For example, a lambda can own move-only state:

cpp
auto value = std::make_unique<int>(42);

vix::threadpool::parallel_pipeline(
  pool,
  [value = std::move(value)](){
    consume(*value);
  }
);

The stage is moved into the pipeline submission path.

The reusable Pipeline builder has different storage requirements because it stores stages as std::function<void()>.

Exception propagation ​

If a stage throws:

cpp
vix::threadpool::parallel_pipeline(
  pool,
  [](){
    perform_first_operation();
  },
  [](){
    throw std::runtime_error{"stage failed"};
  },
  [](){
    perform_third_operation();
  }
);

the exception is stored in that stage's Future.

The pipeline later rethrows an exception to the caller.

One failure does not stop Future consumption ​

The implementation catches exceptions from individual Future::get() calls.

For example:

text
Future A
   ↓
success

Future B
   ↓
throws
   ↓
remember exception

Future C
   ↓
get()

Future D
   ↓
get()

all Futures consumed
   ↓
rethrow remembered exception

The pipeline waits for all already submitted stages before propagating the remembered failure.

Other stages are not automatically cancelled ​

Suppose:

text
stage A → throws
stage B → running
stage C → queued
stage D → running

the failure of A does not automatically cancel B, C, and D.

They continue according to normal ThreadPool behavior.

The pipeline consumes all their Futures before returning or throwing.

Use an explicit shared cancellation state if stages should cooperate on stopping after an application-level failure.

First encountered exception ​

Stage Futures are stored in submission order.

They are consumed in that same order.

The retained exception is therefore the first one encountered while traversing those Futures.

It is not necessarily the first stage that failed in wall-clock time.

For example:

text
stage C fails first in time
stage A fails later

Future consumption:
A
B
C

the exception from stage A can be encountered first.

Treat the propagated exception as:

text
first exception encountered
during Future consumption

not as a temporal ordering of failures.

Rejected stage submission ​

Stages are submitted through:

cpp
pool.submit(...);

If the pool rejects one stage, its returned Future<void> contains a rejection result.

Later:

cpp
future.get();

throws std::system_error.

That error enters the same exception collection path:

text
stage rejected
      ↓
Future error
      ↓
get() throws
      ↓
remember exception
      ↓
consume remaining Futures
      ↓
rethrow

Queue limits and pool lifecycle therefore apply normally.

Bounded queues ​

A pipeline submits all stages before waiting.

With many stages and a small per-worker queue capacity:

cpp
vix::threadpool::ThreadPoolConfig config;
config.thread_count = 2;
config.max_queue_size = 1;

vix::threadpool::ThreadPool pool(config);

some stage submissions can be rejected under sufficient pressure.

parallel_pipeline does not provide its own admission queue.

Every stage uses the normal ThreadPool scheduler and local worker queues.

See Queue and Rejection Policies.

Existing pool remains running ​

When an existing pool is supplied:

cpp
vix::threadpool::ThreadPool pool(4);

vix::threadpool::parallel_pipeline(
  pool,
  first_stage,
  second_stage
);

the pool remains running after the pipeline returns.

It can immediately be reused:

cpp
auto future = pool.submit([](){
  return 42;
});

The pipeline never shuts down an externally supplied pool.

Temporary-pool overload ​

An explicit pool is optional:

cpp
vix::threadpool::parallel_pipeline(
  [](){
    perform_first_operation();
  },
  [](){
    perform_second_operation();
  }
);

This creates a default ThreadPool internally.

Conceptually:

text
parallel_pipeline(stages...)
          ↓
create default ThreadPool
          ↓
submit stages
          ↓
consume all Futures
          ↓
return or rethrow
          ↓
temporary pool destroyed

Temporary pool with options ​

Options can also be used without an explicit pool:

cpp
vix::threadpool::ParallelPipelineOptions options;

options.task_options.set_priority(
  vix::threadpool::TaskPriority::high
);

vix::threadpool::parallel_pipeline(
  options,
  [](){
    perform_first_operation();
  },
  [](){
    perform_second_operation();
  }
);

A temporary default ThreadPool is created and all stages receive the supplied task options.

Reuse an existing pool ​

For repeated concurrent stage groups:

cpp
vix::threadpool::ThreadPool pool(4);

vix::threadpool::parallel_pipeline(
  pool,
  first_a,
  first_b
);

vix::threadpool::parallel_pipeline(
  pool,
  second_a,
  second_b
);

reuses the same worker runtime.

Using temporary-pool overloads creates a new pool for each call.

Pipeline builder ​

For stages assembled progressively, the module provides:

cpp
vix::threadpool::Pipeline

For example:

cpp
vix::threadpool::Pipeline pipeline;

pipeline
  .add([](){
    load_config();
  })
  .add([](){
    warm_cache();
  })
  .add([](){
    prepare_metrics();
  });

pipeline.run(pool);

Pipeline stores the stages and can execute them later.

Pipeline stores std::function<void()> ​

The stage type is:

cpp
using Stage = std::function<void()>;

The builder therefore stores a vector of:

cpp
std::function<void()>

objects.

This is different from the direct variadic API, where each callable type is preserved while being forwarded into its task wrapper.

Builder stage requirements ​

Because Pipeline stores stages in std::function<void()>, a stage added to the builder must satisfy the storage requirements of std::function.

In normal C++20 use, this means the stored callable must be copy-constructible.

A move-only lambda such as one owning a std::unique_ptr does not fit the reusable builder's current std::function<void()> storage model.

Use direct parallel_pipeline() when preserving a move-only stage is required.

Add a stage ​

Use:

cpp
pipeline.add([](){
  perform_work();
});

add() returns:

cpp
Pipeline&

which allows chaining:

cpp
pipeline
  .add(first_stage)
  .add(second_stage)
  .add(third_stage);

Stages are stored in registration order.

Inspect stage count ​

Use:

cpp
const std::size_t count = pipeline.size();

For:

cpp
vix::threadpool::Pipeline pipeline;

pipeline.add(first_stage);
pipeline.add(second_stage);

the count is:

text
2

Check whether the Pipeline is empty ​

Use:

cpp
if (pipeline.empty())
{
  // No stages are registered.
}

A new builder is empty:

text
size()  0
empty() true

After one add():

text
size()  1
empty() false

Clear registered stages ​

Use:

cpp
pipeline.clear();

Afterward:

text
size()  0
empty() true

clear() removes the stored stages.

It does not affect tasks from an earlier run() that have already completed or are executing in another thread.

Ordinary run() itself waits for all tasks before returning.

Configure the builder ​

Construct a pipeline with options:

cpp
vix::threadpool::ParallelPipelineOptions options;

options.task_options.set_priority(
  vix::threadpool::TaskPriority::high
);

vix::threadpool::Pipeline pipeline(options);

Every stage executed by that builder uses those options.

Read builder options ​

Use:

cpp
const auto& options = pipeline.options();

The returned value is a const reference to the builder's stored:

cpp
vix::threadpool::ParallelPipelineOptions

Inspect the nested task options through that object.

Replace builder options ​

Use:

cpp
vix::threadpool::ParallelPipelineOptions options;

options.task_options.set_priority(
  vix::threadpool::TaskPriority::highest
);

pipeline.set_options(options);

Future calls to:

cpp
pipeline.run(...);

use the new configuration.

Existing stages remain registered.

set_options() does not modify stages ​

This:

cpp
pipeline.set_options(options);

changes only:

text
pipeline execution options

It does not:

text
add stages
remove stages
execute stages

The same stored stage set can therefore be run under new task options.

Run the builder ​

Execute stored stages with:

cpp
pipeline.run(pool);

The builder follows the same execution shape as the direct function:

text
registered stages
      ↓
submit each stage
      ↓
store Future<void>
      ↓
consume every Future
      ↓
rethrow first encountered exception if any

The call is synchronous at the boundary.

When run(pool) returns, its generated Futures have all been consumed.

Empty builder execution ​

Running an empty builder:

cpp
vix::threadpool::Pipeline pipeline;

pipeline.run(pool);

is a no-op.

No tasks are submitted.

No special empty check is required because the internal stage loop simply has no entries.

Builder stages remain registered after run() ​

run() does not clear the pipeline.

For example:

cpp
vix::threadpool::Pipeline pipeline;

pipeline.add([](){
  perform_work();
});

pipeline.run(pool);
pipeline.run(pool);

executes the registered stage twice, once during each call to run().

Conceptually:

text
stored stages
    ↓
run #1
    ↓
stages remain
    ↓
run #2

Call:

cpp
pipeline.clear();

when the stored stage set should be removed.

Builder stages are copied for each run ​

During Pipeline::run(), each stored std::function<void()> stage is copied into the submitted task wrapper:

text
stored Stage
     ↓
copy into task wrapper
     ↓
ThreadPool::submit()

The stored stage object therefore remains in the pipeline for future executions.

This is part of what makes Pipeline reusable.

Repeated execution and captured state ​

Because stages remain registered, captures also remain part of those stored callables.

For example:

cpp
int count = 0;

vix::threadpool::Pipeline pipeline;

pipeline.add([&count](){
  ++count;
});

pipeline.run(pool);
pipeline.run(pool);

the same captured reference is used during both runs.

The caller is responsible for ensuring captured references remain valid for every execution.

Builder temporary-pool execution ​

A Pipeline can execute without an explicit pool:

cpp
vix::threadpool::Pipeline pipeline;

pipeline
  .add(first_stage)
  .add(second_stage);

pipeline.run();

run() creates a default temporary ThreadPool and forwards to:

cpp
pipeline.run(pool);

The temporary pool is destroyed after all stage Futures have been consumed.

Builder exceptions ​

Pipeline::run() uses the same exception strategy as parallel_pipeline().

If multiple stages fail:

text
stage A → exception A
stage B → exception B
stage C → success

the builder still consumes every Future.

It then rethrows the first exception encountered in stage-submission order.

The pipeline's registered stages remain stored even after run() throws.

A later call to run() can execute them again.

Builder rejection ​

Because Pipeline::run() also uses:

cpp
pool.submit(...)

a rejected stage produces a rejected Future.

That error is captured during:

cpp
future.get();

and enters the same first-exception path.

A failed run() does not automatically remove the rejected stage from the reusable builder.

Direct API vs Pipeline builder ​

Use direct parallel_pipeline() when stages are known at the call site:

cpp
vix::threadpool::parallel_pipeline(
  pool,
  first_stage,
  second_stage,
  third_stage
);

Use Pipeline when stages are assembled incrementally or should be executed repeatedly:

cpp
vix::threadpool::Pipeline pipeline;

pipeline.add(first_stage);

if (condition)
{
  pipeline.add(optional_stage);
}

pipeline.add(last_stage);

pipeline.run(pool);

The conceptual distinction is:

text
parallel_pipeline()
  stage set belongs to one call


Pipeline
  stage set stored as reusable object

Pipeline is not a dependency graph ​

Neither API models dependencies such as:

text
A must finish before B
B must produce data for C
C can start after A and B

All registered stages are independent from the execution abstraction's perspective.

If dependencies exist, represent them explicitly with:

text
Future values
separate submit() operations
Scopes
application synchronization
another dependency-aware abstraction

Do not depend on pipeline stage registration order.

Pipeline is not streaming ​

The current pipeline does not process a stream through repeated transformations such as:

text
item
 ↓
stage 1
 ↓
stage 2
 ↓
stage 3

It is closer to:

text
independent operation 1
independent operation 2
independent operation 3
        ↓
execute concurrently
        ↓
join

This distinction is important when choosing the abstraction.

Shared mutable state ​

Independent stages can still access shared application state.

For example:

cpp
int value = 0;

vix::threadpool::parallel_pipeline(
  pool,
  [&value](){
    ++value;
  },
  [&value](){
    ++value;
  }
);

can contain a data race because both stages can execute concurrently.

Use appropriate synchronization:

cpp
std::atomic<int> value{0};

vix::threadpool::parallel_pipeline(
  pool,
  [&value](){
    value.fetch_add(1, std::memory_order_relaxed);
  },
  [&value](){
    value.fetch_add(1, std::memory_order_relaxed);
  }
);

The pipeline coordinates task execution, not application memory access.

Stage concurrency depends on the pool ​

A pipeline with four stages does not guarantee that four stages execute simultaneously.

For example:

cpp
vix::threadpool::ThreadPool pool(2);

with:

text
4 stages
2 workers

can execute approximately as:

text
Worker 1 → stage A
Worker 2 → stage B

then

Worker 1 → stage C
Worker 2 → stage D

Actual scheduling depends on queue state, task options, and other work already present in the pool.

parallel_pipeline provides concurrency opportunities, not a fixed degree of simultaneous execution.

Other pool work can interleave ​

Generated stages are ordinary ThreadPool tasks.

If the pool already contains unrelated tasks:

text
existing task
pipeline stage A
pipeline stage B
existing task
pipeline stage C

the scheduler and local queue priorities determine execution.

Pipeline stages do not receive an isolated worker group.

They participate in the same runtime as other submitted work.

Calling from a worker ​

parallel_pipeline waits synchronously for all stage Futures.

If a task running on a pool calls the pipeline using that same pool:

text
Worker
  ↓
outer task
  ↓
parallel_pipeline(same pool)
  ↓
submit stages
  ↓
wait for stages

the outer worker remains occupied while waiting.

The stages require other available workers to execute.

Nested pipeline deadlock ​

If every worker becomes blocked waiting for stages submitted back to the same pool:

text
Worker 1 → outer task waiting
Worker 2 → outer task waiting
Worker 3 → outer task waiting
Worker 4 → outer task waiting

pipeline stages
      ↓
queued

no worker remains available to run the pipeline stages.

Avoid saturating a pool with tasks that synchronously submit and wait for additional work on the same pool.

Convenience namespace ​

The direct explicit-pool API is also available as:

cpp
vix::threadpool::parallel::pipeline(
  pool,
  first_stage,
  second_stage,
  third_stage
);

This forwards to:

cpp
vix::threadpool::parallel_pipeline(
  pool,
  first_stage,
  second_stage,
  third_stage
);

Convenience namespace with options ​

Explicit options are also supported:

cpp
vix::threadpool::ParallelPipelineOptions options;

options.task_options.set_priority(
  vix::threadpool::TaskPriority::high
);

vix::threadpool::parallel::pipeline(
  pool,
  options,
  first_stage,
  second_stage
);

The current parallel::pipeline() convenience namespace provides explicit-pool forms.

Use top-level:

cpp
vix::threadpool::parallel_pipeline(...)

when a temporary-pool overload is required.

Complete example ​

cpp
#include <atomic>
#include <vix/threadpool/all.hpp>

int main()
{
  vix::threadpool::ThreadPool pool(4);

  std::atomic<int> completed{0};

  vix::threadpool::parallel_pipeline(
    pool,
    [&completed](){
      completed.fetch_add(1, std::memory_order_relaxed);
    },
    [&completed](){
      completed.fetch_add(1, std::memory_order_relaxed);
    },
    [&completed](){
      completed.fetch_add(1, std::memory_order_relaxed);
    },
    [&completed](){
      completed.fetch_add(1, std::memory_order_relaxed);
    }
  );

  return completed.load(std::memory_order_relaxed) == 4 ? 0 : 1;
}

The pipeline returns only after all four stage Futures have been consumed.

Builder example ​

cpp
#include <atomic>
#include <vix/threadpool/all.hpp>

int main()
{
  vix::threadpool::ThreadPool pool(4);
  std::atomic<int> completed{0};

  vix::threadpool::Pipeline pipeline;

  pipeline
    .add([&completed](){
      completed.fetch_add(1, std::memory_order_relaxed);
    })
    .add([&completed](){
      completed.fetch_add(1, std::memory_order_relaxed);
    })
    .add([&completed](){
      completed.fetch_add(1, std::memory_order_relaxed);
    });

  pipeline.run(pool);

  if (pipeline.size() != 3)
  {
    return 1;
  }

  return completed.load(std::memory_order_relaxed) == 3 ? 0 : 1;
}

run() does not remove the registered stages.

Direct execution model ​

The direct algorithm is:

text
parallel_pipeline(pool, options, stages...)
                   ↓
               no stages?
                ┌───┴───┐
               yes      no
                │        │
              return     ▼
                  submit stage 1
                  submit stage 2
                  submit stage N
                         ↓
                    Future<void>
                    Future<void>
                    Future<void>
                         ↓
                 consume all Futures
                         ↓
                exception captured?
                   ┌─────┴─────┐
                  yes          no
                   │            │
                rethrow       return

Builder execution model ​

Pipeline adds reusable storage:

text
Pipeline
  │
  ├── Stage 1
  ├── Stage 2
  ├── Stage N
  │
  └── ParallelPipelineOptions
             ↓
          run(pool)
             ↓
      copy stages into tasks
             ↓
        submit all stages
             ↓
       consume all Futures
             ↓
      return or rethrow
             ↓
    stages remain registered

The important properties are:

  • parallel_pipeline runs independent stages concurrently.
  • It is not a sequential data-processing pipeline.
  • There is no automatic value passing between stages.
  • Stage return values are discarded.
  • Stages are submitted in argument order, but execution order is not guaranteed.
  • All stages are submitted before Future consumption begins.
  • Each stage becomes an ordinary ThreadPool::submit() operation.
  • A call with no stages performs no work.
  • ParallelPipelineOptions currently contains shared TaskOptions.
  • The same task options are applied to every stage.
  • One shared affinity can route all stages to one worker and serialize them.
  • Cancellation, deadlines, timeouts, priorities, queue limits, and rejection use normal ThreadPool semantics.
  • A stage failure does not automatically cancel other stages.
  • Every submitted Future is consumed before the first encountered exception is rethrown.
  • A rejected stage submission propagates through the same Future exception path.
  • The top-level API provides temporary-pool overloads.
  • The direct variadic API can preserve move-only stage callables through forwarding and capture.
  • Pipeline provides reusable stage storage.
  • Pipeline::Stage is std::function<void()>, so the builder uses copyable callable storage.
  • Pipeline::add() returns Pipeline& for chaining.
  • Pipeline::run() does not clear registered stages.
  • The same builder can be executed repeatedly.
  • Pipeline::clear() explicitly removes all stored stages.
  • Builder options can be read with options() and replaced with set_options().
  • Pipeline::run() can use an existing pool or a temporary default pool.
  • Pipeline stages share the same ThreadPool with unrelated runtime work.
  • Shared mutable application state must still be synchronized.
  • Calling a blocking pipeline from every worker of the same saturated pool can exhaust the workers and deadlock nested work.
  • parallel::pipeline() provides convenience forwarding for explicit-pool forms.

Continue with Periodic Tasks for work that is resubmitted repeatedly over time.

Released under the MIT License.