简单使用
tf::Executor 提供了异步执行Task的操作tf::Executor::async,并返回Future,用于保留该函数调用的结果。
#include <taskflow/taskflow.hpp>
void print_str(char const* str) {
std::cout << str << std::endl;
}
int main() {
tf::Executor executor;
std::future<int> future = executor.async([](){
print_str("async task");
return 1;
});
print_str(std::to_string(future.get()).c_str());
}
如果不需要关系返回值,推荐使用Executor::silent_async ,相较于tf::Executor::async,减少了一些状态共享的开销:
#include <taskflow/taskflow.hpp>
int main() {
tf::Executor executor;
executor.silent_async([](){
print_str("async task");
return 1;
});
// sleep 1s
std::this_thread::sleep_for(std::chrono::seconds(1));
}
调度器会自动检测异步任务是从外部线程还是工作线程提交的,并使用work stealing来安排其执行:
#include <taskflow/taskflow.hpp>
void print_str(char const* str) {
std::cout << str << std::endl;
}
int main() {
tf::Executor executor;
tf::Taskflow taskflow;
tf::Task my_task = taskflow.emplace(
[&executor](){
// 从 my_task 内部启动一个异步任务
executor.async([&executor](){
// 从 一个异步任务内部启动一个异步任务,可能执行在其他worker线程中
executor.async([&](){});
});
}
);
executor.run(taskflow);
executor.wait_for_all(); // 等待所以任务执行结束
}
由分析工具,可以看到,这个嵌套的异步任务分别执行在不同的线程中:
从executor创建的异步任务不属于任何任务流。异步任务的生命周期由创建任务的executor自动管理。
Subflow 创建异步任务
Subflow也可以创建自己的异步任务,Taskflow保证这些异步任务在join前结束:
#include <cassert>
#include <taskflow/taskflow.hpp>
void print_str(char const* str) {
std::cout << str << std::endl;
}
int main() {
tf::Executor executor;
tf::Taskflow taskflow;
std::atomic<int> counter{0};
taskflow.emplace(
[&executor,&counter](tf::Subflow& sbf){
// 创建100个异步任务,注意,这里是Subflow创建的
for(int i = 0; i < 100; i++) {
sbf.silent_async([&counter](){
++counter;
});
}
// 显式调用join,这100个任务将会在这里全部完成
sbf.join();
assert(counter == 100); // 验证这里的所有异步任务均回收
}
);
executor.run(taskflow).wait();
taskflow.dump(std::cout);
}
注意: 只可以在一个joinable 的Subflow中创建异步任务,从Detach的Subflow启动异步任务会导致未定义的行为。
为异步任务添加依赖(动态任务图)
动态任务图的优势:
- 探索任务图并行性:在一些应用场景中,任务之间的依赖关系可能不是事先完全确定的,而是会根据程序的运行状态动态变化。通过动态创建任务图,可以根据实时的控制流来调整任务的执行顺序和并行度,从而实现更高效的并行处理。
- 重叠任务图创建时间与任务执行时间:在传统的任务图构建方法中,通常需要先构建完整的任务图,然后再开始执行任务。但在某些情况下,我们可以在创建任务图的同时执行任务,这样可以减少等待任务图完全构建完成的时间,提高整体的执行效率。
创建一个动态任务图
当应用程序中无法构建任务图的运行模型时,可以使用tf::Executor::dependent_async和tf::Executor::silent_dependent_async来动态创建任务图。这种类型的并行性也被称为实时任务图并行性:
#include <taskflow/taskflow.hpp>
void print_str(char const* str) {
std::cout << str << std::endl;
}
int main() {
tf::Executor executor;
// silent_dependent_async 返回一个AsyncTask对象,用于构建后续的任务依赖
tf::AsyncTask A = executor.silent_dependent_async([&](){
print_str("Task A");
});
// 构建任务依赖,B依赖A
tf::AsyncTask B = executor.silent_dependent_async([&](){
print_str("Task B");
}, A);
// 构建任务依赖,C依赖A
tf::AsyncTask C = executor.silent_dependent_async([&](){
print_str("Task C");
}, A);
// dependent_async 返回std::pair<tf::AsyncTask, std::future<void>>
// D依赖B和C, fuD 来等待任务D执行完成
auto [D, fuD] = executor.dependent_async([&](){
print_str("Task D");
}, B, C);
fuD.get(); // 等待D执行完成
}
tf::Executor::dependent_async和tf::Executor::silent_dependent_async都创建一个类型为tf::AsyncTask的任务,以异步运行给定的函数。此外,tf::Executor::dependent_async返回一个std::future,最终保存执行结果。当从两个调用返回时,executor安排worker在满足其依赖项时运行任务。也就是说,任务执行与创建任务图同时进行,而不是如静态图一样,构建完整个图后再执行。
图左展示的是静态图的执行逻辑,先建图,再执行,而图右展示的是动态图的执行逻辑,边建图,边执行。
注意:动态图的构建只允许将当前任务的依赖项与之前创建的任务相关联,因此用户需要保证拓扑顺序的正确性。
另外,如果整个动态图均使用silent_dependent_async构建,也可以使用executor.wait_for_all()来保证所有异步任务均会完成。
int main() {
tf::Executor executor;
tf::AsyncTask A = executor.silent_dependent_async([](){ printf("A\n"); });
tf::AsyncTask B = executor.silent_dependent_async([](){ printf("B\n"); }, A);
tf::AsyncTask C = executor.silent_dependent_async([](){ printf("C\n"); }, A);
tf::AsyncTask D = executor.silent_dependent_async([](){ printf("D\n"); }, B, C);
executor.wait_for_all();
}
如果一个任务的依赖是依靠运行时的结果得出的,而不能显式指定,可以使用dependent_async和silent_dependent_async的重载,来动态确定:
#include <cstdio>
#include <taskflow/taskflow.hpp>
void print_str(char const* str) {
std::cout << str << std::endl;
}
int main() {
tf::Executor executor;
std::vector<tf::AsyncTask> dependents; // 存放动态依赖
int n = 0;
printf("input n:");
scanf("%d",&n); // 动态输入依赖数
for(int i = 0; i < n; i++) {
dependents.push_back(
executor.silent_dependent_async([i](){
char name[16];
std::sprintf(name,"Task:%c\n", 'A'+i);
print_str(name);
})
); // 创建异步任务,并添加到dependents中
}
executor.silent_dependent_async([](){
print_str("All dependents completed\n");
}, dependents.begin(), dependents.end()); // 创建异步任务,等待所有依赖完成
executor.wait_for_all();
}
因为printf不是线程安全的,所以打印可能并不符合预期,但是执行顺序是正确的。
下图是n=8时的性能分析图:
可以看到,虽然依赖是运行时才确定的,但是任务的调度仍然保持着较高的并行度。
tf::AsyncTask是一个轻量级句柄,保留由executor创建的依赖异步任务的共享所有权。这种共享所有权确保异步任务在将其添加到另一个异步任务的依赖项列表中时保持活动状态,从而避免了经典的ABA问题。
// main thread retains shared ownership of async task A
tf::AsyncTask A = executor.silent_dependent_async([](){});
// task A remains alive (i.e., at least one ref count by the main thread)
// when being added to the dependency list of async task B
tf::AsyncTask B = executor.silent_dependent_async([](){}, A);
注意: executor.silent_dependent_async和executor.dependent_async是线程安全的,即可以在多线程环境下安全地添加任务,只要拓扑上的任务在逻辑上是正确的。
同时,tf::AsyncTask也包含类似std::future一样的is_done方法,可重入future。
// create a dependent async task that returns 100
auto [task, fu] = executor.dependent_async([](){ return 100; });
// loops until the dependent async task completes
while(!task.is_done());
assert(fu.get() == 100);
动态任务图计算斐波那契数列
有了动态任务图,就可以很方便地将一些递归任务拆解成并行任务:
#include <cstdio>
#include <taskflow/taskflow.hpp>
#include <tuple>
void print_str(char const* str) {
std::cout << str << std::endl;
}
int main() {
tf::Executor executor;
int n = 0;
printf("input n:");
scanf("%d",&n); // 动态输入依赖数
std::function<int(int)> fibonacci;
fibonacci = [&](int N){
if(N < 2) {
return N;
}
auto [t1, fu1] = executor.dependent_async(std::bind(fibonacci, N-1));
auto [t2, fu2] = executor.dependent_async(std::bind(fibonacci, N-2));
// worker内等待必须是corun_until,否则会死锁
executor.corun_until([&](){ return t1.is_done() && t2.is_done(); });
return fu1.get() + fu2.get();
};
auto [task, fib11] = executor.dependent_async(std::bind(fibonacci, n));
std::cout<<"fibonacci("<<n<<") = "<<fib11.get()<<std::endl;
}
当n=5时,性能分析图如下: