I'm trying to traverse a tree-like structure from the bottom-up, and in parallel. Due to some other constraints I also have to allow for thread counts that may be lower than the width of a given set of child nodes. Each parent node also requires all child nodes to be visited before it itself can be visited. I've come up with an implementation that seems to work, but I'd like to get your opinion on it, and know whether there are some obvious issues with the logic it depends on. Note that I'm using the dp thread pool library for the thread pool itself.
Algorithm
#include <iostream>
#include <map>
#include <queue>
class node {
public:
node(int value)
: m_value(value) {}
const std::vector<node*>& get_children() const {
return m_children;
}
void add_child(node* child) {
m_children.push_back(child);
}
void print() const {
std::cout << m_value << '\n';
}
private:
int m_value;
std::vector<node*> m_children;
};
// get nodes in breadth-first order
std::vector<node*> get_bfs_ordered_nodes(node* root) {
std::vector<node*> order;
std::queue<node*> q;
q.push(root);
while (!q.empty()) {
node* current_node = q.front();
q.pop();
order.push_back(current_node);
for (node* child : current_node->get_children()) {
q.push(child);
}
}
return order;
}
void parallel_bottom_up_traversal(node* root, dp::thread_pool<>& pool) {
// get nodes in breadth-first order
std::vector<node*> nodes = get_bfs_ordered_nodes(root);
std::map<node*, std::shared_future<void>> node_futures;
for (auto it = nodes.rbegin(); it != nodes.rend(); ++it) {
node* current_node = *it;
// store futures of children
std::vector<std::shared_future<void>> child_futures;
for (node* child : current_node->get_children()) {
child_futures.push_back(node_futures[child]);
}
// create a task for the node and add it to the thread pool
const auto future = pool.enqueue([current_node, child_futures]() {
// wait for all children to be processed
for (auto& fut : child_futures) {
fut.get();
}
// process the node
current_node->print();
});
// store the future for this node
node_futures[current_node] = future;
}
// wait for the root to be processed
node_futures[root].get();
}
Usage
int main() {
dp::thread_pool pool(2);
node* root = new node(0);
node* child1 = new node(1);
node* child2 = new node(2);
node* child3 = new node(3);
node* child4 = new node(4);
node* child5 = new node(5);
root->add_child(child1);
root->add_child(child2);
child1->add_child(child3);
child3->add_child(child4);
child3->add_child(child5);
parallel_bottom_up_traversal(root, pool);
delete child5;
delete child4;
delete child3;
delete child2;
delete child1;
delete root;
return 0;
}
The tree represented in my usage example can be seen below:
The traversal order is as follows:
5
4
3
2
1