Cookbook » Task Group

A task group is a lightweight mechanism in Taskflow to spawn and manage a collection of asynchronous tasks cooperatively within a single executor. Task groups allow tasks to be executed recursively, asynchronously, or with dependencies, enabling efficient implementation of recursive parallel algorithms.

Create a %Task Group

A task group (tf::TaskGroup) is created from a worker in an executor using tf::Executor::task_group(). Since task groups rely on cooperative execution, they must be created inside a task that is already running on the executor. For example, the code below creates a task group from an asynchronous task:

tf::Executor executor;
executor.silent_async([&](){
  tf::TaskGroup tg = executor.task_group();
});

Internally, a task group is bound to the executor and the worker that creates it. This worker is referred to as the parent worker of the task group and is the only worker allowed to issue cooperative execution (tf::TaskGroup::corun) on that task group. Attempting to create a task group from a non-worker thread will result in an exception. This restriction ensures that task groups can safely participate in the executor's work-stealing loop and enables efficient cooperative execution while preserving the execution context required for recursion.

tf::Executor executor;
tf::TaskGroup tg = executor.task_group(); // throws

Submit Asynchronous Tasks with Cooperative Execution

tf::TaskGroup supports submitting asynchronous tasks that execute cooperatively with other workers in the same executor. All tasks submitted to a task group are logically grouped and can be explicitly synchronized using tf::TaskGroup::corun(). The task group provides four categories of asynchronous submission APIs:

Each variant serves a distinct purpose depending on whether you need, including a returned future, dependency ordering between tasks, etc. For instance, the code below creates 100 tasks using tf::TaskGroup::silent_async and one task using tf::TaskGroup::async, followed by a tf::TaskGroup::corun() to cooperatively execute all tasks in the task group until every task has completed:

executor.async([&](){
  tf::TaskGroup tg = executor.task_group();

  std::atomic<int> counter{0};
  
  // spawn 100 silent-async tasks (without future return)
  for(int i=0; i<100; i++) {
    tg.silent_async([&](){ counter++; });
  }
  
  // spawn one async task (with future return)
  auto fu = tg.async([](){ return 42; });
  
  // cooperatively run all tasks in the group
  tg.corun();
  
  assert(counter == 100);
  assert(fu.get() == 42);
});

If you need dependencies among async tasks, use tf::TaskGroup::dependent_async or tf::TaskGroup::silent_dependent_async. For instance, the task group below builds a dynamic task graph of three tasks, A, B, and C, where C runs after A and B.

executor.async([&](){
  auto tg = executor.task_group();
  tf::AsyncTask A = tg.silent_dependent_async([](){ printf("A\n"); });
  tf::AsyncTask B = tg.silent_dependent_async([](){ printf("B\n"); });
  tf::AsyncTask C = tg.silent_dependent_async([](){ printf("C\n"); }, A, B);
  tg.corun();
});

Cancel a %Task Group

You can mark a task group as cancelled to stop any not-yet-started tasks in the group from running. Tasks that are already running will continue to completion, but no new tasks belonging to the task group will be scheduled after cancellation. The example below demonstrates how tf::TaskGroup::cancel() prevents pending tasks in a task group from executing , while allowing already running tasks to complete cooperatively. The first set of tasks deliberately occupies all but one worker thread, ensuring that subsequently spawned tasks remain pending. After invoking tf::TaskGroup::cancel(), these pending tasks are never scheduled, even after the blocked workers are released. A final call to tf::TaskGroup::corun() synchronizes with all tasks in the group, guaranteeing safe completion and verifying that cancellation successfully suppresses task execution.

const size_t W = 12;  // must be >1 for this example to work
tf::Executor executor(W);

executor.async([&executor, W](){

  auto tg = executor.task_group();

  // deliberately block the other W-1 workers
  std::atomic<size_t> latch(0);
  for(size_t i=0; i<W-1; ++i) {
    tg.async([&](){
      ++latch;
      while(latch != 0);
    });
  }
  
  // wait until the other W-1 workers are blocked
  while(latch != W-1);

  // spawn other tasks which should never run after cancellation
  for(size_t i=0; i<100; ++i) {
    tg.async([&](){ throw std::runtime_error("this should never run"); });
  }
  
  // cancel the task group and unblock the other W-1 workers
  assert(tg.is_cancelled() == false);
  tg.cancel();
  assert(tg.is_cancelled() == true);
  latch = 0;

  tg.corun();
});

Note that cancellation is cooperative: tasks should not assume immediate termination. Users must still call tf::TaskGroup::corun() to synchronize with all spawned tasks and ensure safe completion or cancellation. Failing to do so results in undefined behavior.

Implement Recursive Task Parallelism

tf::TaskGroup is particularly well suited for implementing recursive task parallelism, where tasks dynamically spawn additional tasks during execution. Because task groups support cooperative execution via tf::TaskGroup::corun(), the worker thread can preserve its execution context across recursive calls. This design makes task groups a powerful choice for parallelizing recursive algorithms, such as divide-and-conquer, tree traversal, and dynamic programming. The example below demonstrates how to implement a parallel Fibonacci algorithm using a task group:

tf::Executor executor;

size_t fibonacci(size_t N) {
  if(N < 2) return N;

  size_t res1, res2;
  tf::TaskGroup tg = executor.task_group();

  tg.silent_async([N, &res1](){ res1 = fibonacci(N-1); });
  res2 = fibonacci(N-2);
  
  // cooperatively run tasks until all tasks spawned by `tg` complete
  tg.corun(); 
  return res1 + res2;
}

int main() {
  size_t N = 30, res;
  res = executor.async([](){ return fibonacci(30); }).get();
  std::cout << N << "-th Fibonacci number is " << res << '\n';
  return 0;
}

The function fibonacci spawns one recursive call as an asynchronous task and computes the other directly. Calling tf::TaskGroup::corun() ensures the asynchronous branch completes before the results are combined, while allowing the current worker to cooperatively execute spawned tasks and preserve its execution context.