Skip to content

pelicanmapping/weejobs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

34 Commits
 
 
 
 
 
 
 
 

Repository files navigation

weejobs

Weejobs is a simple header-only C++17 API for scheduling asynchronous tasks. It consists of a single header file and has no dependencies aside from the STL.

Features include:

  • Thread pools with configurable sizes
  • Futures & continuations
  • Work stealing
  • Job prioritization
  • Automatic cancelation
  • Header-only C++17

Setup

The main API is the jobs::runtime object. In this document, we will use a runtime instance which has been declared like so:

jobs::runtime runtime;

Using a Global Singleton

If you would rather use a global singleton instead, you need to do two things. First, define WEEJOBS_USE_SINGLETON before including weejobs.h. Second, in a source file, declare the WEEJOBS_INSTANCE macro:

#define WEEJOBS_USE_SINGLETON
#include <weejobs.h>
WEEJOBS_INSTANCE;

Then you can access the singleton jobs::runtime object from anywhere by calling jobs::instance().

Namespace

The default namespace for weejobs is jobs. You can customize that by setting a macro before including the header:

#define WEEJOBS_NAMESPACE my_jobs_namespace
#include <weejobs.h>

Shared Module

If you plan to use the weejobs API from a shared module (DLL) under Windows, you will also need to set the export directive:

#define WEEJOBS_EXPORT MY_PROJECT_EXPORT

Examples - Running Jobs

In these examples, runtime is your jobs::runtime object, OR the global singleton returned from jobs::instance() if so configured.

Scheduling a job

The simplest usage is to spawn a job with no return value (fire and forget):

    auto job = []() { std::cout << "Hello, world!" << std::endl; };
    runtime.dispatch(job);

Getting a future result

You can also spawn a job and get a "future result." In this case, the job's signature should take a jobs::cancelable& argument so it can check for cancelation (more on that later) and the job must return a value.

    auto job = [](jobs::cancelable&) { return 7; };
    jobs::future<int> result = runtime.dispatch(job);
    
    // later...
    if (result.available())
       std::cout << "Result = " << result.value() << std::endl;
    else if (result.canceled())
       std::cout << "Job was canceled" << std::endl;
    else
       // still running.... come back later?

Hold on to that future result! If there are no references to the future result, the system will try to cancel the job before it runs. Hint: future objects are fully assignable by value. There is no need to store pointers or them; they are internally reference-counted. As long as at least one copy of the future exists, the corresponding job will be allowed to run.

Waiting for a job to finish

Use join() to block until a job completes:

    auto job = [url](jobs::cancelable&) {
        return fetch_data_from_network(url);
    };
    auto result = runtime.dispatch(job);
    ...
    auto value = result.join();

Chaining jobs together

To automatically start a new job when another job completes, you can use the then construct. In the example below, the result of job1 (an integer) gets passed to job2 as soon as the result of job1 becomes available:

    auto job1 = [](jobs::cancelable& c)
    {
       return 7;
    };
    
    auto job2 = [](const int& input, jobs::cancelable& c)
    {
        return input * 2;
    };
    
    auto result1 = runtime.dispatch(job1);
    auto result2 = result1.then_dispatch<int>(runtime, job2);

Checking for cancelation

Here's how to check for cancelation inside a job. If you dispatch a job, and the future goes out of scope, jobs::cancelable::canceled() will return true and the job can exit early instead of wasting resources.

   auto job = [url](jobs::cancelable& state) {
       std::string data;
       if (!state.canceled())
           data = fetch_data_from_network(url);
       return data;
   };

   auto result = runtime.dispatch(job);
   
   // if "result" goes out of scope, `state.canceled()` in the job will return true
   // AND `result.canceled()` will be true.

Examples - Controlling Jobs

Job pools

You can dispatch a job to a specific job pool. A job pool is just a collection of dedicated threads with a priority queue. Use runtime::get_pool to get or create a job pool. You can use set_concurrency to allocate the number of threads the pool should use (the default is 2).

    auto my_pool = runtime.get_pool("My Job Pool");
    my_pool.set_concurrency(4);
    
    auto job = []() { std::cout << "Hello, world!" << std::endl; };

    jobs::context context;
    context.pool = my_pool;

    runtime.dispatch(job, context);

The default job pool in unnamed, and you can access it with runtime::get_pool().

(Note: if work-stealing is enabled, a job may end up running in a different pool if the designated pool is too busy. See the section on work-stealing for more information.)

Prioritizing jobs

Each job pool has a priority queue. You can use a lambda function in your context to specify the priority of a job. Since it uses a lambda function, the priorty can change between the time you dispatch the job and the time it gets pulled from the queue and executed:

    jobs::context context;
    context.priority = []() { return calculate_priority(); };
    
    runtime.dispatch(job, context);

Automatic cancelation

When you get a future from dispatching a job, that future is your ticket to getting an eventual return value. If you discard that future before the job starts executing, the job may not run at all. This is called automatic cancelation and is the default behavior. You can disable this if you wish, forcing all dispatched jobs to run:

    jobs::context context;
    context.can_cancel = false;

    auto result = runtime.dispatch(job1, context);
    // now job1 will still spawn even if 'result' goes out of scope.

Grouping jobs

You can group jobs together. This lets you dispatch a number of jobs and then wait for all of them to finish before continuing:

    jobs::context context;
    context.group = jobs::jobgroup::create();
    
    runtime.dispatch(job1, context);
    runtime.dispatch(job2, context);
    runtime.dispatch(job3, context);

    // block until all 3 jobs are finished
    group.join();

Work-stealing

Warning: this feature is experimental. When you dispatch a job, you can ask it to run in a specific job pool. A job pool will always try to pull jobs from its own queue of pending tasks. When a job pool has no pending jobs its threads will sit idle. However, if you enable work-stealing, a job pool may steal jobs from other pools in order to better distribute the load.

You can enable work-stealing by calling this method:

    runtime.set_allow_work_stealing(true);

You can also disable work-stealing for individual job pools. For example, if you have a job pool that you want dedicated to a particular type of task, you can tell it never to steal work from other pools:

    auto pool = runtime.get_pool("my_dedicated_pool");
    pool->set_can_steal_work(false);

License

Weejobs is distributed under the MIT license. Weejobs is Copyright 2026 Pelican Mapping.

About

C++ Job Scheduler

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages