【发布时间】:2017-01-10 15:51:38
【问题描述】:
这个问题通过一个简化的例子更容易解释(因为我的实际情况远非“最小”):给定一个...
template <typename T>
void post_in_thread_pool(T&& f)
...函数模板,我想创建一个具有树状递归结构的并行异步算法。我将使用std::count_if 作为占位符来编写下面的结构示例。我要使用的策略如下:
如果我检查的范围的长度小于
64,我将回退到连续的std::count_if函数。 (0)-
如果它大于或等于
64,我将在线程池中生成一个在范围左半边递归的作业,并在当前线程上计算范围的右半边。 (1)我将使用原子共享
int来“等待”计算两半。 (2)我将使用原子共享
int来累积部分结果。 (3)
简化代码:
auto async_count_if(auto begin, auto end, auto predicate, auto continuation)
{
// (0) Base case:
if(end - begin < 64)
{
continuation(std::count_if(begin, end, predicate));
return;
}
// (1) Recursive case:
auto counter = make_shared<atomic<int>>(2); // (2)
auto cleanup = [=, accumulator = make_shared<atomic<int>>(0) /*(3)*/]
(int partial_result)
{
*accumulator += partial_result;
if(--*counter == 0)
{
continuation(*accumulator);
}
};
const auto mid = std::next(i_begin, sz / 2);
post_in_thread_pool([=]
{
async_count_if(i_begin, mid, predicate, cleanup);
});
async_count_if(mid, i_end, predicate, cleanup);
}
代码可以如下使用:
std::vector<int> v(512);
std::iota(std::begin(v), std::end(v), 0);
async_count_if{}(std::begin(v), std::end(v),
/* predicate */ [](auto x){ return x < 256; },
/* continuation */ [](auto res){ std::cout << res << std::endl; });
上面代码中的问题是auto cleanup。由于auto 将被推导出为cleanup lambda 的每个实例化的唯一类型,并且由于cleanup 按值捕获cont...由于以下原因,将在编译时计算无限大的嵌套lambda 类型递归,导致如下错误:
致命错误:递归模板实例化超出最大深度 1024
从概念上讲,您可以大致认为构建的类型是这样的:
cont // user-provided continuation
cleanup0<cont> // recursive step 0
cleanup1<cleanup0<cont>> // recursive step 1
cleanup2<cleanup1<cleanup0<cont>>> // recursive step 2
// ...
(!):记住async_count_if 只是一个例子,以展示我真实情况的“树状”递归结构。我知道异步count_if 可以通过单个原子计数器和sz / 64 任务轻松实现。
我想避免错误,尽量减少任何可能的运行时间或内存开销。
-
一种可能的解决方案是使用
std::function<void(int)> cleanup,它允许代码正确编译和运行,但会产生次优汇编并引入额外的动态分配。 wandbox example- 另一种可能的解决方案是使用
std::size_t模板参数 + 特化来人为地限制async_count_if::operator()的递归深度 - 不幸的是,这会使二进制大小膨胀并且非常不雅。
- 另一种可能的解决方案是使用
困扰我的是,当我调用async_count_if 时,我知道范围的大小:它是std::distance(i_begin, i_end)。如果我知道范围的大小,我还可以推断出所需的计数器和延续的数量:(2^k - 1),其中k 是递归树的深度。
因此,我认为应该有一种方法可以在async_count_if的第一次调用中预先计算“控制结构”,并通过引用将其传递给递归调用。这个“控制结构”可以为(2^k - 1) 原子计数器和(2^k - 1) 清理/延续函数包含足够的空间。
不幸的是,我找不到一个干净的方法来实现这一点,并决定在这里发布一个问题,因为在开发异步并行递归算法时,这个问题似乎应该很常见。
在不引入不必要开销的情况下,有什么优雅的方式来处理这个问题?
【问题讨论】:
-
我肯定遗漏了一些非常明显的东西,但是为什么你需要多个计数器和结构?您可以预先计算迭代的总计数器(如果我没记错的话)并在所有迭代中与累加器一起共享它
-
@dyp:你能发一个例子吗?即使我在运行时在两个延续之间做出决定,两者的完整类型也会被递归推导出来。
-
@VittorioRomeo 啊抱歉,我不得不画这个是为了看到树中的每个节点都需要 log(N)*state 才能继续推送到线程池。
标签: c++ multithreading asynchronous recursion c++14