Open-source project
dougbinks/enkiTS avatar
dougbinks/enkiTS

enkiTS: task scheduling for C and C++ without runtime allocations

A permissively licensed C and C++ Task Scheduler for creating parallel programs. Requires C++11 support.

2,041 stars175 forksC++Zlib

At a glance

What is it?
enkiTS is a Zlib-licensed task scheduler for C and C++ programs that need data and task parallelism across CPU cores. Zero allocations happen during scheduling itself, integration is a matter of adding source files, and the C API changed in a breaking way at v1.11.
Who is it for?
enkiTS suits C and C++ projects that need task and data parallelism on consumer hardware without runtime allocations during dispatch. Windows and Linux are the primary development targets.
Can I use it commercially?
Yes. Zlib is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 13 days ago.
What is it written in?
Mainly C++, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Zero allocations during scheduling, designed for consumer hardware first

enkiTS is a task scheduler for C and C++ programs that need to distribute work across multiple CPU cores, and the primary goal is to let developers create programs handling both data and task level parallelism to fully use multicore CPUs while keeping the code small enough to understand. It was developed for, and continues to be used in, the Avoyd codebase at enkisoftware.

The design puts fast performance on consumer devices first, then scalability to larger core counts second. For a game running on a player's machine with a small number of threads, that ordering means the scheduler is tuned for the common hardware case before tuning for machines with many more.

Zero allocations during scheduling is the concrete commitment that follows from that goal. Task dispatch does not call the heap allocator at runtime, so the scheduler adds no allocation overhead to a game loop or a real-time context where allocation spikes introduce latency. The entire scheduler source is a handful of files under `src/`, and the C++ API lives in `TaskScheduler.h`.

Adding source files manually or via cmake, with an optional system install

enkiTS integrates as source files rather than a prebuilt library, and requires C++11 support at minimum. For a C++ project, add `enkiTS/src` to the include path and compile `TaskScheduler.cpp` alongside the project's own source. For the C API, `TaskScheduler_c.cpp` is also compiled. On Unix and Linux, pthreads is required as an additional link dependency.

cmake is the alternative for projects that prefer a build system. From the enkiTS directory:

bash
mkdir build
cd build
cmake ..
make all

On Windows with Visual Studio, cmake generates `enkiTS.sln` rather than Makefiles, and the solution is then opened in the IDE.

The cmake script does not install headers system-wide by default. Setting the cmake variable `ENKITS_INSTALL` to ON enables installation, at which point headers land under `include/enkiTS` in the install prefix. That subdirectory naming is intentional: it prevents the headers from conflicting with other packages in the include search path, and applications must then use `#include "enkiTS/TaskScheduler.h"` rather than `#include "TaskScheduler.h"`.

Embedding the source in each project rather than relying on a system install keeps each copy independent and avoids coordinating a global version upgrade across multiple projects.

Initialize, AddTaskSetToPipe and WaitforTask are the three calls for a first run

The C++ API starts with an `enki::TaskScheduler` object, and the minimal example from the project shows three calls in sequence:

cpp
#include "TaskScheduler.h"

enki::TaskScheduler g_TS;

struct ParallelTaskSet : enki::ITaskSet {
    void ExecuteRange( enki::TaskSetPartition range_, uint32_t threadnum_ ) override {
    }
};

int main(int argc, const char * argv[]) {
    g_TS.Initialize();
    ParallelTaskSet task;
    g_TS.AddTaskSetToPipe( &task );
    g_TS.WaitforTask( &task );
    return 0;
}

`g_TS.Initialize()` sets up the worker threads, `AddTaskSetToPipe` submits the task to the internal pipe, and `WaitforTask` blocks until the task completes. The code comment accompanying the example notes that the calling thread itself runs available tasks while waiting rather than sitting idle, which means the main thread participates in the work.

`ExecuteRange` receives a `TaskSetPartition` describing the slice of work for this call, along with a `threadnum_` that identifies which worker thread is running it, both accessible inside the method body without needing additional synchronization. Multiple task sets can be in flight at once by calling `AddTaskSetToPipe` more than once before waiting.

Braided parallelism: tasks that spawn child tasks from inside ExecuteRange

Braided parallelism is the name the project uses for a specific capability: tasks that themselves create and submit further tasks. The comment inside `ExecuteRange` in the basic example reads "can issue tasks with g_TS", which signals that calling the scheduler from inside a running task is safe and supported.

The lambda form provides the same capability without a derived struct:

cpp
#include "TaskScheduler.h"

enki::TaskScheduler g_TS;

int main(int argc, const char * argv[]) {
   g_TS.Initialize();
   enki::TaskSet task( 1, []( enki::TaskSetPartition range_, uint32_t threadnum_ ) {
   } );
   g_TS.AddTaskSetToPipe( &task );
   g_TS.WaitforTask( &task );
   return 0;
}

The lambda receives the same partition and thread number as the struct form, so a task that spawns further work constructs a new `TaskSet` inside the lambda body and submits it to `g_TS` exactly as the outer task was submitted.

Two parallelism patterns follow from this design. Data parallelism distributes a single operation over a range by increasing the task set size and reading `range_` to identify the slice for this call. Task parallelism dispatches entirely different work items as separate tasks from inside a running one, with both patterns using the same `AddTaskSetToPipe` and `WaitforTask` calls.

Pinned tasks, up to five priority levels, and external thread registration

Three features in enkiTS address control over where and when tasks run, and each has both a C++ and a C example in the `example/` directory.

Pinned tasks are scheduled to run on a specific named thread rather than any available worker. Waiting for pinned tasks was introduced in v1.10 and suits IO threads that do no other work: `WaitForNewPinnedTasks` blocks the calling thread until a pinned task arrives for it. The full examples are in `example/WaitForNewPinnedTasks.cpp` and `example/WaitForNewPinnedTasks_c.c`.

Task priorities let the scheduler run higher-priority work before lower-priority items. The number of priority levels is set at compile time via the preprocessor define `ENKITS_TASK_PRIORITIES_NUM`, which defaults to 3 and can be raised to at most 5. Since this is a compile-time define rather than a runtime parameter, changing the priority count requires recompiling the scheduler.

External threads are the third option: configuring the scheduler with `numExternalTaskThreads` registers threads from outside enkiTS so they can use the enkiTS API. The examples are in `example/ExternalTaskThread.cpp` and `example/ExternalTaskThread_c.c`.

Completion Actions avoid a scheduler round-trip, but the C API broke at v1.11

Completion Actions run code when a task finishes without adding the completed task back to the scheduler as a new item, which avoids that round-trip cost. The supported use case is safely deleting a completed task, and examples are in `example/CompletionAction.cpp` and `example/CompletionAction_c.c`.

The v1.11 release notes include this: "Breaking C API change to Completion Actions - added Pre and Post completion functions." The C++ API is not described as breaking in those notes, so only C interface users who upgrade from any pre-v1.11 embedded copy need to update their Completion Actions code. That distinction matters if the C and C++ interfaces are used in different parts of the same codebase.

v1.12 brought improved performance on systems with large numbers of hardware threads. Compare that with the feature list, which puts consumer devices first and scalability second in the design's stated priority ordering. The most recent published version specifically addressed the secondary concern in that ordering, which is where development work was directed in the gap between those two versions.

The span from 2022 to 2026 is also worth noting for anyone maintaining an embedded copy, since the C API breaking change means that any copy predating 2022 carries a different Completion Actions interface. For C API users, the practical step is to check each Completion Actions call site against the current `TaskScheduler_c.h` before upgrading.

Less frequent testing on Mac OS and ARM Android, and an undocumented Zig build

Primary development is on x64 and x86 Intel architectures running Windows, and Linux has well tested support. Mac OS and ARM Android receive somewhat less frequent testing, and iOS is listed as "should work" without any claim of verified support.

For a project targeting Linux or Windows desktop, the test coverage sits on the primary development path. For Mac OS or ARM Android, building and running the test suite on the actual hardware before depending on enkiTS matters more than it would on the primary targets, since those paths get less developer attention and the documentation does not quantify how much less.

The top-level repository holds `build.zig` and `build.zig.zon` alongside `CMakeLists.txt`. cmake and manual source integration are the documented paths, but the Zig build has no corresponding guidance anywhere in the repository. For projects already using Zig as their build system, those files are present without documentation on targets, flags or feature support.

Custom allocators are also supported: `example/CustomAllocator.cpp` and `example/CustomAllocator_c.c` demonstrate the API, and the custom allocator path exists for both the C++ and C interfaces, which is consistent with the project's goal of keeping the two interfaces at parity.

Editorial conclusion

enkiTS suits C and C++ projects that need task and data parallelism on consumer hardware without runtime allocations during dispatch. Windows and Linux are the primary development targets. For Mac OS, iOS or ARM Android, test on the actual hardware before depending on it, since those paths are described as less frequently tested or unverified. If you are integrating an older embedded copy, check each Completion Actions call site in the C API against `TaskScheduler_c.h` before upgrading, since that interface changed in a breaking way at v1.11. The Zig build files in the repository root exist but carry no documentation.

Frequently asked questions

What does enkiTS require to compile?

enkiTS requires C++11 support. For C++ use, only `TaskScheduler.cpp` needs to be compiled alongside the project. For C use, `TaskScheduler_c.cpp` is also required. On Unix and Linux, pthreads is an additional link dependency.

Can enkiTS be used from the C programming language?

Yes. The C API is in `src/TaskScheduler_c.h`, and a C build requires both `TaskScheduler.cpp` and `TaskScheduler_c.cpp`. The v1.11 release notes described a breaking C API change to Completion Actions, adding Pre and Post completion functions.

How many task priority levels does enkiTS support?

The number of priority levels is set at compile time via the define `ENKITS_TASK_PRIORITIES_NUM`. It defaults to 3 and can be raised to at most 5. Higher priority tasks run before lower ones.

What platforms does enkiTS support?

Supported platforms include Windows, Linux, Mac OS, Android and iOS (listed as should work). Primary development is on x64 and x86 Intel architectures on Windows, with Linux well tested and Mac OS and ARM Android described as less frequently tested.

Does enkiTS allocate memory during task dispatch?

No. The design commits to zero allocations during scheduling itself. Task dispatch does not call the heap allocator at runtime, which is one of the project's stated design goals for consumer device performance.

Official sources

  1. dougbinks/enkiTS on GitHub
  2. Issues
  3. License: Zlib
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/dougbinks-enkits.svg)](https://hysenlabs.com/projects/dougbinks-enkits)