I have an unordered_map<int, float> localCostMap describing the costs (float) between node IDs (int). The calculation of the second value is quite complex, but due to the structure of graph (directed, acyclic, up to two parent nodes) I can save many calculations by pooling the maps for each node into another map like so:
unordered_map<int, shared_ptr<unordered_map<int, float>>> pooledMaps.
Once the values are written (into localCostMap), they do not get updated again, but the calculations required the map entries of the connected nodes, which may leed to lock-ups.
How can I make it so that I can read the values stored in the inner map while also safely adding new entries (e.g. { 3, 1.23 }? I'm new to multithreading and have tried to search for solutions, but the only results I got were older, despite reading that multithreading has improved much, particularly in C++20.
Thank you in advance!
Edit: As requested, here is a minimal working example. Of course, the full algorithm is more complex, considers the edge cases and enters them also for other applicable nodes (e.g. the result of comparing 5 & 7 also applies for 6 & 7).
// Example.h
#pragma once
#include <iostream>
#include <unordered_map>
#include <thread>
struct MyNode {
int id;
int leftNodeID;
int rightNodeID;
std::shared_ptr<std::unordered_map<int, float>> localCostMap; /* inherited from parent (left/right) nodes */
std::unordered_map<int, std::shared_ptr<std::unordered_map<int, float>>> pooledMaps;
MyNode() : id(0), leftNodeID(0), rightNodeID(0) { setLocalCostMap(); }
MyNode(int _id, int leftID, int rightID) :
id(_id), leftNodeID(leftID), rightNodeID(rightID) { setLocalCostMap(); }
void setLocalCostMap();
float calculateNodeCost(int otherNodeID);
};
// Example.cpp
#include "NodeMapMin.h"
MyNode nodes[8];
void MyNode::setLocalCostMap() {
if (leftNodeID == 0) { // rightNodeID also 0
localCostMap = std::make_shared<std::unordered_map<int, float>>();
}
else { // get map from connectednode if possible
auto poolmap = nodes[leftNodeID].pooledMaps.find(rightNodeID);
if (poolmap == nodes[leftNodeID].pooledMaps.end()) {
localCostMap = std::make_shared<std::unordered_map<int, float>>();
nodes[leftNodeID].pooledMaps.insert({ rightNodeID, localCostMap }); // [1] possible conflict
nodes[rightNodeID].pooledMaps.insert({ leftNodeID, localCostMap }); // [1] possible conflict
}
else { localCostMap = poolmap->second; }
}
}
float MyNode::calculateNodeCost(int otherNodeID) {
if (id > 0) {
std::cout << "calculateNodeCost for " << nodes[id].id << " and " << nodes[otherNodeID].id << std::endl;
}
float costs = -1.0f;
auto mapIterator = localCostMap->find(otherNodeID);
if (mapIterator == localCostMap->end()) {
if (id == otherNodeID) { // same node
std::cout << "return costs for " << id << " and " << otherNodeID << " (same node): " << 0.0f << std::endl;
return 0.0f;
}
else if (leftNodeID == 0 || nodes[otherNodeID].leftNodeID == 0) {
costs = ((float)(id + nodes[otherNodeID].id)) / 2;
std::cout << "calculated costs for " << id << " and " << otherNodeID << " (no connections): " << costs << std::endl;
}
else if (leftNodeID == nodes[otherNodeID].leftNodeID &&
rightNodeID == nodes[otherNodeID].rightNodeID) { // same connected nodes
costs = nodes[leftNodeID].calculateNodeCost(rightNodeID); // [2] possible conflict
std::cout << "return costs for " << id << " and " << otherNodeID << " (same connections): " << costs << std::endl;
return costs;
}
else {
costs = nodes[leftNodeID].calculateNodeCost(otherNodeID) +
nodes[rightNodeID].calculateNodeCost(otherNodeID) +
nodes[id].calculateNodeCost(nodes[otherNodeID].leftNodeID) +
nodes[id].calculateNodeCost(nodes[otherNodeID].rightNodeID); // [2] possible conflict
std::cout << "calculated costs for " << id << " and " << otherNodeID << ": " << costs << std::endl;
}
// [3] possible conflict
localCostMap->insert({ otherNodeID, costs });
nodes[otherNodeID].localCostMap->insert({ id, costs });
}
else {
costs = mapIterator->second;
std::cout << "found costs for " << id << " and " << otherNodeID << ": " << costs << std::endl;
}
return costs;
}
float getNodeCost(int node1, int node2) {
return nodes[node1].calculateNodeCost(node2);
}
int main()
{
nodes[0] = MyNode(0, 0, 0); // should not be used
nodes[1] = MyNode(1, 0, 0);
nodes[2] = MyNode(2, 0, 0);
nodes[3] = MyNode(3, 0, 0);
nodes[4] = MyNode(4, 0, 0);
nodes[5] = MyNode(5, 1, 2);
nodes[6] = MyNode(6, 1, 2);
nodes[7] = MyNode(7, 3, 4);
//getNodeCost(5, 7);
//getNodeCost(6, 7);
std::thread w1(getNodeCost, 5, 7);
std::thread w2(getNodeCost, 6, 7);
w1.join();
w2.join();
std::cout << "done";
}
I commented out the single-thread variant, but you can easily see the difference as the multi-threaded version already has more (unneccessary) comparisons.
As you can see, whenever two "noteworthy" comparisons take place, the result is added to localCostMap, which is normally derived from the connected two nodes. Thus, one insert is necessary for all nodes with these two connections (left & right).
I see at least 3 problematic point:
- When initializing the node and inserting the pooled maps for the connected nodes: if two nodes with the same connections were to be added at the same time, they would both want to create and add the maps for the connected nodes. [1]
- When calculating the values, another thread might already be doing it, thus leading to unneccessary calculations. [2]
- When inserting the results into
localCostMap(and by that also to the maps of the connected nodes). [3]