Executor
Contents
After you create a task dependency graph, you need to submit it to threads for execution. In this chapter, we will show you how to execute a task dependency graph.
Create an Executor
To execute a taskflow, you need to create an executor of type tf::N
worker threads. The default value is std::
tf::Executor executor1; // create an executor with the number of workers // equal to std::thread::hardware_concurrency tf::Executor executor2(4); // create an executor of 4 worker threads
An executor can be reused to execute multiple taskflows. In most workloads, you may need only one executor to run multiple taskflows where each taskflow represents a part of a parallel decomposition.
Execute a Taskflow
tf::run_*
methods, tf::
1: // Declare an executor and a taskflow 2: tf::Executor executor; 3: tf::Taskflow taskflow; 4: 5: // Add three tasks into the taskflow 6: tf::Task A = taskflow.emplace([] () { std::cout << "This is TaskA\n"; }); 7: tf::Task B = taskflow.emplace([] () { std::cout << "This is TaskB\n"; }); 8: tf::Task C = taskflow.emplace([] () { std::cout << "This is TaskC\n"; }); 9: 10: // Build precedence between tasks 11: A.precede(B, C); 12: 13: tf::Future<void> fu = executor.run(taskflow); 14: fu.wait(); // block until the execution completes 15: 16: executor.run(taskflow, [](){ std::cout << "end of 1 run"; }).wait(); 17: executor.run_n(taskflow, 4); 18: executor.wait_for_all(); // block until all associated executions finish 19: executor.run_n(taskflow, 4, [](){ std::cout << "end of 4 runs"; }).wait(); 20: executor.run_until(taskflow, [cnt=0] () mutable { return ++cnt == 10; });
Debrief:
- Lines 6-8 create a taskflow of three tasks A, B, and C
- Lines 13-14 run the taskflow once and wait for completion
- Line 16 runs the taskflow once with a callback to invoke when the execution finishes
- Lines 17-18 run the taskflow four times and use tf::
Executor:: wait_for_all to wait for completion - Line 19 runs the taskflow four times and invokes a callback at the end of the forth execution
- Line 20 keeps running the taskflow until the predicate returns true
Issuing multiple runs on the same taskflow will automatically synchronize to a sequential chain of executions in the order of run calls.
executor.run(taskflow); // execution 1 executor.run_n(taskflow, 10); // execution 2 executor.run(taskflow); // execution 3 executor.wait_for_all(); // execution 1 -> execution 2 -> execution 3
tf::Executor executor; // create an executor // create a taskflow whose lifetime is restricted by the scope { tf::Taskflow taskflow; // add tasks to the taskflow // ... // run the taskflow executor.run(f); } // leaving the scope will destroy taskflow while it is running, // resulting in undefined behavior
Similarly, you should avoid touching a taskflow while it is running.
tf::Taskflow taskflow; // Add tasks into the taskflow // ... // Declare an executor tf::Executor executor; tf::Future<void> future = taskflow.run(f); // non-blocking return // alter the taskflow while running leads to undefined behavior f.emplace([](){ std::cout << "Add a new task\n"; });
You must always keep a taskflow alive and must not modify it while it is being run by an executor.
Touch an Executor from Multiple Threads
All run_*
methods are thread-safe. You can have multiple threads call these methods from an executor to run different taskflows. However, the order which taskflow runs first is non-deterministic and is up to the runtime.
1: tf::Executor executor; 2: 3: for(int i=0; i<10; ++i) { 4: std::thread([i, &](){ 5: // ... modify my taskflow 6: executor.run(taskflows[i]); // run my taskflow 7: }).detach(); 8: }
Observe Thread Activities
You can observe thread activities in an executor when a worker thread participates in executing a task and leaves the execution using tf::
class ObserverInterface { virtual ~ObserverInterface() = default; virtual void set_up(size_t num_workers) = 0; virtual void on_entry(size_t worker_id, tf::TaskView task_view) = 0; virtual void on_exit(size_t worker_id, tf::TaskView task_view) = 0; };
There are three methods you must define in your derived class, tf::
You can associate an executor with one or multiple observers (though one is common) using tf::
#include <taskflow/taskflow.hpp> struct MyObserver : public tf::ObserverInterface { MyObserver(const std::string& name) { std::cout << "constructing observer " << name << '\n'; } void set_up(size_t num_workers) override final { std::cout << "setting up observer with " << num_workers << " workers\n"; } void on_entry(size_t w, tf::TaskView tv) override final { std::ostringstream oss; oss << "worker " << w << " ready to run " << tv.name() << '\n'; std::cout << oss.str(); } void on_exit(size_t w, tf::TaskView tv) override final { std::ostringstream oss; oss << "worker " << w << " finished running " << tv.name() << '\n'; std::cout << oss.str(); } }; int main(){ tf::Executor executor(4); // Create a taskflow of eight tasks tf::Taskflow taskflow; auto A = taskflow.emplace([] () { std::cout << "1\n"; }).name("A"); auto B = taskflow.emplace([] () { std::cout << "2\n"; }).name("B"); auto C = taskflow.emplace([] () { std::cout << "3\n"; }).name("C"); auto D = taskflow.emplace([] () { std::cout << "4\n"; }).name("D"); auto E = taskflow.emplace([] () { std::cout << "5\n"; }).name("E"); auto F = taskflow.emplace([] () { std::cout << "6\n"; }).name("F"); auto G = taskflow.emplace([] () { std::cout << "7\n"; }).name("G"); auto H = taskflow.emplace([] () { std::cout << "8\n"; }).name("H"); // create an observer std::shared_ptr<MyObserver> observer = executor.make_observer<MyObserver>( "MyObserver" ); // run the taskflow executor.run(taskflow).get(); // remove the observer (optional) executor.remove_observer(std::move(observer)); return 0; }
The above code produces the following output:
constructing observer MyObserver setting up observer with 4 workers worker 2 ready to run A 1 worker 2 finished running A worker 2 ready to run B 2 worker 1 ready to run C worker 2 finished running B 3 worker 2 ready to run D worker 3 ready to run E worker 1 finished running C 4 5 worker 1 ready to run F worker 2 finished running D worker 3 finished running E 6 worker 2 ready to run G worker 3 ready to run H worker 1 finished running F 7 8 worker 2 finished running G worker 3 finished running H
It is expected each line of std::