API reference

Tasks

task.hpp

namespace concore
namespace v1

Typedefs

using task_function = std::function<void()>

A function type that is compatible with a task.

This function takes no arguments and returns nothing. It represents generic work.

A concore task is essentially a wrapper over a task_function.

See

task

class task
#include <task.hpp>

A task. Core abstraction for representing an independent unit of work.

A task can be enqueued into an executor and executed at a later time. That is, this represents work that can be scheduled.

Tasks have move-only semantics, and disable copy semantics. Also, the library prefers to move tasks around instead of using shared references to the task. That means that, after construction and initialization, once passed to an executor, the task cannot be modified.

It is assumed that a task can only be executed once.

Ensuring correctness when working with tasks

Within the independent unit of work definition, the word independent is crucial. It is the one that guarantees thread-safety of the applications relying on tasks to represent concurrency.

A task needs to be independent, meaning that it must not be run in parallel with other tasks that touch the same data (and one of them is writing). In other words, it enforces no data races, and no corruptions (in this context data races have the negative effect and they represent undefined behavior).

Please note that this does not say that there can’t be two tasks that touch the same data (and one of them is writing). It says that if we have such case we need to ensure that these tasks are not running in parallel. In other words, one need to apply constraints on these tasks to ensure that they re not run in parallel.

If constraints are added on the tasks ensuring that there are no two conflicting tasks that run in parallel, then we can achieve concurrency without data races.

At the level of task object, there is no explicit way of adding constraints on the tasks. The constraints can be added on top of tasks. See chained_task and serializer.

task_function and task_group

A task is essentially a pair of a task_function an a task_group. The task_function part offers storage for the work associated with the task. The task_group part offers a way of controlling the task execution.

One or most tasks can belong to a task_group. To add a task to an existing task_group pass the task_group object to the constructor of the task object. By using a task_group the user can tell the system to cancel the execution of all the tasks that belong to the task_group. It can also implement logic that depends on the the task_group having no tasks attached to it.

See

task_function, task_group, chained_task, serializer

Public Functions

task() = default

Default constructor.

Brings the task into a valid state. The task has no action to be executed, and does not belong to any task group.

template<typename T>
task(T ftor)

Constructs a new task given a functor.

When the task will be executed, the given functor will be called. This typically happens on a different thread than this constructor is called.

Parameters
  • ftor: The functor to be called when executing task.

Template Parameters
  • T: The type of the functor. Must be compatible with task_function.

To be assumed that the functor will be called at a later time. All the resources needed by the functor must be valid at that time.

template<typename T>
task(T ftor, task_group grp)

Constructs a new task given a functor and a task group.

When the task will be executed, the given functor will be called. This typically happens on a different thread than this constructor is called.

Parameters
  • ftor: The functor to be called when executing task.

  • grp: The task group to place the task in the group.

Template Parameters
  • T: The type of the functor. Must be compatible with task_function.

To be assumed that the functor will be called at a later time. All the resources needed by the functor must be valid at that time.

Through the given group one can cancel the execution of the task, and check (indirectly) when the task is complete.

After a call to this constructor, the group becomes “active”. Calling task_group::is_active() will return true.

See

get_task_group()

template<typename T>
task &operator=(T ftor)

Assignment operator from a functor.

This can be used to change the task function inside the task.

Return

The result of the assignment

Parameters
  • ftor: The functor to be called when executing task.

Template Parameters
  • T: The type of the functor. Must be compatible with task_function.

~task()

Destructor.

If the task belongs to the group, the group will contain one less active task. If this was the last task registered in the group, after this call, calling task_group::is_active() will yield false.

task(task&&) = default

Move constructor.

task &operator=(task&&) = default

Move operator.

task(const task&) = delete

Copy constructor is DISABLED.

task &operator=(const task&) = delete

Copy assignment operator is DISABLED.

void swap(task &other)

Swap the content of the task with another task.

Parameters
  • other: The other task

operator bool() const noexcept

Bool conversion operator.

Indicates if a valid functor is set into the tasks, i.e., if there is anything to be executed.

void operator()()

Function call operator; performs the action stored in the task.

This is called by the execution engine whenever the task is ready to be executed. It will call the functor stored in the task.

The functor can throw, and the execution system is responsible for catching the exception and ensuring its properly propagated to the user.

This is typically called after some time has passed since task creation. The user must ensure that the functor stored in the task is safe to be executed at that point.

const task_group &get_task_group() const

Gets the task group.

This allows the users to consult the task group associated with the task and change it.

Return

The group associated with this task.

Private Members

task_function fun_

The function to be called.

This can be associated with a task through construction and by using the special assignment operator.

Please note that, as the tasks cannot be copied and shared, and as the task system prefers moving tasks, after the task is enqueued this is constant.

task_group task_group_

The group that this tasks belongs to.

This can be set by the constructor, or can be set by calling get_task_group(). As the library prefers passing tasks around by moving them, after the task was enqueued, the task group cannot be changed.

task_group.hpp

namespace concore
namespace v1
class task_group
#include <task_group.hpp>

Used to control a group of tasks (cancellation, waiting, exceptions).

Tasks can point to one task_group object. A task_group object can point to a parent task_group object, thus creating hierarchies of task_group objects.

task_group implements shared-copy semantics. If one makes a copy of a task_group object, the actual value of the task_group will be shared. For example, if we cancel one task_group, the second task_group is also canceled. concore takes advantage of this type of semantics and takes all task_group objects by copy, while the content is shared.

Scenario 1: cancellation User can tell a task_group object to cancel the tasks. After this, any tasks that use this task_group object will not be executed anymore. Tasks that are in progress at the moment of cancellation are not by default canceled. Instead, they can check from time to time whether the task is canceled.

If a parent task_group is canceled, all the tasks belonging to the children task_group objects are canceled.

Scenario 2: waiting for tasks A task_group object can be queried to check if all the tasks in a task_group are done executing. Actually, we are checking whether they are still active (the distinction is small, but can be important in some cases). Whenever all the tasks are done executing (they don’t reference the task_group anymore) then the task_group object can tell that. One can easily query this property by calling is_active()

Also, one can spawn a certain number of tasks, associating a task_group object with them and wait for all these tasks to be completed by waiting on the task_group object. This is an active wait: the thread tries to execute tasks while waiting (with the idea that it will try to speed up the completion of the tasks) the waiting algorithm can vary based on other factors.

Scenario 3: Exception handling One can set an exception handler to the task_group. If a task throws an exception, and the associated task_group has a handler set, then the handler will be called. This can be useful to keep track of exceptions thrown by tasks. For example, one might want to add logging for thrown exceptions.

See

task

Public Functions

task_group()

Default constructor.

Creates an empty, invalid task_group. No operations can be called on it. This is used to mark the absence of a real task_group.

See

create()

~task_group()

Destructor.

task_group(const task_group&) = default

Copy constructor.

Creates a shared-copy of this object. The new object and the old one will share the same implementation data.

task_group(task_group&&) = default

Move constructor; rarely used.

task_group &operator=(const task_group&) = default

Assignment operator.

Creates a shared-copy of this object. The new object and the old one will share the same implementation data.

Return

The result of the assignment

task_group &operator=(task_group&&) = default

Move assignment; rarely used.

operator bool() const

Checks if this is a valid task group object.

Returns true if this object was created by create() or if it’s a copy of an object created by calling create().

Such an object is valid, and operations can be made on it. Tasks will register into it and they can be influenced by the task_group object.

An object for which this returns false is considered invalid. It indicates the absence of a real task_group object.

See

create(), task_group()

void set_exception_handler(except_fun_t except_fun)

Set the function to be called whenever an exception is thrown by a task.

On execution, tasks can throw exceptions. If tasks have an associated

task_group, one can use this function to register an exception handler that will be called for exceptions.
Parameters
  • except_fun: The function to be called on exceptions

The given exception function will be called each time a new exception is thrown by a task belonging to this task_group object.

void cancel()

Cancels the execution tasks in the group.

All tasks from this task group scheduled for execution that are not yet started are canceled they won’t be executed anymore. If there are tasks of this group that are in execution, they can continue execution until the end. However, they have ways to check if the task group is canceled, so that they can stop prematurely.

Tasks that are added to the group after the group was canceled will be not executed.

To get rid of the cancellation, one can cal clear_cancel().

See

clear_cancel(), is_cancelled()

void clear_cancel()

Clears the cancel flag; new tasks can be executed again.

This reverts the effect of calling cancel(). Tasks belonging to this group can be executed once more after clear_clance() is called.

Note, once individual tasks were decided that are canceled and not executed, this clear_cancel() cannot revert that. Those tasks will be forever not-executed.

See

cancel(), is_cancelled()

bool is_cancelled() const

Checks if the tasks overseen by this object are canceled.

This will return

true after cancel() is called, and false if clear_cancel() is called. If this return true it means that tasks belonging to this group will not be executed.
Return

True if the task group is canceled, False otherwise.

cancel(), clear_cancel()

bool is_active() const

Checks whether there are active tasks in this group.

Creating one task into the task group will make the task group active, regardless of whether the task is executed or not. The group will become non-active whenever all the tasks created in the group are destroyed.

Return

True if active, False otherwise.

One main assumption of task_groups is that, if a task is created in a task_group, then it is somehow on the path to execution (i.e., enqueued in some sort of executor).

This can be used to check when all tasks from a group are completed.

If a group has sub-groups which have active tasks, this will return true.

See

task

Public Static Functions

task_group create(const task_group &parent = {})

Creates a task_group object, with an optional parent.

As opposed to a default constructor, this creates a valid

task_group object. Operations (canceling, waiting) can be performed on objects created by this function.
Return

The task group created.

Parameters
  • parent: The parent of the task_group object (optional)

The optional parent parameter allows one to create hierarchies of task_group objects. A hierarchy can be useful, for example, for canceling multiple groups of tasks all at once.

See

task_group()

task_group current_task_group()

Returns the task_group object for the current running task.

If there is no task running, this will return an empty (i.e., default-constructed) object. If there is a running task on this thread, it will return the

task_group object for the currently running task.
Return

The task group associated with the current running task

The intent of this function is to be called from within running tasks.

This uses thread-local-storage to store the task_group of the current running task.

See

is_current_task_cancelled(), task_group()

bool is_current_task_cancelled()

Determines if current task cancelled.

This should be called from within tasks to check if the

task_group associated with the current running task was cancelled.
Return

True if current task cancelled, False otherwise.

The intent of this function is to be called from within running tasks.

See

current_task_group()

task_group set_current_task_group(const task_group &grp)

Sets the task group for the current worker.

This is used by implementation of certain algorithms to speed up the use of task groups. Not intended to be heavily used. To be used with care.

Return

The previous set task group (if any).

Parameters
  • grp: The new group to be set for the current worker.

In general, after setting a task group, one may want to restore the old task group. This is why the function returns the previous task_group object.

Private Members

std::shared_ptr<detail::task_group_impl> impl_

Implementation detail of a task group object. Note that the implementation details can be shared between multiple task_group objects.

spawn.hpp

namespace concore
namespace v1

Functions

void spawn(task &&t, bool wake_workers = true)

Spawns a task, hopefully in the current working thread.

This is intended to be called from within a task. In this case, the task will be added to the list of tasks for the current worker thread. The tasks will be added in the front of the list, so it will be executed in front of others.

Parameters
  • t: The task to be spawned

  • wake_workers: True if we should wake other workers for this task

The add-to-front strategy aims as improving locality of execution. We assume that this task is closer to the current task than other tasks in the system.

If the current running task does not finish execution after spawning this new task, it’s advised for the wake_workers parameter to be set to true. If, on the other hand, the current task finishes execution after this, it may be best to not set wake_workers to false and thus try to wake other threads. Waking up other threads can be an efficiency loss that we don’t need if we know that this thread is finishing soon executing the current task.

Note that the given task ca take a task_group at construction. This way, the users can control the groups of the spawned tasks.

template<typename F>
void spawn(F &&ftor, bool wake_workers = true)

Spawn one task, given a functor to be executed.

This is similar to the spawn(task&&, bool) function, but it takes directly a functor instead of a task.

Parameters
  • ftor: The ftor to be executed

  • wake_workers: True if we should wake other workers for this task

Template Parameters
  • F: The type of the functor

If the current task has a group associated, the new task will inherit that group.

See

spawn(task&&, bool)

template<typename F>
void spawn(F &&ftor, task_group grp, bool wake_workers = true)

Spawn one task, given a functor to be executed.

This is similar to the spawn(task&&, bool) function, but it takes directly a functor and a task group instead of a task.

Parameters
  • ftor: The ftor to be executed

  • grp: The group in which the task should be executed.

  • wake_workers: True if we should wake other workers for this task

Template Parameters
  • F: The type of the functor

If the current task has a group associated, the new task will inherit that group.

See

spawn(task&&, bool)

void spawn(std::initializer_list<task_function> &&ftors, bool wake_workers = true)

Spawn multiple tasks, given the functors to be executed.

This is similar to the other two spawn() functions, but it takes a series of functions to be executed. Tasks will be created for all these functions and spawn accordingly.

Parameters
  • ftors: A list of functors to be executed

  • wake_workers: True if we should wake other workers for the last task

The wake_workers will control whether to wake threads for the last task or not. For the others tasks, it is assumed that we always want to wake other workers to attempt to get as many tasks as possible from the current worker task list.

If the current task has a task group associated, all the newly created tasks will inherit that group.

spawn(task&&, bool), spawn_and_wait()

void spawn(std::initializer_list<task_function> &&ftors, task_group grp, bool wake_workers = true)

Spawn multiple tasks, given the functors to be executed.

This is similar to the other two spawn() functions, but it takes a series of functions to be executed. Tasks will be created for all these functions and spawn accordingly.

Parameters
  • ftors: A list of functors to be executed

  • grp: The group in which the functors are to be executed

  • wake_workers: True if we should wake other workers for the last task

The wake_workers will control whether to wake threads for the last task or not. For the others tasks, it is assumed that we always want to wake other workers to attempt to get as many tasks as possible from the current worker task list.

spawn(task&&, bool), spawn_and_wait()

template<typename F>
void spawn_and_wait(F &&ftor)

Spawn a task and wait for it.

This function is similar to the spawn() functions, but, after spawning, also waits for the spawned task to complete. This wait is an active-wait, as it tries to execute other tasks. In principle, the current thread executes the spawn task.

Parameters
  • ftor: The functor of the tasks to be spawned

Template Parameters
  • F: The type of the functor.

This will create a new task group, inheriting from the task group of the currently executing task and add the new task in this new group. The waiting is done on this new group.

See

spawn()

void spawn_and_wait(std::initializer_list<task_function> &&ftors, bool wake_workers = true)

Spawn multiple tasks and wait for them to complete.

This is used when a task needs multiple things done in parallel.

Parameters
  • ftors: A list of functors to be executed

  • wake_workers: True if we should wake other workers for the last task

This function is similar to the spawn() functions, but, after spawning, also waits for the spawned tasks to complete. This wait is an active-wait, as it tries to execute other tasks. In principle, the current thread executes the last of the spawned tasks.

This will create a new task group, inheriting from the task group of the currently executing task and add the new tasks in this new group. The waiting is done on this new group.

void wait(task_group &grp)

Wait on all the tasks in the given group to finish executing.

The wait here is an active-wait. This will execute tasks from the task system in the hope that the tasks in the group are executed faster.

Parameters
  • grp: The task group to wait on

Using this inside active tasks is not going to block the worker thread and thus not degrade performance.

Warning

If one adds task in a group and never executes them, this function will block indefinitely.

See

spawn(), spawn_and_wait()

struct spawn_continuation_executor
#include <spawn.hpp>

Executor that spawns tasks instead of enqueueing them, but not waking other workers. Similar to calling spawn(task, false) on the task.

See

spawn(), spawn_executor, global_executor

Public Functions

template<typename F>
void execute(F &&f) const

Spawns the execution of the given functor.

This will spawn a task that will call the given functor.

Parameters
  • f: The functor object to be executed

void execute(task t)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

struct spawn_executor
#include <spawn.hpp>

Executor that spawns tasks instead of enqueueing them. Similar to calling spawn() on the task.

See

spawn(), spawn_continuation_executor, global_executor

Public Functions

template<typename F>
void execute(F &&f) const

Spawns the execution of the given functor.

This will spawn a task that will call the given functor.

Parameters
  • f: The functor object to be executed

void execute(task t)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

Friends

friend bool operator==(spawn_executor, spawn_executor)
friend bool operator!=(spawn_executor, spawn_executor)

Executors

global_executor.hpp

namespace concore
namespace v1
struct global_executor
#include <global_executor.hpp>

The default global executor type.

This is an executor that passes the tasks directly to concore’s task system. Whenever there is a core available, the task is executed.

Is is the default executor.

The executor takes as constructor parameter the priority of the task to be used when enqueueing the task.

Two executor objects are equivalent if their priorities match.

See

inline_executor, spawn_executor

Public Types

using priority = detail::task_priority

The priority of the task to be used.

Public Functions

global_executor(priority prio = prio_normal)

Tasks with lowest possible priority.

template<typename F>
void execute(F &&f) const
void execute(task t) const

Public Static Attributes

constexpr auto prio_critical = detail::task_priority::critical

The type of the priority.

constexpr auto prio_high = detail::task_priority::high

Critical-priority tasks.

constexpr auto prio_normal = detail::task_priority::normal

High-priority tasks.

constexpr auto prio_low = detail::task_priority::low

Tasks with normal priority.

constexpr auto prio_background = detail::task_priority::background

Tasks with low priority.

Private Members

priority prio_

Friends

friend bool operator==(global_executor l, global_executor r)
friend bool operator!=(global_executor l, global_executor r)

inline_executor.hpp

namespace concore
namespace v1
struct inline_executor
#include <inline_executor.hpp>

Executor type that executes the work inline.

Whenever

execute is called with a functor, the functor is directly called. The calling party will be blocked until the functor finishes execution.
Parameters
  • f: Functor to be executed

Two objects of this type will always compare equal.

Public Functions

template<typename F>
void execute(F &&f) const

Friends

friend bool operator==(inline_executor, inline_executor)
friend bool operator!=(inline_executor, inline_executor)

delegating_executor.hpp

namespace concore
namespace v1
struct delegating_executor
#include <delegating_executor.hpp>

Executor type that forwards the execution to a give functor.

All the functor objects passed to the execute() method will be delegated to a function that takes a task parameter. This function is passed to the constructor of the class.

Public Types

using fun_type = std::function<void(task)>

The type of the function to be used to delegate execution to.

Public Functions

delegating_executor(fun_type f)
void execute(task t) const
template<typename F>
void execute(F &&f) const

Private Members

fun_type fun_

The functor to which the calls are delegated.

Friends

friend bool operator==(delegating_executor l, delegating_executor r)
friend bool operator!=(delegating_executor l, delegating_executor r)

dispatch_executor.hpp

namespace concore
namespace v1
struct dispatch_executor
#include <dispatch_executor.hpp>

Executor that sends tasks to libdispatch.

This executors wraps the task execution from libdispatch.

This executor provides just basic support for executing tasks, but not other features like cancellation, waiting for tasks, etc.

The executor takes as constructor parameter the priority of the task to be used when enqueueing the task.

Two executor objects are equivalent if their priorities match.

See

global_executor

Public Types

enum priority

The priority of the task to be used.

Values:

enumerator prio_high
enumerator prio_normal

High-priority tasks.

enumerator prio_low

Tasks with normal priority.

Public Functions

dispatch_executor(priority prio = prio_normal)
template<typename F>
void execute(F &&f) const

Private Members

priority prio_

Friends

friend bool operator==(dispatch_executor l, dispatch_executor r)
friend bool operator!=(dispatch_executor l, dispatch_executor r)

tbb_executor.hpp

namespace concore
namespace v1
struct tbb_executor
#include <tbb_executor.hpp>

Executor that sends tasks to TBB.

This executors wraps the task execution from TBB.

This executor provides just basic support for executing tasks, but not other features like cancellation, waiting for tasks, etc.

The executor takes as constructor parameter the priority of the task to be used when enqueueing the task.

Two executor objects are equivalent if their priorities match.

See

global_executor

Public Types

enum priority

The priority of the task to be used.

Values:

enumerator prio_high
enumerator prio_normal

High-priority tasks.

enumerator prio_low

Tasks with normal priority.

Public Functions

tbb_executor(priority prio = prio_normal)
template<typename F>
void execute(F &&f) const

Private Members

priority prio_

Friends

friend bool operator==(tbb_executor l, tbb_executor r)
friend bool operator!=(tbb_executor l, tbb_executor r)

any_executor.hpp

namespace concore
namespace v1
class any_executor
#include <any_executor.hpp>

A polymorphic executor wrapper.

This provides a type erasure on an executor type. It can hold any type of executor object in it and wraps its execution.

If the any executor is is not initialized with a valid executor, then calling execute() on it is undefined behavior.

executor, inline_executor, global_executor

Public Functions

any_executor() noexcept = default

Constructs an invalid executor.

any_executor(std::nullptr_t) noexcept

Constructs an invalid executor.

any_executor(const any_executor &other) noexcept

Copy constructor.

any_executor(any_executor &&other) noexcept

Move constructor.

template<typename Executor>
any_executor(Executor e)

Constructor from a valid executor.

This will construct the object by wrapping the given executor. The given executor must be valid.

Parameters
  • e: The executor to be wrapped by this object

any_executor &operator=(const any_executor &other) noexcept
any_executor &operator=(any_executor &&other) noexcept
any_executor &operator=(std::nullptr_t) noexcept
template<typename Executor>
any_executor &operator=(Executor e)

Assignment operator from another executor.

This will implement assignment from another executor. The given executor must be valid.

Return

Reference to this object

Parameters
  • e: The executor to be wrapped in this object

~any_executor()
void swap(any_executor &other) noexcept

Swaps the content of this object with the content of the given object.

template<typename F>
void execute(F &&f) const

The main execution method of this executor.

This implements the

execute() CPO for this executor object, making it conform to the executor concept. It forwards the call to the underlying executor.
Parameters
  • f: Functor to be executed in the wrapped executor

See

executor

void execute(task t) const

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

operator bool() const noexcept

Checks if this executor is wrapping another executor.

const std::type_info &target_type() const noexcept

Returns the type_info for the wrapped executor type.

template<CONCORE_CONCEPT_OR_TYPENAME(executor) Executor> Executor * target () noexcept

Helper method to get the underlying executor, if its type is specified.

template<CONCORE_CONCEPT_OR_TYPENAME(executor) Executor> const Executor * target () const noexcept

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

Private Members

detail::executor_base *wrapper_ = {nullptr}

The wrapped executor object.

Friends

friend bool operator==(any_executor l, any_executor r)

Comparison operator.

friend bool operator==(any_executor l, std::nullptr_t)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

friend bool operator==(std::nullptr_t, any_executor r)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

friend bool operator!=(any_executor l, any_executor r)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

friend bool operator!=(any_executor l, std::nullptr_t r)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

friend bool operator!=(std::nullptr_t l, any_executor r)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

Serializers

serializer.hpp

namespace concore
namespace v1
class serializer
#include <serializer.hpp>

Executor type that allows only one task to be executed at a given time.

If the main purpose of other executors is to define where and when tasks will be executed, the purpose of this executor is to introduce constrains between the tasks enqueued into it.

Given N tasks to be executed, the serializer ensures that there are no two tasks executed in parallel. It serializes the executions of this task. If a tasks starts executing all other tasks enqueued into the serializer are put on hold. As soon as one task is completed a new task is scheduled for execution.

As this executor doesn’t know to schedule tasks for executor it relies on one or two given executors to do the scheduling. If a base_executor is given, this will be the one used to schedule for execution of tasks whenever a new task is enqueued and the pool on on-hold tasks is empty. E.g., whenever we enqueue the first time in the serializer. If this is not given, the global_executor will be used.

If a cont_executor is given, this will be used to enqueue tasks after another task is finished; i.e., enqueue the next task. If this is not given, the serializer will use the base_executor if given, or spawn_continuation_executor.

A serializer in a concurrent system based on tasks is similar to mutexes for traditional synchronization-based concurrent systems. However, using serializers will not block threads, and if the application has enough other tasks, throughput doesn’t decrease.

Guarantees:

  • no more than 1 task is executed at once.

  • the tasks are executed in the order in which they are enqueued.

See

any_executor, global_executor, spawn_continuation_executor, n_serializer, rw_serializer

Public Functions

serializer(any_executor base_executor = {}, any_executor cont_executor = {})

Constructor.

If

base_executor is not given, global_executor will be used. If cont_executor is not given, it will use base_executor if given, otherwise it will use spawn_continuation_executor for enqueueing continuations.
Parameters
  • base_executor: Executor to be used to enqueue new tasks

  • cont_executor: Executor that enqueues follow-up tasks

The first executor is used whenever new tasks are enqueued, and no task is in the wait list. The second executor is used whenever a task is completed and we need to continue with the enqueueing of another task. In this case, the default, spawn_continuation_executor tends to work better than global_executor, as the next task is picked up immediately by the current working thread, instead of going over the most general flow.

See

global_executor, spawn_continuation_executor

template<typename F>
void execute(F &&f) const

Executes the given functor as a task in the context of the serializer.

If there are no tasks in the serializer, the given functor will be enqueued as a task in the

base_executor given to the constructor (default is global_executor). If there are already other tasks in the serializer, the given functor/task will be placed in a waiting list. When all the previous tasks are executed, this task will also be enqueued for execution.
Parameters
  • f: The functor to be executed

void execute(task t) const

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

void set_exception_handler(except_fun_t except_fun)

Sets the exception handler for enqueueing tasks.

The exception handler set here will be called whenever an exception is thrown while enqueueing a follow-up task. It will not be called whenever the task itself throws an exception; that will be handled by the exception handler set in the group of the task.

Parameters
  • except_fun: The function to be called whenever an exception occurs.

Cannot be called in parallel with task enqueueing and execution.

See

task_group::set_exception_handler

Private Functions

void do_enqueue(task t) const

Enqueues a task for execution.

Private Members

std::shared_ptr<impl> impl_

The implementation object of this serializer. We need this to be shared pointer for lifetime issue, but also to be able to copy the serializer easily.

Friends

friend bool operator==(serializer l, serializer r)
friend bool operator!=(serializer l, serializer r)

n_serializer.hpp

namespace concore
namespace v1
class n_serializer : public std::enable_shared_from_this<n_serializer>
#include <n_serializer.hpp>

Executor type that allows max N tasks to be executed at a given time.

If the main purpose of other executors is to define where and when tasks will be executed, the purpose of this executor is to introduce constrains between the tasks enqueued into it.

Given M tasks to be executed, this serializer ensures that there are no more than N tasks executed in parallel. It serializes the executions of this task. After N tasks start executing all other tasks enqueued into the serializer are put on hold. As soon as one task is completed a new task is scheduled for execution.

As this executor doesn’t know to schedule tasks for executor it relies on one or two given executors to do the scheduling. If a base_executor is given, this will be the one used to schedule for execution of tasks whenever a new task is enqueued and the pool on on-hold tasks is empty. E.g., whenever we enqueue the first time in the serializer. If this is not given, the global_executor will be used.

If a cont_executor is given, this will be used to enqueue tasks after another task is finished; i.e., enqueue the next task. If this is not given, the serializer will use the base_executor if given, or spawn_continuation_executor.

An n_serializer in a concurrent system based on tasks is similar to semaphores for traditional synchronization-based concurrent systems. However, using n_serializer objects will not block threads, and if the application has enough other tasks, throughput doesn’t decrease.

Guarantees:

  • no more than N task is executed at once.

  • if N==1, behaves like the serializer class.

See

serializer, rw_serializer, any_executor, global_executor, spawn_continuation_executor

Public Functions

n_serializer(int N, any_executor base_executor = {}, any_executor cont_executor = {})

Constructor.

If

base_executor is not given, global_executor will be used. If cont_executor is not given, it will use base_executor if given, otherwise it will use spawn_continuation_executor for enqueueing continuations.
Parameters
  • N: The maximum number of tasks allowed to be run in parallel

  • base_executor: Executor to be used to enqueue new tasks

  • cont_executor: Executor that enqueues follow-up tasks

The first executor is used whenever new tasks are enqueued, and no task is in the wait list. The second executor is used whenever a task is completed and we need to continue with the enqueueing of another task. In this case, the default, spawn_continuation_executor tends to work better than global_executor, as the next task is picked up immediately by the current working thread, instead of going over the most general flow.

See

global_executor, spawn_continuation_executor

template<typename F>
void execute(F &&f) const

Executes the given functor in the context of the N serializer.

If there are no more than

N tasks in the serializer, this task will be enqueued in the base_executor given to the constructor (default is global_executor). If there are already enough other tasks in the serializer, the given task will be placed in a waiting list. When all the previous tasks are executed, this task will also be enqueued for execution.
Parameters
  • f: The task functor to be enqueued in the serializer

void execute(task t) const

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

void set_exception_handler(except_fun_t except_fun)

Sets the exception handler for enqueueing tasks.

The exception handler set here will be called whenever an exception is thrown while enqueueing a follow-up task. It will not be called whenever the task itself throws an exception; that will be handled by the exception handler set in the group of the task.

Parameters
  • except_fun: The function to be called whenever an exception occurs.

Cannot be called in parallel with task enqueueing and execution.

See

task_group::set_exception_handler

Private Functions

void do_enqueue(task t) const

Enqueues a task for execution.

Private Members

std::shared_ptr<impl> impl_

The implementation object of this n_serializer. We need this to be shared pointer for lifetime issue, but also to be able to copy the serializer easily.

Friends

friend bool operator==(n_serializer l, n_serializer r)
friend bool operator!=(n_serializer l, n_serializer r)

rw_serializer.hpp

namespace concore
namespace v1
class rw_serializer
#include <rw_serializer.hpp>

Similar to a serializer but allows two types of tasks: READ and WRITE tasks.

This class is not an executor. It binds together two executors: one for READ tasks and one for WRITE tasks. This class adds constraints between READ and WRITE threads.

The READ tasks can be run in parallel with other READ tasks, but not with WRITE tasks. The WRITE tasks cannot be run in parallel neither with READ nor with WRITE tasks.

This class provides two methods to access to the two executors: read() and write(). The read() executor should be used to enqueue READ tasks while the write() executor should be used to enqueue WRITE tasks.

This implementation slightly favors the WRITEs: if there are multiple pending WRITEs and multiple pending READs, this will execute all the WRITEs before executing the READs. The rationale is twofold:

  • it’s expected that the number of WRITEs is somehow smaller than the number of READs (otherwise a simple serializer would probably work too)

  • it’s expected that the READs would want to read the latest data published by the WRITEs, so they are aiming to get the latest WRITE

Guarantees:

  • no more than 1 WRITE task is executed at once

  • no READ task is executed in parallel with other WRITE task

  • the WRITE tasks are executed in the order of enqueueing

See

reader_type, writer_type, serializer, rw_serializer

Public Functions

rw_serializer(any_executor base_executor = {}, any_executor cont_executor = {})

Constructor.

If

base_executor is not given, global_executor will be used. If cont_executor is not given, it will use base_executor if given, otherwise it will use spawn_continuation_executor for enqueueing continuations.
Parameters
  • base_executor: Executor to be used to enqueue new tasks

  • cont_executor: Executor that enqueues follow-up tasks

The first executor is used whenever new tasks are enqueued, and no task is in the wait list. The second executor is used whenever a task is completed and we need to continue with the enqueueing of another task. In this case, the default, spawn_continuation_executor tends to work better than global_executor, as the next task is picked up immediately by the current working thread, instead of going over the most general flow.

See

global_executor, spawn_continuation_executor

reader_type reader() const

Returns an executor to enqueue READ tasks.

Return

The executor for READ types

writer_type writer() const

Returns an executor to enqueue WRITE tasks.

Return

The executor for WRITE types

void set_exception_handler(except_fun_t except_fun)

Sets the exception handler for enqueueing tasks.

The exception handler set here will be called whenever an exception is thrown while enqueueing a follow-up task. It will not be called whenever the task itself throws an exception; that will be handled by the exception handler set in the group of the task.

Parameters
  • except_fun: The function to be called whenever an exception occurs.

Cannot be called in parallel with task enqueueing and execution.

See

task_group::set_exception_handler

Private Members

std::shared_ptr<impl> impl_

Implementation detail shared by both reader and writer types.

class reader_type
#include <rw_serializer.hpp>

The type of the executor used for READ tasks.

Objects of this type will be created by rw_serializer to allow enqueueing READ tasks

Public Functions

reader_type(std::shared_ptr<impl> impl)

Constructor. Should only be called by rw_serializer.

template<typename F>
void execute(F &&f) const

Enqueue a functor as a write operation in the RW serializer.

Depending on the state of the parent

rw_serializer object this will enqueue the task immediately (if there are no WRITE tasks), or it will place it in a waiting list to be executed later. The tasks on the waiting lists will be enqueued once there are no more WRITE tasks.
Parameters
  • f: The READ functor to be enqueued

void execute(task t) const

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

Private Functions

void do_enqueue(task t) const

Private Members

std::shared_ptr<impl> impl_

Friends

friend bool operator==(reader_type l, reader_type r)
friend bool operator!=(reader_type l, reader_type r)
class writer_type
#include <rw_serializer.hpp>

The type of the executor used for WRITE tasks.

Objects of this type will be created by rw_serializer to allow enqueueing WRITE tasks

Public Functions

writer_type(std::shared_ptr<impl> impl)

Constructor. Should only be called by rw_serializer.

template<typename F>
void execute(F &&f) const

Enqueue a functor as a write operation in the RW serializer.

Depending on the state of the parent

rw_serializer object this will enqueue the task immediately (if there are no other tasks executing), or it will place it in a waiting list to be executed later. The tasks on the waiting lists will be enqueued, in order, one by one. No new READ tasks are executed while we have WRITE tasks in the waiting list.
Parameters
  • f: The WRITE functor to be enqueued

void execute(task t) const

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

void operator()(task t) const

Enqueue a functor as a write operation in the RW serializer.

Depending on the state of the parent

rw_serializer object this will enqueue the task immediately (if there are no other tasks executing), or it will place it in a waiting list to be executed later. The tasks on the waiting lists will be enqueued, in order, one by one. No new READ tasks are executed while we have WRITE tasks in the waiting list.
Parameters
  • f: The WRITE functor to be enqueued

Private Functions

void do_enqueue(task t) const

Private Members

std::shared_ptr<impl> impl_

Friends

friend bool operator==(writer_type l, writer_type r)
friend bool operator!=(writer_type l, writer_type r)

Other task-based features

task_graph.hpp

namespace concore
namespace v1

Functions

void add_dependency(chained_task prev, chained_task next)

Add a dependency between two tasks.

This creates a dependency between the given tasks. It means that

next will only be executed only after prev is completed.
Parameters
  • prev: The task dependent on

  • next: The task that depends on prev

See

chained_task, add_dependencies()

void add_dependencies(chained_task prev, std::initializer_list<chained_task> nexts)

Add a dependency from a task to a list of tasks.

This creates dependencies between

prev and all the tasks in nexts. It’s like calling add_dependency() multiple times.
Parameters
  • prev: The task dependent on

  • nexts: A set of tasks that all depend on prev

All the tasks in the nexts lists will not be started until prev is completed.

See

chained_task, add_dependency()

void add_dependencies(std::initializer_list<chained_task> prevs, chained_task next)

Add a dependency from list of tasks to a tasks.

This creates dependencies between all the tasks from

prevs to the next task. It’s like calling add_dependenc() multiple times.
Parameters
  • prevs: The list of tasks that next is dependent on

  • next: The task that depends on all the prevs tasks

The next tasks will not start until all the tasks from the prevs list are complete.

See

chained_task, add_dependency()

class chained_task
#include <task_graph.hpp>

A type of tasks that can be chained with other such tasks to create graphs of tasks.

This is a wrapper on top of a task, and cannot be directly interchanged with a task. This can directly enqueue the encapsulated task, and also, one can create a task on top of this one (as this defines the call operator, and it’s also a functor).

One can create multiple chained_task objects, then call add_dependency() oradd_dependencies() to create dependencies between such task objects. Thus, one can create graphs of tasks from chained_task objects.

The built graph must be acyclic. Cyclic graphs can lead to execution stalls.

After building the graph, the user should manually start the execution of the graph by enqueueing a chained_task that has no predecessors. After completion, this will try to enqueue follow-up tasks, and so on, until all the graph is completely executed.

A chained task will be executed only after all the predecessors have been executed. If a task has three predecessors it will be executed only when the last predecessor completes. Looking from the opposite direction, at the end of the task, the successors are checked; the number of active predecessors is decremented, and, if one drops to zero, that successor will be enqueued.

The chained_task can be configured with an executor this will be used when enqueueing successor tasks.

If a task throws an exception, the handler in the associated task_group is called (if set) and the execution of the graph will continue. Similarly, if a task from the graph is canceled, the execution of the graph will continue as if the task wasn’t supposed to do anything.

See

task, add_dependency(), add_dependencies(), task_group

Public Functions

chained_task() = default

Default constructor. Constructs an invalid chained_task. Such a task cannot be placed in a graph of tasks.

chained_task(task t, any_executor executor = {})

Constructor.

This will initialize a valid

chained_task. After this constructor, add_dependency() and add_dependencies() can be called to add predecessors and successors of this task.
Parameters
  • t: The task to be executed

  • executor: The executor to be used for the successor tasks (optional)

If this tasks tries to start executing successor tasks it will use the given executor.

If no executor is given, the spawn_executor will be used.

See

add_dependency(), add_dependencies(), task

template<typename F>
chained_task(F f, any_executor executor = {})

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

void operator()()

The call operator.

This will be called when executing the chained_task. It will execute the task received on constructor and then will check if it needs to start executing successors it will try to start executing the successors that don’t have any other active predecessors.

This will use the executor given at construction to start successor tasks.

void set_exception_handler(except_fun_t except_fun)

Sets the exception handler for enqueueing tasks.

The exception handler set here will be called whenever an exception is thrown while enqueueing a follow-up task. It will not be called whenever the task itself throws an exception; that will be handled by the exception handler set in the group of the task.

Parameters
  • except_fun: The function to be called whenever an exception occurs.

See

task_group::set_exception_handler

operator bool() const noexcept

Bool conversion operator.

Indicates if this is a valid chained task.

void clear_next()

Clear all the dependencies that go from this task.

This is useful for constructing and destroying task graphs manually.

Private Members

std::shared_ptr<detail::chained_task_impl> impl_

Friends

friend void add_dependency(chained_task, chained_task)

Add a dependency between two tasks.

This creates a dependency between the given tasks. It means that

next will only be executed only after prev is completed.
Parameters
  • prev: The task dependent on

  • next: The task that depends on prev

See

chained_task, add_dependencies()

friend void add_dependencies(chained_task, std::initializer_list<chained_task>)

Add a dependency from a task to a list of tasks.

This creates dependencies between

prev and all the tasks in nexts. It’s like calling add_dependency() multiple times.
Parameters
  • prev: The task dependent on

  • nexts: A set of tasks that all depend on prev

All the tasks in the nexts lists will not be started until prev is completed.

See

chained_task, add_dependency()

friend void add_dependencies(std::initializer_list<chained_task>, chained_task)

Add a dependency from list of tasks to a tasks.

This creates dependencies between all the tasks from

prevs to the next task. It’s like calling add_dependenc() multiple times.
Parameters
  • prevs: The list of tasks that next is dependent on

  • next: The task that depends on all the prevs tasks

The next tasks will not start until all the tasks from the prevs list are complete.

See

chained_task, add_dependency()

pipeline.hpp

namespace concore
namespace v1

Enums

enum stage_ordering

The possible ordering constraints for a stage in a pipeline.

Values:

enumerator in_order

All the items are processed in the order they were started, one at a time.

enumerator out_of_order

Items are processed one at a time, but not necessarily in a specific order.

enumerator concurrent

No constraints; all items can be processed concurrently.

Variables

constexpr auto pipeline_end = pipeline_end_t{}

Tag value to help end the expression of building a pipeline.

template<typename T>
class pipeline
#include <pipeline.hpp>

Implements a pipeline, used to add concurrency to processing of items that need several stages of processing with various ordering constraints.

A pipeline is a sequence of stages in a the processing of a items (lines). All items share the same stages. All the stages operate on the same type (line type).

Template Parameters
  • T: The type of items that flow through the pipeline.

The pipeline is built with the help of the pipeline_builder class.

Even if the stages need to be run in sequence, we might get concurrency out of this structure as we can execute multiple lines concurrently. However, most of the pipelines also add more constraints on the execution of lines.

The first constraint that one might add is the number of lines that can be processed concurrently. This is done by a parameter passed to the constructor.

The other way to constraint the pipeline is to add various ordering constraints for the stages. We might have multiple ordering constraints for stages:

  • in_order: at most one item can be processed in the stage, and the items are executed in the order in which they are added to the pipeline

  • out_of_order: at most one item can be processed in the stage, but the order of processing doesn’t matter

  • concurrent: no constraints are set; the items can run concurrently on the stage

The in_order type adds the more constraints to a stage; out_of_order is a bit more flexible, but still limits the concurrency a lot. The most relaxed mode is concurrent.

If a pipeline has only in_order stages, then the concurrency of the pipeline grows to the number of stages it has; but typically the concurrency is very limited. We gain concurrency if we can make some of the stages concurrent. A pipeline scales well with respect to concurrency if most of its processing is happening in concurrent stages.

If a stage processing throws an exception, then, for that particular line, the next stages will not be run. If some of the next stages are in_order then the next items will not be blocked by not executing this stage; i.e., the processing in the stage is just skipped.

Example of building a pipeline: auto my_pipeline = concore::pipeline_builder<int>() | concore::stage_ordering::concurrent | [&](int idx) { work1(idx); } | concore::stage_ordering::out_of_order | [&](int idx) { work2(idx); } | concore::pipeline_end; for ( int i=0; i<100; i++) my_pipeline.push(i);

See

pipeline_builder, stage_ordering, serializer, n_serializer

Public Functions

void push(T line_data)

Pushes a new item (line) through the pipeline.

This will start processing from the first stage and will iteratively pass through all the stages of the pipeline. The same line data is passed to the functors registered with each stage of the pipeline; i.e., all the stages of the pipeline work on the same line.

Parameters
  • line_data: The data associated with the line

Private Functions

pipeline(detail::pipeline_impl &&impl)

Private Members

detail::pipeline_impl impl_

Implementation details of the pipeline; with type erasure.

friend pipeline_builder< T >
template<typename T>
class pipeline_builder
#include <pipeline.hpp>

Front-end to create pipeline objects by adding stages, step by step.

This tries to extract the building of the pipeline stages from the

pipeline class.
Template Parameters
  • T: The type of the data corresponding to a line.

It just knows how to configure a pipeline and then to create an actual pipeline object.

After we get a pipeline object, this builder cannot be used anymore.

Example of building a pipeline: auto my_pipeline = concore::pipeline_builder<MyT>() | concore::stage_ordering::concurrent | [&](MyT data) { work1(data); } | concore::stage_ordering::out_of_order | [&](MyT data) { work2(data); } | concore::pipeline_end;

Public Functions

pipeline_builder(int max_concurrency = 0xffff)

Constructs a pipeline object.

Parameters
  • max_concurrency: The concurrency limit for the pipeline

pipeline_builder(int max_concurrency, task_group grp)

Constructs a pipeline object.

Parameters
  • max_concurrency: The concurrency limit for the pipeline

  • grp: The group in which tasks need to be executed

pipeline_builder(int max_concurrency, task_group grp, any_executor exe)

Constructs a pipeline object.

Parameters
  • max_concurrency: The concurrency limit for the pipeline

  • grp: The group in which tasks need to be executed

  • exe: The executor to be used by the pipeline

pipeline_builder(int max_concurrency, any_executor exe)

Constructs a pipeline object.

Parameters
  • max_concurrency: The concurrency limit for the pipeline

  • exe: The executor to be used by the pipeline

template<typename F>
pipeline_builder &add_stage(stage_ordering ord, F &&work)

Adds a stage to the pipeline.

This takes a functor of type

void (T) and an ordering and constructs a stage in the pipeline with them.
Parameters
  • ord: The ordering for the stage

  • work: The work to be done in this stage

Template Parameters
  • F: The type of the work

See

stage_ordering

operator pipeline<T>()

Creates the actual pipeline object, ready to process items.

After calling this, we can no longer own any pipeline data, and we cannot add stages any longer. The returned pipeline object is ready to process items with the stages defined by this class.

pipeline<T> build()

Creates the actual pipeline object, ready to process items.

After calling this, we can no longer own any pipeline data, and we cannot add stages any longer. The returned pipeline object is ready to process items with the stages defined by this class.

Return

Resulting pipeline object.

pipeline_builder &operator|(stage_ordering ord)

Pipe operator to specify the ordering for the next stages.

This allows easily constructing pipelines by using the ‘|’ operator.

Return

The same pipeline_builder object

Parameters
  • ord: The ordering to be applied to next stages

template<typename F>
pipeline_builder &operator|(F &&work)

Pipe operator to add new stages to the pipeline.

This adds a new stage to the pipeline, using the latest specified stage ordering. If no stage ordering is specified, before adding this stage, the

in_order is used.
Return

The same pipeline_builder object

Parameters
  • work: The work corresponding to the stage; needs to be of type void(T)

Template Parameters
  • F: The type of the functor

pipeline<T> operator|(pipeline_end_t)

Pipe operator to a tag that tells us that we are done building the pipeline.

This will actually finalize the building process and return the corresponding

pipeline object. After this is called, any other operations on the builder are illegal.
Return

The pipeline object built by this pipeline_builder object.

Parameters
  • <unnamed>: A tag value

Private Members

detail::pipeline_impl impl_

Implementation details of the pipeline; with type erasure.

stage_ordering next_ordering_ = {stage_ordering::in_order}

The next stage ordering to apply.

struct pipeline_end_t
#include <pipeline.hpp>

Tag type to help end the expression of building a pipeline.

finish_task.hpp

namespace concore
namespace v1
struct finish_event
#include <finish_task.hpp>

A finish event.

This can be passed to various tasks that would notify this whenever the task is complete. Depending on how the event is configured, after certain number of tasks are completed this triggers an event. This can be used to join the execution of multiple tasks that can run in parallel.

The notify_done() function must be called whenever the task is done.

This is created via finish_task and finish_wait.

Once a finish even is triggered, it cannot be reused anymore.

See

finish_task, finish_wait

Public Functions

void notify_done() const

Called by other tasks to indicate their completion.

When the right number of tasks have called this, then the event is trigger; that is, executing a task or unblocking some wait call.

Private Functions

finish_event(std::shared_ptr<detail::finish_event_impl> impl)

Users cannot construct this directly; it needs to be done through finish_task and finish_wait.

Private Members

std::shared_ptr<detail::finish_event_impl> impl_

Implementation details; shared between multiple objects of the same kind.

friend finish_task
friend finish_wait
struct finish_task
#include <finish_task.hpp>

Abstraction that allows executing a task whenever multiple other tasks complete.

This is created with a task (and an executor) and a count number. If the specific number of other tasks will complete, then the given task is executed (in the given executor).

With the help of this class one might implement a concurrent join: when a specific number of task are done, then a continuation is triggered.

This abstraction can also be used to implement simple continuation; it’s just like a join, but with only one task to wait on.

This will expose a finish_event object with the event() function. Other tasks will call finish_event::notify_done() whenever they are completed. When the right amount of tasks call it, then the task given at the constructor will be executed.

After constructing a finish_task object, and passing the corresponding finish_event to the right amount of other tasks, this object can be safely destructed. Its presence will not affect in any way the execution of the task.

Example usage: concore::finish_task done_task([]{ printf(“done.”); system_cleanup(); }, 3); // Spawn 3 tasks auto event = done_task.event(); concore::spawn([event]{ do_work1(); event.notify_done(); }); concore::spawn([event]{ do_work2(); event.notify_done(); }); concore::spawn([event]{ do_work3(); event.notify_done(); }); // When they complete, the first task is triggered

See

finish_event, finish_wait

Public Functions

finish_task(task &&t, any_executor e, int count = 1)
finish_task(task &&t, int count = 1)
template<typename F>
finish_task(F f, any_executor e, int count = 1)
template<typename F>
finish_task(F f, int count = 1)
finish_event event() const

Getter for the finish_event object that should be distributed to other tasks.

Private Members

finish_event event_

The event that triggers the execution of the task. The task to be executed is stored within the event itself.

struct finish_wait
#include <finish_task.hpp>

Abstraction that allows waiting on multiple tasks to complete.

This is similar to finish_task, but instead of executing a task, this allows the user to wait on all the tasks to complete. This wait, as expected, is a busy-way: other tasks are executed while waiting for the finish event.

Similar to finish_task, this can also be used to implement concurrent joins. Instead of spawning a task whenever the tasks are complete, this allows the current thread to wait on the tasks to be executed

This will expose a finish_event object with the event() function. Other tasks will call finish_event::notify_done() whenever they are completed. When the right amount of tasks call it, then the wait() method will be ‘unblocked’.

Example usage: concore::finish_wait done(3); auto event = done_task.event(); // Spawn 3 tasks concore::spawn([event]{ do_work1(); event.notify_done(); }); concore::spawn([event]{ do_work2(); event.notify_done(); }); concore::spawn([event]{ do_work3(); event.notify_done(); });

// Wait for all 3 tasks to complete done.wait();

See

finish_event, finish_task

Public Functions

finish_wait(int count = 1)
finish_event event() const

Getter for the finish_event object that should be distributed to other tasks.

void wait()

Wait for all the tasks to complete.

This will wait for the right number of calls to the finish_event::notify_done() function on the exposed event. Until that, this will attempt to get some work from the system and execute it. Whenever the right number of tasks are completed (i.e., the right amount of calls to finish_event::notify_done() are made), then this will be unblocked; it will return as soon as possible.

This can be called several times, but after the first time this is unblocked, the subsequent calls will exit immediately.

Private Members

task_group wait_grp_

The task group we are waiting on.

finish_event event_

The event used to wait for the termination of tasks.

Algorithms

conc_for.hpp

namespace concore
namespace v1

Functions

template<typename It, typename UnaryFunction>
void conc_for(It first, It last, const UnaryFunction &f, const task_group &grp, partition_hints hints)

A concurrent for algorithm.

If there are no dependencies between the iterations of a for loop, then those iterations can be run in parallel. This function attempts to parallelize these iterations. On a machine that has a very large number of cores, this can execute each iteration on a different core.

Parameters
  • first: Iterator pointing to the first element in a collection

  • last: Iterator pointing to the last element in a collection (1 past the end)

  • f: Functor to apply to each element of the collection

  • grp: Group in which to execute the tasks

  • hints: Hints that may be passed to the

  • work: The work to be applied to be executed for the elements

Template Parameters
  • It: The type of the iterator to use

  • UnaryFunction: The type of function to be applied for each element

  • WorkType: The type of a work object to be used

This ensure that the given work/functor is called exactly once for each element from the given sequence. But the call may happen on different threads.

The function does not return until all the iterations are executed. (It may execute other non-related tasks while waiting for the conc_for tasks to complete).

This generates internal tasks by spawning and waiting for those tasks to complete. If the user spawns other tasks during the execution of an iteration, those tasks would also be waited on. This can be a method of generating more work in the concurrent for loop.

One can cancel the execution of the tasks by passing a task_group in, and canceling that task_group.

One can also provide hints to the implementation to fine-tune the algorithms to better fit the data it operates on. Please note however that the implementation may completely ignore all the hints it was provided.

There are two forms of this function: one that uses a functor, and one that takes a work as parameter. The version with the ‘work’ given as argument may be faster in certain cases in which, between iterators, we can store temporary data.

The work structure given to the function must have the following structure: struct GenericWorkType { using iterator = my_iterator_type; void exec(my_iterator_type first, my_iterator_type last) { … } };

This work will be called for various chunks from the input. The ‘iterator’ type defined in the given work must support basic random-iterator operations, but without dereference. That is: difference, incrementing, and addition with an integer. The work objects must be copyable.

In the case that no work is given, the algorithm expects either input iterators, or integral types.

Warning

If the iterations are not completely independent, this results in undefined behavior.

See

partition_hints, partition_method

template<typename It, typename UnaryFunction>
void conc_for(It first, It last, const UnaryFunction &f, const task_group &grp)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

template<typename It, typename UnaryFunction>
void conc_for(It first, It last, const UnaryFunction &f, partition_hints hints)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

template<typename It, typename UnaryFunction>
void conc_for(It first, It last, const UnaryFunction &f)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

template<typename WorkType>
void conc_for(typename WorkType::iterator first, typename WorkType::iterator last, WorkType &work, const task_group &grp, partition_hints hints)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

template<typename WorkType>
void conc_for(typename WorkType::iterator first, typename WorkType::iterator last, WorkType &work, const task_group &grp)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

template<typename WorkType>
void conc_for(typename WorkType::iterator first, typename WorkType::iterator last, WorkType &work, partition_hints hints)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

template<typename WorkType>
void conc_for(typename WorkType::iterator first, typename WorkType::iterator last, WorkType &work)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

conc_reduce.hpp

namespace concore
namespace v1

Functions

template<typename It, typename Value, typename BinaryOp, typename ReductionOp>
Value conc_reduce(It first, It last, Value identity, const BinaryOp &op, const ReductionOp &reduction, task_group grp, partition_hints hints)

A concurrent for algorithm.

If there are no dependencies between the iterations of a for loop, then those iterations can be run in parallel. This function attempts to parallelize these iterations. On a machine that has a very large number of cores, this can execute each iteration on a different core.

Parameters
  • first: Iterator pointing to the first element in a collection

  • last: Iterator pointing to the last element in a collection (1 past the end)

  • f: Functor to apply to each element of the collection

  • grp: Group in which to execute the tasks

  • hints: Hints that may be passed to the

Template Parameters
  • It: The type of the iterator to use

  • UnaryFunction: The type of function to be applied for each element

This ensure that the given functor is called exactly once for each element from the given sequence. But the call may happen on different threads.

The function does not return until all the iterations are executed. (It may execute other non-related tasks while waiting for the conc_for tasks to complete).

This generates internal tasks by spawning and waiting for those tasks to complete. If the user spawns other tasks during the execution of an iteration, those tasks would also be waited on. This can be a method of generating more work in the concurrent for loop.

One can cancel the execution of the tasks by passing a task_group in, and canceling that task_group.

One can also provide hints to the implementation to fine-tune the algorithms to better fit the data it operates on. Please note however that the implementation may completely ignore all the hints it was provided.

Warning

If the iterations are not completely independent, this results in undefined behavior.

See

partition_hints, partition_method

template<typename It, typename Value, typename BinaryOp, typename ReductionOp>
Value conc_reduce(It first, It last, Value identity, const BinaryOp &op, const ReductionOp &reduction, task_group grp)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

template<typename It, typename Value, typename BinaryOp, typename ReductionOp>
Value conc_reduce(It first, It last, Value identity, const BinaryOp &op, const ReductionOp &reduction, partition_hints hints)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

template<typename It, typename Value, typename BinaryOp, typename ReductionOp>
Value conc_reduce(It first, It last, Value identity, const BinaryOp &op, const ReductionOp &reduction)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

template<typename WorkType>
void conc_reduce(typename WorkType::iterator first, typename WorkType::iterator last, WorkType &work, const task_group &grp, partition_hints hints)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

template<typename WorkType>
void conc_reduce(typename WorkType::iterator first, typename WorkType::iterator last, WorkType &work, const task_group &grp)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

template<typename WorkType>
void conc_reduce(typename WorkType::iterator first, typename WorkType::iterator last, WorkType &work, partition_hints hints)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

template<typename WorkType>
void conc_reduce(typename WorkType::iterator first, typename WorkType::iterator last, WorkType &work)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

conc_scan.hpp

namespace concore
namespace v1

Functions

template<typename It, typename It2, typename Value, typename BinaryOp>
Value conc_scan(It first, It last, It2 d_first, Value identity, const BinaryOp &op, task_group grp, partition_hints hints)

A concurrent scan algorithm.

This implements the prefix sum algorithm. Assuming the given operation is summation, this will write in the destination corresponding to each element, the sum of the previous elements, including itself. Similar to std::inclusive_sum.

Parameters
  • first: Iterator pointing to the first element in the collection

  • last: Iterator pointing to the last element in the collection (1 past the end)

  • d_first: Iterator pointing to the first element in the destination collection

  • identity: The identity element (i.e., 0)

  • op: The operation to be applied (i.e., summation)

  • grp: Group in which to execute the tasks

  • hints: Hints that may be passed to the algorithm

Template Parameters
  • It: The type of the iterator in the input collection

  • It2: The type of the output iterator

  • Value: The type of the values we are operating on

  • BinaryOp: The type of the binary operation (i.e., summation)

This will try to parallelize the prefix sum algorithm. It will try to create enough task to make all the sums in parallel. In the process of parallelizing, this will create twice as much work as the serial algorithm

One can also provide hints to the implementation to fine-tune the algorithms to better fit the data it operates on. Please note however that the implementation may completely ignore all the hints it was provided.

The operation needs to be able to be called in parallel.

Return

The result value after applying the operation to the input collection

template<typename It, typename It2, typename Value, typename BinaryOp>
Value conc_scan(It first, It last, It2 d_first, Value identity, const BinaryOp &op, task_group grp)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

template<typename It, typename It2, typename Value, typename BinaryOp>
Value conc_scan(It first, It last, It2 d_first, Value identity, const BinaryOp &op, partition_hints hints)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

template<typename It, typename It2, typename Value, typename BinaryOp>
Value conc_scan(It first, It last, It2 d_first, Value identity, const BinaryOp &op)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

conc_sort.hpp

namespace concore

Functions

template<typename It, typename Comp>
void conc_sort(It begin, It end, const Comp &comp, task_group grp)
template<typename It, typename Comp>
void conc_sort(It begin, It end, const Comp &comp)
template<typename It>
void conc_sort(It begin, It end)
template<typename It>
void conc_sort(It begin, It end, task_group grp)

partition_hints.hpp

namespace concore
namespace v1

Enums

enum partition_method

The method of dividing the work for concurrent algorithms on ranges.

Using this would provide a hint the conc_for or conc_reduce algorithms on how to partition the input data.

The implementation of the algorithms may choose not to follow the specified method. Typically the default method (auto_partition) works good enough, so most users don’t need to change this.

Values:

enumerator auto_partition

Automatically partitions the data, trying to maximize locality.

This method tries to create as many tasks as needed to fill the available workers, but tries not to split the work too much to reduce locality. If only one worker is free to do work, this method tries to put all the iterations in the task without splitting the work.

Whenever new workers can take tasks, this method tries to ensure that the furthest away elements are taken from the current processing.

This method tries as much as possible to keep all the available workers busy, hopefully to finish this faster. It works well if different iterations take different amounts of time (work is not balanced).

It uses spawning to divide the work. Can be influenced by the granularity level.

This is the default method for random-access iterators.

enumerator upfront_partition

Partitions the data upfront.

Instead of partitioning the data on the fly lie the auto_partiion method, this will partition the data upfront, creating as many tasks as needed to cover all the workers. This can minimize the task management, but it doesn’t necessarily to ensure that all the workers have tasks to do, especially in unbalanced workloads.

Locality is preserved when splitting upfront.

This method only works for random-access iterators.

enumerator iterative_partition

Partitions the iterations as it advances through them.

This partition tries to create a task for each iteration (or, if granularity is > 1, for a number of iterations), and the tasks are created as the algorithm progresses. Locality is not preserved, as nearby elements typically end up on different threads. This method tries to always have tasks to be executed. When a task finished, a new task is spawned.

This method works for forward iterators.

This is the default method for non-random-access iterators.

enumerator naive_partition

Naive partition.

This creates a task for each iteration (or, depending of granularity, on each group of iterations). If there are too many elements in the given range, this can spawn too many tasks.

Does not preserve locality, but is ensures that the tasks are filling up the worker threads in the best possible way.

struct partition_hints
#include <partition_hints.hpp>

Hints to alter the behavior of a conc_for or conc_reduce algorithms.

The hints in this structure can influence the behavior of the conc_for and conc_reduce algorithms, but the algorithms can decide to ignore these hints.

In general, the algorithms performs well on a large variety of cases, when the functions being executed per element are not extremely fast. Therefore, manually giving hints to it is not usually needed. If the operations are really fast, the user might want to play with the granularity to ensure that the work unit is sufficiently large.

See

partition_method

Public Members

partition_method method_ = partition_method::auto_partition

The method of partitioning the input range.

int granularity_ = {-1}

The granularity of the algorithm.

When choosing how many iterations to handle in one task, this parameter can instruct the algorithm to not place less than the value here. This can be used when the iterations are really small, and the task management overhead can become significant.

Does not apply to the partition_method::upfront_partition method

int tasks_per_worker_ = {-1}

The (maximum) number of tasks to create per worker

Whenever this is set, we ensure that we don’t break the work into too many tasks. It has a similar effect to setting the granularity.

If, for example, this is set to 10, and we have 8 workers, then the upfront partition will create maximum 80 tasks. The auto partition will not create more than 80 tasks.

C++23 executors

execution.hpp

namespace concore
namespace v1

Functions

template<typename Receiver, typename ...Vs>
void set_value(Receiver &&r, Vs&&... vs)

Customization point object that can be used to set values to receivers.

This is called by a sender whenever the sender has finished work and produces some values. This can be called even if the sender doesn’t have any values to send to the receiver.

Parameters
  • r: The receiver object that is signaled about sender’s success

  • vs...: The values sent by the sender

The Receiver type should model concepts receiver and receiver_of<Vs...>.

See

set_done(), set_error()

template<typename Receiver>
void set_done(Receiver &&r)

Customization point object that can be used to signal stop to receivers.

This is called by a sender whenever the sender is stopped, and the execution of the task cannot continue. When this is called, set_value() is not called anymore.

Parameters
  • r: The receiver object that is signaled about sender’s stop signal

The Receiver type should model concept receiver.

See

set_value(), set_error()

template<typename Receiver, typename Err>
void set_error(Receiver &&r, Err &&e)

Customization point object that can be used to notify receivers of errors.

This is called by a sender whenever the sender has an error to report to the sender. Sending an error means that the sender is done processing; it will not call set_value() and set_done().

Parameters
  • r: The receiver object that is signaled about sender’s error

  • e: The error to be to the receiver

The Receiver type should model concept receiver<E>.

See

set_value(), set_done()

template<typename Executor, typename Ftor>
void execute(Executor &&e, Ftor &&f)

Customization point object that can be used to execute work on executors.

This will tell the executor object to invoke the given functor, according to the rules defined in the executor.

Parameters
  • e: The executor object we are using for our execution

  • f: The functor to be invoked

The Executor type should model concept executor_of<Ftor>.

template<typename Sender, typename Receiver>
auto connect(Sender &&s, Receiver &&r)

Connect a sender with a receiver, returning an async operation object.

The type of the

rcv parameter must model the receiver concept.
Parameters
  • snd: The sender object, that triggers the work

  • rcv: The receiver object that receives the results of the work

Usage example:

auto op = connect(snd, rcv);
// later
start(op);

template<typename Oper>
void start(Oper &&o)

Customization point object that can be used to start asynchronous operations.

This is called whenever one needs to start an asynchronous operation.

Parameters
  • o: The operation that should be started

The Oper type should model concept operation_state.

template<typename Sender, typename Receiver>
void submit(Sender &&s, Receiver &&r)

Submit work from a sender, by combining it with a receiver.

The

sender_to<Sender, Receiver> concept must hold.
Parameters
  • snd: The sender object, that triggers the work

  • rcv: The receiver object that receives the results of the work

If there is no submit customization point defined for the given Sender object (taking a Receiver object), then this will fall back to calling connect().

Usage example:

submit(snd, rcv);

See

connect()

template<typename Scheduler>
auto schedule(Scheduler &&s)

Transforms a scheduler (an execution context) into a single-shot sender.

Usage example:

sender auto snd = schedule(sched);
Parameters
  • sched: The scheduler object

template<typename Executor, typename Ftor, typename Num>
void bulk_execute(Executor &&e, Ftor &&f, Num n)

Customization point object that can be used to bulk_execute work on executors.

This will tell the executor object to invoke the given functor, according to the rules defined in the executor.

Parameters
  • e: The executor object we are using for our execution

  • f: The functor to be invoked

  • n: The number of times we have to invoke the functor

Variables

template<typename E> concept executor

Concept that defines an executor.

An executor object is an object that can “execute” work. Given a functor compatible with

invocable_archetype, the executor will be able to execute that function, in some specified manner.
Template Parameters
  • E: The type that we want to model the concept

Properties that a type needs to have in order for it to be an executor:

  • it’s copy-constructible

  • the copy constructor is nothrow

  • it’s equality-comparable

  • one can call ‘execute(obj, invocable_archetype{})’, where ‘obj’ is an object of the type

To be able to call execute on an executor, the executor type must have one the following:

  • an inner method ‘execute’ that takes a functor

  • an associated ‘execute’ free function that takes the executor and a functor

  • an customization point tag_invoke(execute_t, Ex, Fn)

See

executor_of, execute_t, execute

template<typename E, typename F> concept executor_of

Defines an executor that can execute a given functor type.

This is similar to executor, but instead of being capable of executing ‘void()’ functors, this can execute functors of the given type ‘F’

Template Parameters
  • E: The type that we want to model the concept

  • F: The type functor that can be called by the executor

See

executor

template<typename T, typename E = std::exception_ptr> concept receiver

Concept that defines a bare-bone receiver.

A receiver represents the continuation of an asynchronous operation. An asynchronous operation may complete with a (possibly empty) set of values, an error, or it may be canceled. A receiver has three principal operations corresponding to the three ways an asynchronous operation may complete:

set_value, set_error, and set_done. These are collectively known as a receiver’s completion-signal operations.
Template Parameters
  • T: The type being checked to see if it’s a bare-bone receiver

  • E: The type of errors that the receiver accepts; default std::exception_ptr

The following constraints must hold with respect to receiver’s completion-signal operations:

  • None of a receiver’s completion-signal operations shall be invoked before concore::start has been called on the operation state object that was returned by concore::connect to connect that receiver to a sender.

  • Once concore::start has been called on the operation state object, exactly one of the receiver’s completion-signal operations shall complete non-exceptionally before the receiver is destroyed.

  • If concore::set_value exits with an exception, it is still valid to call concore::set_error or concore::set_done on the receiver.

A bare-bone receiver is a receiver that only checks for the following CPOs:

  • set_done()

  • set_error(E)

The set_value() CPO is ignored in a bare-bone receiver, as a receiver may have many ways to be notified about the success of a sender.

In addition to these, the type should be move constructible and copy constructible.

See

receiver_of, set_done(), set_error()

template<typename T, typename E = std::exception_ptr, typename... Vs> concept receiver_of

Concept that defines a receiver of a particular kind.

A receiver represents the continuation of an asynchronous operation. An asynchronous operation may complete with a (possibly empty) set of values, an error, or it may be canceled. A receiver has three principal operations corresponding to the three ways an asynchronous operation may complete:

set_value, set_error, and set_done. These are collectively known as a receiver’s completion-signal operations.
Template Parameters
  • T: The type being checked to see if it’s a bare-bone receiver

  • Vs...: The types of the values accepted by the receiver

  • E: The type of errors that the receiver accepts; default std::exception_ptr

The following constraints must hold with respect to receiver’s completion-signal operations:

  • None of a receiver’s completion-signal operations shall be invoked before concore::start has been called on the operation state object that was returned by concore::connect to connect that receiver to a sender.

  • Once concore::start has been called on the operation state object, exactly one of the receiver’s completion-signal operations shall complete non-exceptionally before the receiver is destroyed.

  • If concore::set_value exits with an exception, it is still valid to call concore::set_error or concore::set_done on the receiver.

This concept checks that all three CPOs are defined (as opposed to receiver who only checks set_done and set_error).

This is an extension of the receiver concept, but also requiring the set_value() CPO to be present, for a given set of value types.

See

receiver, set_value(), set_done(), set_error()

template<typename S> concept sender

Concept that defines a sender.

A sender represents an asynchronous operation not yet scheduled for execution. A sender’s responsibility is to fulfill the receiver contract to a connected receiver by delivering a completion signal.

Template Parameters
  • S: The type that is being checked to see if it’s a sender

A sender, once the asynchronous operation is started must successfully call exactly one of these on the associated receiver:

  • set_value() in the case of success

  • set_done() if the operation was canceled

  • set_error() if an exception occurred during the operation, or while calling set_value()

The sender starts working when submit(S, R) or start(connect(S, R)) is called passing the sender object in. The sender should not execute any other work after calling one of the three completion signals operations. The sender should not finish its work without calling one of these/

A sender typically has a connect() method to connect to a receiver, but this is not mandatory. A CPO can be provided instead for the connect() method.

A sender typically exposes the type of the values it sets, and the type of errors it can generate, but this is not mandatory.

See

receiver, receiver_of, typed_sender, sender_to

template<typename S> concept typed_sender

Concept that defines a typed sender.

This is just like the sender concept, but it requires the type information; that is the types that expose the types of values it sets to the receiver and the type of errors it can generate.

Template Parameters
  • S: The type that is being checked to see if it’s a typed sender

See

sender, sender_to

template<typename S, typename R> concept sender_to

Concept that brings together a sender and a receiver.

This concept extends the

sender concept, and ensures that it can connect to the given receiver type. It does that by checking if concore::connect(S, R) is valid.
Template Parameters
  • S: The type of sender that is assessed

  • R: The type of receiver that the sender must conform to

See

sender, receiver

template<typename OpState> concept operation_state

Concept that defines an operation state.

An object whose type satisfies

operation_state represents the state of an asynchronous operation. It is the result of calling concore::connect with a sender and a receiver.
Template Parameters
  • OpState: The type that is being checked to see if it’s a operation_state

A compatible type must implement the start() CPO. In addition, any object of this type must be destructible. Only object types model operation states.

See

sender, receiver, connect()

template<typename S> concept scheduler

Concept that defines a scheduler.

A scheduler type allows a schedule() operation that creates a sender out of the scheduler. A typical scheduler contains an execution context that will pass to the sender on its creation.

Template Parameters
  • S: The type that is being checked to see if it’s a scheduler

The type that match this concept must be move and copy constructible and must also define the schedule() CPO.

See

sender

struct bulk_execute_t
#include <execution.hpp>

Customization-point-object tag for bulk_execute.

To add support for bulk_execute to a type T, one can define:

template <typename F, typename N>
void tag_invoke(bulk_execute_t, T, F, N);

struct connect_t
#include <execution.hpp>

Customization-point-object tag for connect.

To add support for connect to a type S, with the receiver R, one can define: template <typename r>=”“> void tag_invoke(connect_t, S, R);

See

connect()

struct execute_t
#include <execution.hpp>

Type to use for customization point for execute.

This can be used for types that do not directly model the executor concepts. One can define a tag_invoke customization point to make the type be an executor.

For any given type Ex, and a functor type Fn, defining void tag_invoke(execute_t, Ex, Fn) {…}

will make the executor_of<Ex, Fn> be true. that is, one can later call:

execute(ex, f);
, where ex is an object of type Ex, and f is an object of type Fn.

See

execute()

struct invocable_archetype
#include <execution.hpp>

A type representing the archetype of an invocable object.

This essentially represents a ‘void()’ functor.

Public Functions

void operator()() & noexcept
struct receiver_invocation_error : public runtime_error, public nested_exception

Public Functions

receiver_invocation_error() noexcept
struct schedule_t
#include <execution.hpp>

Customization-point-object tag for schedule.

To add support for schedule to a type S, one can define:

template <typename S>
auto tag_invoke(schedule_t, S);

struct set_done_t
#include <execution.hpp>

Type to use for customization point for set_done.

This can be used for types that do not directly model the receiver concepts. One can define a tag_invoke customization point to make the type be a receiver.

For a type to be receiver, it needs to have the following customization points:

  • tag_invoke(set_value_t, receiver, ...)

  • tag_invoke(set_done_t, receiver)

  • tag_invoke(set_error_t, receiver, err)

See

set_done()

struct set_error_t
#include <execution.hpp>

Type to use for customization point for set_error.

This can be used for types that do not directly model the receiver concepts. One can define a tag_invoke customization point to make the type be a receiver.

For a type to be receiver, it needs to have the following customization points:

  • tag_invoke(set_value_t, receiver, ...)

  • tag_invoke(set_done_t, receiver)

  • tag_invoke(set_error_t, receiver, err)

See

set_error()

struct set_value_t
#include <execution.hpp>

Type to use for customization point for set_value.

This can be used for types that do not directly model the receiver concepts. One can define a tag_invoke customization point to make the type be a receiver.

For a type to be receiver, it needs to have the following customization points:

  • tag_invoke(set_value_t, receiver, ...)

  • tag_invoke(set_done_t, receiver)

  • tag_invoke(set_error_t, receiver, err)

See

set_value()

struct start_t
#include <execution.hpp>

Type to use for customization point for starting async operations.

This can be used for types that do not directly model the operation_state concept. One can define a tag_invoke customization point to make the type be an operation_state.

See

start()

struct submit_t
#include <execution.hpp>

Customization-point-object tag for submit.

To add support for submit to a type S, with the receiver R, one can define:

template <typename R>
void tag_invoke(submit_t, S, R);

See

submit()

thread_pool.hpp

namespace concore
namespace v1
class static_thread_pool
#include <thread_pool.hpp>

A pool of threads that can execute work.

This is constructed with the number of threads that are needed in the pool. Once constructed, these threads cannot be detached from the pool. The user is allowed to attach other threads to the pool, but without any possibility of detaching them earlier than the destruction of the pool. There is no automatic resizing of the pool.

The user can manually signal the thread pool to stop processing items, and/or wait for the existing work items to drain out.

When destructing this object, the implementation ensures to wait on all the in-progress items.

Any scheduler or executor objects that are created cannot exceed the lifetime of this object.

Properties of the static_thread_pool executor:

  • blocking.always

  • relationship.fork

  • outstanding_work.untracked

  • bulk_guarantee.parallel

  • mapping.thread

Public Types

using scheduler_type = detail::thread_pool_scheduler

The type of scheduler that this object exposes.

using executor_type = detail::thread_pool_executor

The type of executor that this object exposes.

Public Functions

static_thread_pool(std::size_t num_threads)

Constructs a thread pool.

This thread pool will create the given number of “internal” threads. This number of threads cannot be changed later on. In addition to these threads, the user might manually add other threads in the pool by calling the

attach() method.
Parameters
  • num_threads: The number of threads to statically create in the thread pool

See

attach()

static_thread_pool(const static_thread_pool&) = delete
static_thread_pool &operator=(const static_thread_pool&) = delete
static_thread_pool(static_thread_pool&&) = default
static_thread_pool &operator=(static_thread_pool&&) = default
~static_thread_pool()

Destructor for the static pool.

Ensures that all the tasks already in the pool are drained out before destructing the pool. New tasks will not be executed anymore in the pool.

This is equivalent to calling stop() and then wait().

void attach()

Attach the current thread to the thread pool.

The thread that is calling this will temporary join this thread pool. The thread will behave as if it was created during the constructor of this class. The thread will be released from the pool (and return to the caller) whenever the stop() and wait() are releasing the threads from this pool.

If the thread pool is stopped, this will exit immediately without attaching the current thread to the thread pool.

See

stop(), wait()

void join()

Alternative name for attach()

void stop()

Signal all work to complete.

This will signal the thread pool to stop working as soon as possible. This will return immediately without waiting on the worker threads to complete.

After calling this, no new work will be taken by the thread pool.

This will cause the threads attached to this pool to detach (after completing ongoing work).

This is not thread-safe. Ensure that this is called from a single thread.

void wait()

Wait for all the threads attached to the thread pool to complete.

If not already stopped, it will signal the thread pool for completion. Calling just wait() is similar to calling stop() and then wait().

If there are ongoing tasks in the pool that are still executing, this will block until all these tasks are completed.

After the call to this function, all threads are either terminated or detached from the thread pool.

See

stop(), attach()

scheduler_type scheduler() noexcept

Returns a scheduler that can be used to schedule work here.

The returned scheduler object can be used to create sender objects that may be used to submit receiver objects to this thread pool.

The returned object has the following properties:

  • execution::allocator

  • execution::allocator(std::allocator<void>())

See

executor()

executor_type executor() noexcept

Returns an executor object that can add work to this thread pool.

This returns an executor object that can be used to submit function objects to be executed by this thread pool.

The returned object has the following properties:

  • execution::blocking.possibly

  • execution::relationship.fork

  • execution::outstanding_work.untracked

  • execution::allocator

  • execution::allocator(std::allocator<void>())

See

scheduler()

Private Members

std::unique_ptr<detail::pool_data> impl_

The implementation data; use pimpl idiom.

Friends

friend task_group get_associated_group(const static_thread_pool &pool)

as_invocable.hpp

namespace concore
namespace v1
struct as_invocable
#include <as_invocable.hpp>

Wrapper that transforms a receiver into a functor.

The receiver should model receivereceiver_of<>.

Template Parameters
  • R: The type of the receiver

This will store a reference to the receiver; the receiver must not get out of scope.

When this functor is called set_value() will be called on the receiver. If an exception is thrown, the set_error() function is called.

If the functor is never called, the destructor of this object will call set_done().

See

as_receiver

Public Functions

as_invocable(R &r) noexcept
as_invocable(as_invocable &&other) noexcept
as_invocable &operator=(as_invocable &&other) noexcept
as_invocable(const as_invocable&) = delete
as_invocable &operator=(const as_invocable&) = delete
~as_invocable()
void operator()() noexcept

Private Members

R *receiver_

as_operation.hpp

namespace concore
namespace v1
template<CONCORE_CONCEPT_OR_TYPENAME(executor) E, CONCORE_CONCEPT_OR_TYPENAME(receiver_of) R> as_operation
#include <as_operation.hpp>

Wrapper that transforms an executor and a receiver into an operation.

This is a convenience wrapper to shortcut the usage of scheduler and sender.

Template Parameters
  • E: The type of the executor

  • R: The type of the receiver

See

as_invocable, as_sender

Public Types

using executor_type = concore::detail::remove_cvref_t<E>
using receiver_type = concore::detail::remove_cvref_t<R>

Public Functions

as_operation(executor_type e, receiver_type r) noexcept
void start() noexcept

Private Members

executor_type executor_
receiver_type receiver_

as_receiver.hpp

namespace concore
namespace v1
template<typename F>
struct as_receiver
#include <as_receiver.hpp>

Wrapper that transforms a functor into a receiver.

This will implement the operations specific to a receiver given a functor. The receiver will call the functor whenever

set_value() is called. It will not do anything on set_done() and it will terminate the program if set_error() is called.
Template Parameters
  • F: The type of the functor

Public Functions

as_receiver(F &&f) noexcept
void set_value() noexcept(noexcept(f_()))

Called whenever the sender completed the work with success.

void set_done() noexcept

Called whenever the work was canceled.

template<typename E>
void set_error(E) noexcept

Called whenever there was an error while performing the work in the sender.

Private Members

F f_

The functor to be called when the sender finishes the work.

as_sender.hpp

namespace concore
namespace v1
template<CONCORE_CONCEPT_OR_TYPENAME(executor) E> as_sender
#include <as_sender.hpp>

Wrapper that transforms a receiver into a functor.

The receiver should model receiver_of<>.

Template Parameters
  • R: The type of the receiver

This will store a reference to the receiver; the receiver must not get out of scope.

When this functor is called set_value() will be called on the receiver. If an exception is thrown, the set_error() function is called.

If the functor is never called, the destructor of this object will call set_done().

See

as_receiver

Public Types

using value_types = Variant<Tuple<>>
using error_types = Variant<std::exception_ptr>

Public Functions

as_sender(E e) noexcept
template<CONCORE_CONCEPT_OR_TYPENAME(receiver_of) R> as_operation< E, R > connect (R &&r) &&
template<CONCORE_CONCEPT_OR_TYPENAME(receiver_of) R> as_operation< E, R > connect (R &&r) const &

Public Static Attributes

constexpr bool sends_done = false

Private Members

E ex_

Data

data/concurrent_queue.hpp

namespace concore
namespace v1
template<typename T, queue_type conc_type = queue_type::multi_prod_multi_cons>
class concurrent_queue
#include <concurrent_queue.hpp>

Concurrent double-ended queue implementation.

Based on the conc_type parameter, this can be:

  • single-producer, single-consumer

  • single-producer, multi-consumer

  • multi-producer, single-consumer

  • multi-producer, multi-consumer

Template Parameters
  • T: The type of elements to store

  • conc_type: The expected concurrency for the queue

Note, that the implementation for some of these alternatives might coincide.

The queue, has 2 ends:

  • the back: where new element can be added

  • the front: from which elements can be extracted

The queue has only 2 operations corresponding to pushing new elements into the queue and popping elements out of the queue.

See

queue_type, push(), pop()

Public Types

using value_type = T

The value type of the concurrent queue.

Public Functions

concurrent_queue() = default

Default constructor. Creates a valid empty queue.

~concurrent_queue() = default
concurrent_queue(const concurrent_queue&) = delete

Copy constructor is DISABLED.

const concurrent_queue &operator=(const concurrent_queue&) = delete

Copy assignment is DISABLED.

concurrent_queue(concurrent_queue&&) = default
concurrent_queue &operator=(concurrent_queue&&) = default
void push(T &&elem)

Pushes one element in the back of the queue.

This ensures that is thread-safe with respect to the chosen queue_type concurrency policy.

Parameters
  • elem: The element to be added to the queue

See

try_pop()

bool try_pop(T &elem)

Try to pop one element from the front of the queue.

Try to pop one element from the front of the queue. Returns false if the queue is empty. This is considered the default popping operation.

If the queue is empty, this will return false and not touch the given parameter. If the queue is not empty, it will extract the element from the front of the queue and store it in the given parameter.

Return

True if an element was popped; false otherwise.

Parameters
  • elem: [out] Location where to put the popped element

This ensures that is thread-safe with respect to the chosen queue_type concurrency policy.

See

push()

Private Types

using node_ptr = detail::node_ptr

Private Members

detail::concurrent_queue_data queue_

The data holding the actual queue.

detail::node_factory<T> factory_

Object that creates nodes, and keeps track of the freed nodes.

data/concurrent_queue_type.hpp

namespace concore
namespace v1

Enums

enum queue_type

Queue type, based o the desired level of concurrency for producers and consumers.

Please note that this express only the desired type. It doesn’t mean that implementation will be strictly obey the policy. The implementation can be more conservative and fall-back to less optimal implementation. For example, the implementation can always use the multi_prod_multi_cons type, as it includes all the constraints for all the other types.

Values:

enumerator single_prod_single_cons

Single-producer, single-consumer concurrent queue.

enumerator single_prod_multi_cons

Single-producer, multiple-consumer concurrent queue.

enumerator multi_prod_single_cons

Multiple-producer, single-consumer concurrent queue.

enumerator multi_prod_multi_cons

Multiple-producer, multiple-consumer concurrent queue.

enumerator default_type

The default queue type. Multiple-producer, multiple-consumer concurrent queue.

Low level

low_level/spin_backoff.hpp

Defines

CONCORE_LOW_LEVEL_SHORT_PAUSE()

Pauses the CPU for a short while.

The intent of this macro is to pause the CPU, without consuming energy, while waiting for some other condition to happen. The pause should be sufficiently small so that the current thread will not give up its work quanta.

This pause should be smaller than the pause caused by CONCORE_LOW_LEVEL_YIELD_PAUSE().

This is used in spin implementations that are waiting for certain conditions to happen, and it is expected that these condition will become true in a very short amount of time.

The implementation of this uses platform-specific instructions.

See

CONCORE_LOW_LEVEL_YIELD_PAUSE(), concore::v1::spin_backoff

CONCORE_LOW_LEVEL_YIELD_PAUSE()

Pause that will make the current thread yield its CPU quanta.

This is intended to be a longer pause than CONCORE_LOW_LEVEL_SHORT_PAUSE(). It is used in spin algorithms that wait for some condition to become true, but apparently that condition does not become true soon enough. Instead of blocking the CPU waiting on this condition, we give up the CPU quanta to be used by other threads; hopefully, by running other threads, that condition can become true.

See

CONCORE_LOW_LEVEL_SHORT_PAUSE(), concore::v1::spin_backoff

namespace concore
namespace v1
class spin_backoff
#include <spin_backoff.hpp>

Class that can spin with exponential backoff.

This is intended to be used for implement spin-wait algorithms. It is assumed that the thread that is calling this will wait on some resource from another thread, and the other thread should release that resource shortly. Instead of giving up the CPU quanta, we prefer to spin a bit until we can get the resource

This will spin with an exponential long pause; after a given threshold this will just yield the CPU quanta of the current thread.

See

concore::spin_mutex

Public Functions

void pause()

Pauses a short while.

Calling this multiple times will pause more and more. In the beginning the pauses are short, without yielding the CPU quanta of the current thread. But, after a threshold this attempts to give up the CPU quanta for the current executing thread.

Private Members

int count_ = {1}

The count of ‘pause’ instructions we should make.

low_level/spin_mutex.hpp

namespace concore
namespace v1
class spin_mutex
#include <spin_mutex.hpp>

Mutex class that uses CPU spinning while attempting to take the lock.

For mutexes that protect very small regions of code, a spin_mutex can be much faster than a traditional mutex. Instead of taking a lock, this will spin on the CPU, trying to avoid yielding the CPU quanta.

This uses an exponential backoff spinner. If after some time doing small waits it cannot enter the critical section, it will yield the CPU quanta of the current thread.

Spin mutexes should only be used to protect very-small regions of code; a handful of CPU instructions. For larger scopes, a traditional mutex may be faster; but then, think about using serializer to avoid mutexes completely.

See

spin_backoff

Public Functions

spin_mutex() = default

Default constructor.

Constructs a spin mutex that is not acquired by any thread.

~spin_mutex() = default
spin_mutex(const spin_mutex&) = delete

Copy constructor is DISABLED.

spin_mutex &operator=(const spin_mutex&) = delete

Copy assignment is DISABLED.

spin_mutex(spin_mutex&&) = delete
spin_mutex &operator=(spin_mutex&&) = delete
void lock()

Acquires ownership of the mutex.

Uses a spin_backoff to spin while waiting for the ownership to be free. When exiting this function the mutex will be owned by the current thread.

An unlock() call must be made for each call to lock().

See

try_lock(), unlock()

bool try_lock()

Tries to lock the mutex; returns false if the mutex is not available.

This is similar to

lock() but does not wait for the mutex to be free again. If the mutex is acquired by a different thread, this will return false.
Return

True if the mutex ownership was acquired; false if the mutex is busy

An unlock() call must be made for each call to this method that returns true.

See

lock(), unlock()

void unlock()

Releases the ownership on the mutex.

This needs to be called for every lock() and for every try_lock() that returns true. It should not be called without a matching lock() or try_lock().

See

lock(), try_lock()

Private Members

std::atomic_flag busy_ = ATOMIC_FLAG_INIT

True if the spin mutex is taken.

low_level/shared_spin_mutex.hpp

namespace concore
namespace v1
class shared_spin_mutex
#include <shared_spin_mutex.hpp>

A shared (read-write) mutex class that uses CPU spinning.

For mutexes that protect very small regions of code, a shared_spin_mutex can be much faster than a traditional shared_mutex. Instead of taking a lock, this will spin on the CPU, trying to avoid yielding the CPU quanta.

The ownership of the mutex can fall in 3 categories:

  • no ownership no thread is using the mutex

  • exclusive ownership only one thread can access the mutex, exclusively (WRITE operations)

  • shared ownership multiple threads can access the mutex in a shared way (READ operations)

While one threads acquires exclusive ownership, no other thread can have shared ownership. Multiple threads can have a shared ownership over the mutex.

This implementation favors exclusive ownership versus shared ownership. If a thread is waiting for exclusive ownership and one thread is waiting for the shared ownership, the thread that waits on the exclusive ownership will be granted the ownership first.

This uses an exponential backoff spinner. If after some time doing small waits it cannot enter the critical section, it will yield the CPU quanta of the current thread.

Spin shared mutexes should only be used to protect very-small regions of code; a handful of CPU instructions. For larger scopes, a traditional shared mutex may be faster; but then, think about using rw_serializer to avoid mutexes completely.

See

spin_mutex, spin_backoff, rw_serializer

Public Functions

shared_spin_mutex() = default

Default constructor.

Constructs a shared spin mutex that is in the no ownership state.

~shared_spin_mutex() = default
shared_spin_mutex(const shared_spin_mutex&) = delete

Copy constructor is DISABLED.

shared_spin_mutex &operator=(const shared_spin_mutex&) = delete

Copy assignment is DISABLED.

shared_spin_mutex(shared_spin_mutex&&) = delete
shared_spin_mutex &operator=(shared_spin_mutex&&) = delete
void lock()

Acquires exclusive ownership of the mutex.

This will put the mutex in the exclusive ownership case. If other threads have exclusive or shared ownership, this will wait until those threads are done

Uses a spin_backoff to spin while waiting for the ownership to be free. When exiting this function the mutex will be exclusively owned by the current thread.

An unlock() call must be made for each call to lock().

See

try_lock(), unlock(), lock_shared()

bool try_lock()

Tries to acquire exclusive ownership; returns false it fails the acquisition.

This is similar to

lock() but does not wait for the mutex to be free again. If the mutex is acquired by a different thread, or if the mutex has shared ownership this will return false.
Return

True if the mutex exclusive ownership was acquired; false if the mutex is busy

An unlock() call must be made for each call to this method that returns true.

See

lock(), unlock()

void unlock()

Releases the exclusive ownership on the mutex.

This needs to be called for every lock() and for every try_lock() that returns true. It should not be called without a matching lock() or try_lock().

See

lock(), try_lock()

void lock_shared()

Acquires shared ownership of the mutex.

This will put the mutex in the shared ownership case. If other threads have exclusive ownership, this will wait until those threads are done.

Uses a spin_backoff to spin while waiting for the ownership to be free. When exiting this function the mutex will be exclusively owned by the current thread.

An unlock_shared() call must be made for each call to lock().

See

try_lock_shared(), unlock_shared(), lock()

bool try_lock_shared()

Tries to acquire shared ownership; returns false it fails the acquisition.

This is similar to

lock_shared() but does not wait for the mutex to be free again. If the mutex is exclusively acquired by a different thread this will return false.
Return

True if the mutex shared ownership was acquired; false if the mutex is busy

An unlock_shared() call must be made for each call to this method that returns true.

See

lock_shared(), unlock_shared()

void unlock_shared()

Releases the sjared ownership on the mutex.

This needs to be called for every lock_shared() and for every try_lock_shared() that returns true. It should not be called without a matching lock_shared() or try_lock_shared().

See

lock_shared(), try_lock_shared()

Private Members

std::atomic<uintptr_t> lock_state_ = {0}

The state of the shared spin mutex. The first 2 LSB will indicate whether we have a writer or we are having a pending writer. The rest of the bits indicates the count of the readers.

Private Static Attributes

constexpr uintptr_t has_writer_ = 1

Bitmask to check if we have a writer acquiring the mutex.

constexpr uintptr_t has_writer_pending_ = 2

Bitmask to check if somebody tries to acquire the mutex as writer.

constexpr uintptr_t has_writer_or_pending_ = has_writer_ | has_writer_pending_

Bitmask indicating that we have a writer, or a pending writer.

constexpr uintptr_t readers_ = ~(has_writer_or_pending_)

Bitmask with that capture all the readers that we have.

constexpr uintptr_t is_busy_ = (has_writer_ | readers_)

Bitmask indicating whether we have a writer or readers.

constexpr uintptr_t reader_increment_ = 4

The increment that we need to use for each reader.

low_level/semaphore.hpp

namespace concore
namespace v1
class binary_semaphore
#include <semaphore.hpp>

A semaphore that has two states: SIGNALED and WAITING.

It’s assumed that the user will not call signal() multiple times.

It may be implemented exactly as a semaphore, but on some platforms it can be implemented more efficiently.

See

semaphore

Public Functions

binary_semaphore()
~binary_semaphore()

Destructor.

binary_semaphore(const binary_semaphore&) = delete

Copy constructor is DISABLED.

void operator=(const binary_semaphore&) = delete

Copy assignment is DISABLED.

binary_semaphore(binary_semaphore&&) = delete
void operator=(binary_semaphore&&) = delete
void wait()

Wait for the semaphore to be signaled.

This will put the binary semaphore in the WAITING state, and wait for a thread to signal it. The call will block until a corresponding thread will signal it.

See

signal(0)

void signal()

Signal the binary semaphore.

Puts the semaphore in the SIGNALED state. If there is a thread that waits on the semaphore it will wake it.

class semaphore
#include <semaphore.hpp>

The classic “semaphore” synchronization primitive.

It atomically maintains an internal count. The count can always be increased by calling signal(), which is always a non-blocking call. When calling wait(), the count is decremented; if the count is still positive the call will be non-blocking; if the count goes below zero, the call to wait() will block until some other thread calls signal().

See

binary_semaphore

Public Functions

semaphore(int start_count = 0)

Constructs a new semaphore instance.

Parameters
  • start_count: The value that the semaphore count should have at start

~semaphore()

Destructor.

semaphore(const semaphore&) = delete

Copy constructor is DISABLED.

void operator=(const semaphore&) = delete

Copy assignment is DISABLED.

semaphore(semaphore&&) = delete
void operator=(semaphore&&) = delete
void wait()

Decrement the internal count and wait on the count to be positive.

If the count of the semaphore is positive this will decrement the count and return immediately. On the other hand, if the count is 0, it wait for it to become positive before decrementing it and returning.

See

signal()

void signal()

Increment the internal count.

If there are at least one thread that is blocked inside a wait() call, this will wake up a waiting thread.

See

wait()

low_level/concurrent_dequeue.hpp

namespace concore
namespace v1
template<typename T>
class concurrent_dequeue
#include <concurrent_dequeue.hpp>

Concurrent double-ended queue implementation, for a small number of elements.

This will try to preallocate a vector with enough elements to cover the most common cases. Operations on the concurrent queue when we have few elements are fast: we only make atomic operations, no memory allocation. We only use spin mutexes in this case.

Template Parameters
  • T: The type of elements to store

If we have too many elements in the queue, we switch to a slower implementation that can grow to a very large number of elements. For this we use regular mutexes.

Note 1: when switching between fast and slow, the FIFO ordering of the queue is lost.

Note 2: for efficiency reasons, the element size should be at least as a cache line (otherwise we can have false sharing when accessing adjacent elements)

Note 3: we expect very-low contention on the front of the queue, and some contention at the end of the queue. And of course, there will be more contention when the queue is empty or close to empty.

Note 4: we expect contention over the atomic that stores the begin/end position in the fast queue

The intent of this queue is to hold tasks in the task system. There, we typically add any enqueued tasks to the end of the queue. The tasks that are spawned while working on some task are pushed to the front of the queue. The popping of the tasks is typically done on the front of the queue, but when stealing tasks, popping is done from the back of the queue trying to maximize locality for nearby tasks.

Public Types

using value_type = T

Public Functions

concurrent_dequeue(size_t expected_size)

Constructs a new instance of the queue, with the given preallocated size.

If we ever add more elements in our queue than the given limit, our queue starts to become slower.

Parameters
  • [in] expected_size: How many elements to preallocate in our fast queue.

The number of reserved elements should be bigger than the expected concurrency.

void push_back(T &&elem)

Pushes one element in the back of the queue. This is considered the default pushing operation.

void push_front(T &&elem)

Push one element to the front of the queue.

bool try_pop_front(T &elem)

Try to pop one element from the front of the queue. Returns false if the queue is empty. This is considered the default popping operation.

bool try_pop_back(T &elem)

Try to pop one element from the back of the queue. Returns false if the queue is empty.

void unsafe_clear()

Clears the queue.

Private Members

detail::bounded_dequeue<T> fast_deque_

The fast dequeue implementation; uses a fixed number of elements.

std::deque<T> slow_access_elems_

Deque of elements that have slow access; we use this if we go beyond our threshold.

std::mutex bottleneck_

Protects the access to slow_access_elems_.

std::atomic<int> num_elements_slow_ = {0}

The number of elements stored in slow_access_elems_; used it before trying to take the lock.