enkiTS Task Scheduler

repository·master·Indexed 24 days ago

https://github.com/dougbinks/enkits

A lightweight C/C++ task scheduler for data and task-level parallelism on multicore CPUs. Optimized for consumer devices with zero allocations during scheduling, enkiTS supports braided parallelism, task pinning, priorities, dependencies, and external thread registration. It provides interfaces like ITaskSet for parallel workloads and IPinnedTask for thread-specific execution, along with a configurable TaskScheduler for managing thread lifecycles and synchronization.

Tokens
4.3K
Snippets
10
Records
20
Agent score
34%

What's inside enkiTS

  1. Overview of enki Task Scheduler

    master

    enkiTS is a lightweight, permissively licensed C and C++ Task Scheduler designed for creating parallel programs. It aims to help developers handle both data-level and task-level parallelism to maximize multicore CPU performance. It is designed to be lean, fast on low thread counts, and scalable, with a focus on zero allocations during scheduling.

    Key features include:

    • Braided Parallelism: Tasks can be issued from other tasks or from the thread that created the Task System.
    • Task Pinning: Ability to pin tasks to specific threads.
    • Task Priorities: Configurable task priorities (default is 3, but can be increased via ENKITS_TASK_PRIORITIES_NUM).
    • External Threads: Ability to register external threads for use with the API.
    • Dependencies: Support for setting dependencies between tasks.
    • Completion Actions: Perform actions upon task completion to avoid re-scheduling overhead.
    • Custom Allocators: API for providing custom allocators.
  2. Build enkiTS using CMake

    master

    For Windows, macOS, or Linux with CMake installed, follow these steps in the enkiTS directory:

    1. Create a build directory: mkdir build
    2. Enter the directory: cd build
    3. Configure with CMake: cmake ..
    4. Build:
      • On Unix/Linux: run make all
      • On Windows: open the generated enkiTS.sln in Visual Studio.
    mkdir build
    cd build
    cmake ..
    # then run 'make all' or open enkiTS.sln
  3. Build and integrate enkiTS (C++)

    master

    To use the C++ interface, add the files in enkiTS/src to your build system and add that directory to your include path.

    Steps:

    1. Include the header: #include "TaskScheduler.h"
    2. Add enkiTS/src to your include path.
    3. Compile/Add to project: TaskScheduler.cpp.

    Note: Unix/Linux builds will likely require the pthreads library.

    #include "TaskScheduler.h"
  4. Install enkiTS via CMake

    master

    While it is recommended to use enkiTS directly from source in each project, you can install it using the CMake script by setting the ENKITS_INSTALL variable to ON (it defaults to OFF).

    When installed, header files are placed in a subdirectory of the include path: include/enkiTS.

    When building applications against an installed version, ensure the include path is set correctly. You should use the prefixed include path: #include "enkiTS/TaskScheduler.h" instead of #include "TaskScheduler.h".

    #include "enkiTS/TaskScheduler.h"
  5. Register external threads

    master

    If you have threads created outside of enkiTS that need to use the TaskScheduler API (e.g., to add tasks or wait for tasks), you must register them.

    1. Set numExternalTaskThreads in TaskSchedulerConfig during Initialize().
    2. Call RegisterExternalTaskThread() (or RegisterExternalTaskThread(uint32_t threadNumToRegister_)) from the external thread.
    3. Call DeRegisterExternalTaskThread() when the thread is finished using the API.
  6. Initialize the TaskScheduler

    master

    Before adding tasks, you must initialize the TaskScheduler. You can initialize it with default settings, a specific number of total threads, or a custom TaskSchedulerConfig object.

    • Initialize(): Creates GetNumHardwareThreads() - 1 tasking threads. The thread that calls initialize is considered thread 0.
    • Initialize(uint32_t numThreadsTotal_): Creates numThreadsTotal_ - 1 threads.
    • Initialize(TaskSchedulerConfig config_): Uses advanced configuration settings.
  7. Manage task priorities

    master

    enkiTS allows you to set priorities for task sets using enki::TASK_PRIORITY_LOW and enki::TASK_PRIORITY_HIGH. When calling WaitforTask, you can pass a priority level to ensure the calling thread only executes tasks of that priority or higher while waiting.

    #include "TaskScheduler.h"
    
    enki::TaskScheduler g_TS;
    
    struct ExampleTask : enki::ITaskSet {
        ExampleTask( ) { m_SetSize = size_; }
        void ExecuteRange( enki::TaskSetPartition range_, uint32_t threadnum_ ) override {
            // implementation
        }
    };
    
    int main(int argc, const char * argv[]) {
        g_TS.Initialize();
    
        ExampleTask lowPriorityTask( 10 );
        lowPriorityTask.m_Priority  = enki::TASK_PRIORITY_LOW;
    
        ExampleTask highPriorityTask( 1 );
        highPriorityTask.m_Priority = enki::TASK_PRIORITY_HIGH;
    
        g_TS.AddTaskSetToPipe( &lowPriorityTask );
        for( int task = 0; task < 10; ++task ) {
            g_TS.AddTaskSetToPipe( &highPriorityTask );
            // wait for task but only run tasks of the same priority or higher on this thread
            g_TS.WaitforTask( &highPriorityTask, highPriorityTask.m_Priority );
        }
        g_TS.WaitforTask( &lowPriorityTask );
    
        return 0;
    }
  8. Implement tasks using ITaskSet

    master

    To define a parallel task set in C++, implement the enki::ITaskSet interface. You must override the ExecuteRange method, which provides an enki::TaskSetPartition (representing the range of work) and the threadnum_ (the ID of the thread executing the range).

    #include "TaskScheduler.h"
    
    enki::TaskScheduler g_TS;
    
    struct ParallelTaskSet : enki::ITaskSet {
        void ExecuteRange(enki::TaskSetPartition range_, uint32_t threadnum_) override {
            // do something here, can issue tasks with g_TS
        }
    };
    
    int main(int argc, const char * argv[]) {
        g_TS.Initialize();
        ParallelTaskSet task;
        g_TS.AddTaskSetToPipe( &task );
        g_TS.WaitforTask( &task );
        return 0;
    }
  9. Register and use external task threads

    master

    If you have an existing thread pool or external threads that you want to participate in the enkiTS scheduling, you can register them using g_TS.RegisterExternalTaskThread().

    To support these threads, configure the enki::TaskSchedulerConfig by setting numExternalTaskThreads before calling Initialize().

    #include "TaskScheduler.h"
    
    enki::TaskScheduler g_TS;
    
    struct ParallelTaskSet : ITaskSet {
        void ExecuteRange( enki::TaskSetPartition range_, uint32_t threadnum_ ) override {
            // Do something
        }
    };
    
    void threadFunction() {
        g_TS.RegisterExternalTaskThread();
    
        // ... work ...
    
        ParallelTaskSet task;
        g_TS.AddTaskSetToPipe( &task );
        g_TS.WaitforTask( &task);
    
        g_TS.DeRegisterExternalTaskThread();
    }
    
    int main(int argc, const char * argv[]) {
        enki::TaskSchedulerConfig config;
        config.numExternalTaskThreads = 1;
    
        g_TS.Initialize( config );
    
        std::thread exampleThread( threadFunction );
        exampleThread.join();
    
        return 0;
    }
  10. Use Pinned Tasks

    master

    Pinned tasks are executed on specific threads. Implement the enki::IPinnedTask interface and override Execute(). By default, a pinned task is assigned to thread 0 (the main thread).

    Note: You must call g_TS.RunPinnedTasks() on the main thread to execute pinned tasks for that thread. Tasking threads handle this automatically in their internal loop.

    #include "TaskScheduler.h"
    
    enki::TaskScheduler g_TS;
    
    struct PinnedTask : enki::IPinnedTask {
        void Execute() override {
          // do something here
        }
    };
    
    int main(int argc, const char * argv[]) {
        g_TS.Initialize();
        PinnedTask task;
        g_TS.AddPinnedTask( &task );
    
        // RunPinnedTasks must be called on main thread to run any pinned tasks for that thread.
        g_TS.RunPinnedTasks();
    
        g_TS.WaitforTask( &task );
        return 0;
    }
  11. Manage IO threads with WaitForNewPinnedTasks

    master

    For threads that spend significant time blocked (e.g., IO threads), you can use g_TS.WaitForNewPinnedTasks() inside a loop. This allows the thread to 'sleep' until new pinned tasks are available, preventing high CPU usage while idling.

    Combine this with g_TS.RunPinnedTasks() to execute the tasks once they arrive.

    #include "TaskScheduler.h"
    
    enki::TaskScheduler g_TS;
    
    struct RunPinnedTaskLoopTask : enki::IPinnedTask {
        void Execute() override {
            while( !g_TS.GetIsShutdownRequested() ) {
                g_TS.WaitForNewPinnedTasks(); // sleep until new pinned tasks arrive
                g_TS.RunPinnedTasks();
            }
        }
    };
    
    struct PretendDoFileIO : enki::IPinnedTask {
        void Execute() override {
            // Do file IO
        }
    };
    
    int main(int argc, const char * argv[]) {
        enki::TaskSchedulerConfig config;
        config.numTaskThreadsToCreate += 1;
    
        g_TS.Initialize( config );
    
        RunPinnedTaskLoopTask runPinnedTaskLoopTasks;
        runPinnedTaskLoopTasks.threadNum = g_TS.GetNumTaskThreads() - 1;
        g_TS.AddPinnedTask( &runPinnedTaskLoopTasks );
    
        PretendDoFileIO pretendDoFileIO;
        pretendDoFileIO.threadNum = runPinnedTaskLoopTasks.threadNum;
        g_TS.AddPinnedTask( &pretendDoFileIO );
    
        g_TS.WaitforAllAndShutdown();
    
        return 0;
    }
  12. Use C++ 11 lambdas for tasks

    master

    You can create tasks using enki::TaskSet with a lambda instead of defining a struct. The lambda signature must accept enki::TaskSetPartition range_ and uint32_t threadnum_.

    #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_  ) {
             // do something here
          }  );
    
       g_TS.AddTaskSetToPipe( &task );
       g_TS.WaitforTask( &task );
       return 0;
    }