Skip to content

Latest commit

 

History

History
1825 lines (1268 loc) · 30.3 KB

task-groups.md

File metadata and controls

1825 lines (1268 loc) · 30.3 KB

Task Groups

TaskGroup is a thread-safe coordination object for manually tracking a group of related tasks.

It records:

task IDs
pending task count
completed task count
failed task count
cancelled task count
timed-out task count
rejected task count
first reported exception
shared cancellation state

Unlike Scope, a TaskGroup does not submit work itself.

The application registers tasks and reports their final outcomes explicitly.

Basic model

The lifecycle is:

create TaskGroup
      ↓
register task IDs
      ↓
work executes elsewhere
      ↓
report each task outcome
      ↓
wait until pending == 0

For example:

#include <chrono>
#include <thread>
#include <vix/threadpool/all.hpp>

int main()
{
  vix::threadpool::TaskGroup group;

  group.add_task(vix::threadpool::TaskId{1});
  group.add_task(vix::threadpool::TaskId{2});

  std::thread first([&group](){
    std::this_thread::sleep_for(
        std::chrono::milliseconds{10}
    );

    group.finish_task(
        vix::threadpool::TaskStatus::completed,
        vix::threadpool::TaskResult::success
    );
  });

  std::thread second([&group](){
    std::this_thread::sleep_for(
        std::chrono::milliseconds{20}
    );

    group.finish_task(
        vix::threadpool::TaskStatus::completed,
        vix::threadpool::TaskResult::success
    );
  });

  group.close();
  group.wait();

  first.join();
  second.join();

  return group.completed_tasks() == 2 ? 0 : 1;
}

TaskGroup coordinates the accounting and waiting. The threads or ThreadPool tasks performing the actual work are managed separately.

A new group

Create an empty group with:

vix::threadpool::TaskGroup group;

Its initial state is:

empty             true
done              true
closed            false
cancelled         false
total_tasks       0
pending_tasks     0
completed_tasks   0
failed_tasks      0
cancelled_tasks   0
timed_out_tasks   0
rejected_tasks    0

A new group is considered done because there are no pending tasks.

Register a task

Use:

const bool added = group.add_task(
        vix::threadpool::TaskId{1}
);

A successful registration increments both:

total_tasks
pending_tasks

For example:

group.add_task(vix::threadpool::TaskId{1});
group.add_task(vix::threadpool::TaskId{2});
group.add_task(vix::threadpool::TaskId{3});

produces:

total_tasks   3
pending_tasks 3

and:

group.done();

returns false.

Invalid task IDs are rejected

invalid_task_id cannot be registered:

const bool added = group.add_task(
        vix::threadpool::invalid_task_id
);

The result is:

false

The group counters are not modified.

invalid_task_id has value zero.

Task IDs are tracking information

The group stores every registered ID:

const auto ids = group.task_ids();

For:

group.add_task(vix::threadpool::TaskId{1});
group.add_task(vix::threadpool::TaskId{2});

the returned vector contains:

1
2

in registration order.

task_ids() returns a copy of the stored vector.

Changing the returned vector does not modify the group.

Task IDs are not required to be unique

add_task() validates that an ID is non-zero, but it does not check whether the same ID was already registered.

For example:

group.add_task(vix::threadpool::TaskId{1});
group.add_task(vix::threadpool::TaskId{1});

registers two entries.

The resulting counters are:

total_tasks   2
pending_tasks 2

When task identity is important, the caller is responsible for registering unique IDs.

IDs obtained from:

pool.next_task_id();

provide the normal ThreadPool task-ID sequence.

TaskGroup does not submit tasks

This is the central difference from Scope.

TaskGroup has no:

spawn()
submit()
post()
handle()

operation.

This:

vix::threadpool::TaskGroup group;

group.add_task(vix::threadpool::TaskId{1});

only registers accounting state.

It does not create or execute a ThreadPool task.

Conceptually:

TaskGroup
    ↓
register task identity

execution system
    ↓
runs actual work

application integration
    ↓
finish_task()

The application connects those pieces.

Report task completion

Call finish_task() once the registered work reaches its final outcome.

For successful work:

group.finish_task(
        vix::threadpool::TaskStatus::completed,
        vix::threadpool::TaskResult::success
);

This changes:

pending_tasks   -1
completed_tasks +1

When the last pending task is finished:

group.done();

returns true.

Report a failure

Use:

group.finish_task(
        vix::threadpool::TaskStatus::failed,
        vix::threadpool::TaskResult::failure
);

This increments:

failed_tasks

and decrements the pending count.

A failure can also include the captured exception:

group.finish_task(
        vix::threadpool::TaskStatus::failed,
        vix::threadpool::TaskResult::failure,
        std::make_exception_ptr(
                std::runtime_error{"task failed"}
        )
);

The first reported exception is retained by the group.

Report cancellation

Use:

group.finish_task(
        vix::threadpool::TaskStatus::cancelled,
        vix::threadpool::TaskResult::cancelled
);

This increments:

cancelled_tasks

and decrements:

pending_tasks

Requesting group cancellation itself does not perform this accounting automatically.

The task integration must still report its final outcome.

Report timeout

Use:

group.finish_task(
        vix::threadpool::TaskStatus::timed_out,
        vix::threadpool::TaskResult::timeout
);

This increments:

timed_out_tasks

and decrements the pending count.

Report rejection

Use:

group.finish_task(
        vix::threadpool::TaskStatus::rejected,
        vix::threadpool::TaskResult::rejected
);

This increments:

rejected_tasks

and decrements the pending count.

This is useful when manually integrating the group with an execution system whose submission can fail.

Use final status and result values

finish_task() is designed to receive the final task status and result.

The normal pairs are:

Status Result
completed success
failed failure
cancelled cancelled
timed_out timeout
rejected rejected

Use consistent terminal pairs when reporting group outcomes.

For example:

group.finish_task(
        vix::threadpool::TaskStatus::completed,
        vix::threadpool::TaskResult::success
);

rather than mixing an unrelated status and result.

finish_task() does not identify the task

Notice that the API is:

group.finish_task(
        status,
        result
);

not:

finish_task(task_id, status, result)

TaskGroup therefore does not match completion reports against individual registered IDs.

Its IDs are retained for identification and inspection, while completion accounting is counter-based.

The caller must maintain the relationship between actual tasks and their completion reports.

Call finish_task() exactly once per registered task

Because completion is counter-based, the caller should report exactly one final outcome for every successful add_task().

The intended relationship is:

one add_task()
      ↓
one finish_task()

For example:

3 registered tasks
      ↓
3 completion reports
      ↓
pending_tasks == 0

If a registered task never calls finish_task(), wait() can remain blocked indefinitely.

Extra completion reports are not validated

finish_task() decrements pending_tasks only when the count is greater than zero.

However, it still updates the outcome counter even when no tasks remain pending.

Therefore an extra call such as:

group.finish_task(
        vix::threadpool::TaskStatus::completed,
        vix::threadpool::TaskResult::success
);

after all registered tasks are already finished can increase completed_tasks() beyond total_tasks().

TaskGroup does not validate completion reports against individual IDs.

Correct accounting depends on the caller maintaining the one-registration, one-completion rule.

Wait for all registered tasks

Use:

group.wait();

The operation blocks until:

pending_tasks == 0

Conceptually:

wait()
  ↓
pending == 0?
 ┌─────┴─────┐
yes          no
 │            │
return       block
              ↓
       finish_task()
              ↓
       condition variable
              ↓
        check again

finish_task() notifies waiting threads after updating the group state.

wait() does not close the group

This differs from Scope.

Calling:

group.wait();

does not prevent future task registration.

For example:

vix::threadpool::TaskGroup group;

group.wait();

const bool added = group.add_task(
        vix::threadpool::TaskId{1}
);

added can still be true because the group remains open.

This means a bare wait() is only a wait for the current pending count.

Close before waiting for a stable boundary

When no more tasks should enter the group, use:

group.close();
group.wait();

The sequence creates a stable completion boundary:

close()
   ↓
reject new registrations
   ↓
wait()
   ↓
pending reaches zero
   ↓
group remains complete

This is the normal pattern when the complete group membership is known.

Close the group

Call:

group.close();

to prevent future registrations.

Afterward:

group.closed();

returns:

true

and:

group.add_task(vix::threadpool::TaskId{4});

returns:

false

Existing registered tasks remain pending until their outcomes are reported.

close() does not wait

This:

group.close();

does not wait for registered tasks.

For example:

total_tasks   3
pending_tasks 2

can remain true after the group has been closed.

Use:

group.close();
group.wait();

when both membership finalization and completion are required.

close() does not cancel

Closing and cancellation are independent:

close()
   ↓
prevent new task registration


cancel()
   ↓
request cooperative cancellation

A group can be:

closed and not cancelled

or:

cancelled and still open

until the application explicitly changes the other state.

Check completion

Use:

if (group.done())
{
  // No registered task is pending.
}

done() is equivalent to:

pending_tasks == 0

A new empty group is therefore already done.

A group becomes not done after successful task registration:

group.add_task(vix::threadpool::TaskId{1});

and becomes done again after its pending count returns to zero.

done() does not mean closed

These are separate states.

A group can be:

done   true
closed false

For example, a new group has exactly this state.

It can also be:

done   false
closed true

when membership has been finalized but registered work is still outstanding.

The normal final state after:

group.close();
group.wait();

is:

done   true
closed true

empty() means no task was ever registered

Use:

group.empty();

to check whether:

total_tasks == 0

This differs from done().

For example:

group.add_task(vix::threadpool::TaskId{1});

group.finish_task(
        vix::threadpool::TaskStatus::completed,
        vix::threadpool::TaskResult::success
);

now gives:

empty() false
done()  true

The task has finished, but the group is not empty because one task was registered during its lifetime.

Counters are cumulative

TaskGroup does not remove completed registrations or reset its counters after waiting.

For example:

total_tasks       3
pending_tasks     0
completed_tasks   2
cancelled_tasks   1

remains available after:

group.wait();

This makes the group useful for inspecting aggregate outcomes after all work finishes.

There is no public reset operation.

Inspect total tasks

Use:

const auto total = group.total_tasks();

This is the number of successful add_task() registrations during the group's lifetime.

It does not decrease when tasks finish.

Inspect pending tasks

Use:

const auto pending = group.pending_tasks();

This is the number of registered tasks for which completion has not yet been accounted.

Conceptually:

pending_tasks
    ↓
successful registrations
minus
reported completions

When it reaches zero:

group.done();

returns true.

Inspect completed tasks

Use:

const auto completed = group.completed_tasks();

This counts outcomes reported with:

vix::threadpool::TaskStatus::completed

or a successful fallback result when a non-terminal status is supplied.

Normal code should report the terminal completed status directly.

Inspect failed tasks

Use:

const auto failed = group.failed_tasks();

This counts tasks reported as failed.

Check whether at least one failure occurred with:

if (group.has_failure())
{
  // At least one failed task was reported.
}

has_failure() only refers to the failed-task counter.

Cancellation, timeout, and rejection do not make has_failure() return true.

Inspect all non-success outcomes

Use:

if (group.has_error())
{
  // At least one non-success outcome was reported.
}

has_error() returns true when any of these counters is non-zero:

failed_tasks
cancelled_tasks
timed_out_tasks
rejected_tasks

Therefore:

has_failure()
   ↓
only failure

has_error()
   ↓
failure
cancellation
timeout
rejection

A fully successful group reports:

has_failure() false
has_error()   false

First exception

When a failed task reports an exception:

group.finish_task(
        vix::threadpool::TaskStatus::failed,
        vix::threadpool::TaskResult::failure,
        std::make_exception_ptr(
                std::runtime_error{"failure"}
        )
);

the first non-null exception is stored.

Inspect it with:

const auto exception = group.first_exception();

When no exception was captured:

nullptr

is returned.

Only the first exception is retained

Suppose several tasks report exceptions:

Task A → exception A
Task B → exception B
Task C → exception C

The group retains:

exception A

assuming Task A's failure report reached finish_task() first.

Later exceptions do not replace it.

This keeps one representative exception for wait_and_rethrow().

Failure does not require an exception

This is valid:

group.finish_task(
        vix::threadpool::TaskStatus::failed,
        vix::threadpool::TaskResult::failure
);

It increments:

failed_tasks

but does not create an exception.

The group can therefore have:

has_failure() == true
first_exception() == nullptr

wait_and_rethrow() only throws when an actual exception pointer was reported.

Wait and rethrow

Use:

group.wait_and_rethrow();

to wait until all registered work has been reported complete and then rethrow the first captured exception.

For example:

vix::threadpool::TaskGroup group;

group.add_task(vix::threadpool::TaskId{1});

group.finish_task(
        vix::threadpool::TaskStatus::failed,
        vix::threadpool::TaskResult::failure,
        std::make_exception_ptr(
                std::runtime_error{"task failed"}
        )
);

try
{
  group.wait_and_rethrow();
}
catch (const std::runtime_error&)
{
  // First captured exception.
}

The exception is rethrown only after:

pending_tasks == 0

wait_and_rethrow() does not close the group

Like wait(), it does not modify the group's open or closed state.

For a fixed membership boundary, use:

group.close();

try
{
  group.wait_and_rethrow();
}
catch (...)
{
  // Handle the reported task exception.
}

The group remains closed afterward because close() was called explicitly.

wait_and_rethrow() only rethrows exceptions

Cancellation, timeout, and rejection contribute to:

group.has_error();

but do not automatically cause:

group.wait_and_rethrow();

to throw.

For example:

group.finish_task(
        vix::threadpool::TaskStatus::timed_out,
        vix::threadpool::TaskResult::timeout
);

increments the timeout counter but stores no exception.

After waiting, inspect the counters or has_error() when these outcomes matter.

Shared cancellation

Every TaskGroup owns a CancellationSource.

Obtain its token with:

auto token = group.cancellation_token();

Request cancellation with:

group.cancel();

Afterward:

group.cancelled();

returns:

true

and every token connected to the group source observes the same request.

Cancellation is not automatic task integration

TaskGroup does not automatically attach its token to ThreadPool tasks.

This:

vix::threadpool::TaskGroup group;

group.cancel();

only changes the group's shared cancellation state.

Actual work must explicitly observe:

group.cancellation_token();

if it should react to group cancellation.

Conceptually:

TaskGroup
    ↓
CancellationSource
    ↓
CancellationToken

application integration
    ↓
actual task observes token

Cancellation does not finish pending tasks

Calling:

group.cancel();

does not change:

pending_tasks
completed_tasks
failed_tasks
cancelled_tasks
timed_out_tasks
rejected_tasks

For example:

before cancel:
pending_tasks = 3

after cancel:
pending_tasks = 3

Each registered task must still eventually be reported through finish_task().

Otherwise:

group.wait();

can remain blocked.

Use the cancellation token inside work

A manually coordinated task can observe the group token:

auto token = group.cancellation_token();

std::thread worker([&group, token](){
  if (token.stop_requested())
  {
    group.finish_task(
        vix::threadpool::TaskStatus::cancelled,
        vix::threadpool::TaskResult::cancelled
    );

    return;
  }

  perform_work();

  group.finish_task(
        vix::threadpool::TaskStatus::completed,
        vix::threadpool::TaskResult::success
  );
});

The application is responsible for reporting whichever final outcome actually occurred.

Cancellation is cooperative

Group cancellation follows the same model as other ThreadPool cancellation:

group.cancel()
      ↓
shared cancellation state becomes true
      ↓
linked code observes token
      ↓
linked code decides where to stop safely

It does not:

terminate threads
interrupt arbitrary C++ instructions
automatically remove queued tasks
automatically update TaskGroup counters

See Cancellation.

Access the cancellation source

The source itself is available through:

auto& source = group.cancellation_source();

For ordinary cancellation:

group.cancel();

is simpler.

Direct source access is useful when another API specifically requires a CancellationSource.

The source is owned by the group and should not outlive it by reference.

Be careful when resetting the cancellation source

CancellationSource::reset() creates a new cancellation state.

Existing tokens remain attached to the old state.

Therefore:

auto token = group.cancellation_token();

group.cancellation_source().reset();

group.cancel();

requests cancellation on the new state, not on the state still observed by token.

Avoid resetting the group cancellation source while registered work depends on existing tokens.

Thread safety

TaskGroupState protects group accounting with a mutex and uses a condition variable for waiting.

The public operations can therefore be called from multiple threads.

For example:

Thread A
  finish_task()
       │
       │
Thread B│
  finish_task()
       │
       ▼
   TaskGroup
       │
       ▼
Thread C
  wait()

Counter updates and state inspection are synchronized internally.

Waiting and registration can occur concurrently

add_task() and wait() use the same synchronized state, but wait() does not close registrations.

This creates an important semantic distinction.

Suppose:

pending_tasks = 0

A thread calling:

group.wait();

can return immediately.

Another thread can then successfully call:

group.add_task(...);

if the group has not been closed.

Therefore, when registration may happen concurrently, call:

group.close();
group.wait();

once membership is complete.

Closing establishes the registration boundary

close() and add_task() synchronize through the same mutex.

If add_task() obtains the lock first:

task registered
      ↓
close happens afterward

the new task belongs to the group.

If close() obtains the lock first:

closed = true
      ↓
later add_task()
      ↓
false

This provides a clear boundary for concurrent task registration.

Completion notifications

Every call to:

group.finish_task(...);

notifies the group's condition variable.

Waiting threads then reevaluate:

pending_tasks == 0

close() and cancel() also notify waiters, although neither operation by itself makes outstanding tasks complete.

The wait predicate remains based only on the pending-task count.

Destructor does not wait

This is an important difference from Scope.

TaskGroup has a default destructor:

~TaskGroup()

and destruction does not automatically call:

wait()
close()
cancel()

Therefore this is unsafe when another thread can still access the group:

{
  vix::threadpool::TaskGroup group;

  group.add_task(vix::threadpool::TaskId{1});

  // Work using &group continues elsewhere.
}

The caller must ensure all users of the group have finished before destroying it.

Scope and TaskGroup have different lifetime guarantees

Scope provides automatic structured waiting:

Scope destructor
      ↓
wait for tracked Futures

TaskGroup does not:

TaskGroup destructor
      ↓
destroy state immediately

Therefore:

Scope
  automatic task lifetime boundary

TaskGroup
  manual coordination and accounting

Use Scope when automatic ownership of spawned task completion is the main requirement.

Use TaskGroup when manual registration, aggregate counters, task IDs, and explicit completion reporting are required.

TaskGroup does not own task lifetime

A TaskGroup stores:

Task IDs
counters
exception pointer
cancellation source

It does not store:

Future objects
TaskHandle objects
Worker objects
ThreadPool

Therefore the group itself does not keep an asynchronous operation alive or wait on its Future.

The system integrating with TaskGroup remains responsible for the actual execution objects.

TaskGroup is not tied to ThreadPool

A group can coordinate work from any execution mechanism capable of reporting the required outcomes.

The existing API only requires:

register TaskId
report TaskStatus
report TaskResult
optionally report exception

For example, it can coordinate plain std::thread work, as shown in the basic example.

This is why the type does not require a ThreadPool constructor argument.

A ThreadPool integration must be explicit

When connecting a TaskGroup to ThreadPool work, the integration must handle both sides:

before execution
      ↓
add_task(id)

after every possible outcome
      ↓
finish_task(...)

That includes:

success
failure
cancellation
timeout
rejection

A missing completion path leaves the group's pending count non-zero.

TaskGroup does not automatically observe a Future or TaskHandle.

Do not attach the group token without completion accounting

For example, attaching:

options.set_cancellation(
        group.cancellation_token()
);

to a ThreadPool submission can cause the ThreadPool to skip a callable before that callable begins.

If the application's only finish_task() call is inside that callable, it will never run.

The result would be:

registered task
      ↓
cancelled before callable
      ↓
callable skipped
      ↓
finish_task() never called
      ↓
pending_tasks remains non-zero

When integrating TaskGroup with ThreadPool cancellation, ensure the outer integration observes the terminal asynchronous result and reports it to the group.

Aggregate outcome example

The module's task-group example coordinates three manually reported outcomes:

#include <chrono>
#include <iostream>
#include <thread>
#include <vix/threadpool/all.hpp>

int main()
{
  vix::threadpool::TaskGroup group;

  group.add_task(vix::threadpool::TaskId{1});
  group.add_task(vix::threadpool::TaskId{2});
  group.add_task(vix::threadpool::TaskId{3});

  std::thread first([&group](){
    std::this_thread::sleep_for(
        std::chrono::milliseconds{20}
    );

    group.finish_task(
        vix::threadpool::TaskStatus::completed,
        vix::threadpool::TaskResult::success
    );
  });

  std::thread second([&group](){
    std::this_thread::sleep_for(
        std::chrono::milliseconds{40}
    );

    group.finish_task(
        vix::threadpool::TaskStatus::completed,
        vix::threadpool::TaskResult::success
    );
  });

  std::thread third([&group](){
    std::this_thread::sleep_for(
        std::chrono::milliseconds{60}
    );

    group.finish_task(
        vix::threadpool::TaskStatus::cancelled,
        vix::threadpool::TaskResult::cancelled
    );
  });

  group.close();
  group.wait();

  first.join();
  second.join();
  third.join();

  std::cout << "Total tasks: "
            << group.total_tasks()
            << '\n';

  std::cout << "Completed: "
            << group.completed_tasks()
            << '\n';

  std::cout << "Cancelled: "
            << group.cancelled_tasks()
            << '\n';

  std::cout << "Has error: "
            << (group.has_error() ? "yes" : "no")
            << '\n';

  return 0;
}

Output:

Total tasks: 3
Completed: 2
Cancelled: 1
Has error: yes

The output demonstrates that the group keeps aggregate outcome information after all registered work has finished.

TaskGroupState

TaskGroupState is the underlying public state type used by TaskGroup.

It provides the same core operations:

add_task()
finish_task()
close()
cancel()
wait()
wait_and_rethrow()
cancellation_token()
cancellation_source()
done()
closed()
empty()
task counters
task_ids()
first_exception()

TaskGroup is a user-facing wrapper around one TaskGroupState.

Normal application code should prefer:

vix::threadpool::TaskGroup

unless it specifically needs to work with the lower-level state type.

No reset operation

A TaskGroup accumulates information for its entire lifetime.

After:

total_tasks       10
completed_tasks    8
failed_tasks       2
pending_tasks      0

there is no operation that resets these counters to zero.

Create another group for another independent accounting lifetime.

This also keeps task IDs and first-exception state tied to one logical group.

Not copyable or movable

TaskGroup disables:

copy construction
copy assignment
move construction
move assignment

Create the group directly in the lifetime where its coordination state belongs.

This also keeps references used by concurrent completion reporters stable.

Choosing between Scope and TaskGroup

Use Scope when you want:

submit related work
track Futures automatically
wait automatically at destruction
shared cancellation

Use TaskGroup when you want:

manual task registration
explicit task IDs
manual completion reporting
aggregate outcome counters
first-exception storage
shared cancellation state
explicit waiting

The distinction is:

Scope
  owns structured tracking of submitted Futures

TaskGroup
  owns coordination state and accounting

Neither abstraction replaces the other.

Recommended TaskGroup lifecycle

For a fixed set of manually coordinated work, the normal pattern is:

create group
     ↓
add_task() for each logical task
     ↓
start work
     ↓
close()
     ↓
each task calls finish_task() exactly once
     ↓
wait() or wait_and_rethrow()
     ↓
inspect counters
     ↓
destroy group after all users are finished

In code:

vix::threadpool::TaskGroup group;

group.add_task(vix::threadpool::TaskId{1});
group.add_task(vix::threadpool::TaskId{2});

start_work();

group.close();
group.wait();

if (group.has_error())
{
  inspect_group_outcomes();
}

The important properties are:

  • TaskGroup coordinates work but does not submit it.
  • Tasks are registered manually with add_task().
  • Zero is not a valid task ID.
  • Duplicate non-zero IDs are currently accepted.
  • Every successful registration should receive exactly one finish_task() report.
  • finish_task() does not identify or validate the task being finished.
  • Outcome counters are cumulative.
  • empty() means no task has ever been registered.
  • done() means no registered task is currently pending.
  • wait() waits for pending_tasks == 0.
  • wait() does not close the group.
  • wait_and_rethrow() also does not close the group.
  • Call close() before waiting when the registration boundary must be final.
  • close() prevents new registrations but does not wait or cancel.
  • cancel() requests shared cooperative cancellation but does not update completion counters.
  • The cancellation token is not automatically connected to ThreadPool tasks.
  • has_failure() reports only failed tasks.
  • has_error() also includes cancellation, timeout, and rejection.
  • Only the first reported non-null exception is retained.
  • wait_and_rethrow() throws only when an exception was actually reported.
  • The destructor does not wait.
  • TaskGroup does not own Futures, tasks, workers, or a ThreadPool.
  • The caller must guarantee that no concurrent code accesses the group after destruction.
  • TaskGroup is thread-safe for its own state and is neither copyable nor movable.

Continue with Synchronization for Latch and Barrier, or Parallel Algorithms for higher-level parallel work.