基本上,我需要从数千个并发读取的文件中填充数std::map
个数百万个关键条目(订单数量为5000万或更少)。这些键指向的值将从堆(std::bitset
类型)中分配。
std::map<std::string,std::bitset<BITSET_SIZE>*> my_map;
我的第一个担忧是:我不想要两个线程(首先检查是否有一个 key存在,如果不存在,则)从堆中分配空间。 因为我只能拿一个指针而其他分配会 导致内存泄漏,因为我无法跟踪它们。
//count should be thread-safe, since it's defined as const in <map> header file
if(my_map.count(key) == 0){
//some other thread may have initialized the key in the mean time
my_map[key] = new std::bitset<BITSET_SIZE>();
//Now I will lose the pointer to previous heap allocation from other thread
}
一种解决方案是使用一些互斥机制
boost::unique_lock
或boost::shared_lock
的智能组合
并且为了表现而提升:: unique_lock,我很乐意听到你的想法。
std::bitset
)。为此,我认为应该没有任何问题,因为根据我的设置,保证没有两个线程同时在同一个键上工作。 (任何线程都不会为my_map的键添加新键或从基础树结构中删除键)答案 0 :(得分:1)
const
对std::
容器(如map
)的访问也可以保证在不同的线程中合法。
任何非同步的非const
访问都会使任何其他访问(const
或非 - const
)非法(程序行为变得未定义)。
某些操作不 const
,但就同步而言const
。例如,非const find
被视为“const
”,[]
上的vector
也被视为<{1}}。
[]
不是const
,不会被视为const
。我不确定不创建元素的[]
是否被视为const
,我将不得不仔细检查标准。并且由于find
存在并且用明确定义的语义解决了同样的问题,所以在任何情况下我都不会在代码中使用它。
const
并不意味着线程安全,它意味着与其他const
操作的线程安全。线程安全是两位或更多位代码之间的关系,它不是绝对的。因此,在其他人插入时调用.count
是不合法的。
一般来说,共享是线程安全的祸根。解决这个问题的更简单方法是让每个“任务”都有自己的map
来处理。然后将这些map
合并回主映射。
合并发生的复杂程度和频率成为特定应用程序和复制程度的问题。
最简单的解决方案是:
std::map<std::string, std::unique_ptr<std::bitset<BITSET_SIZE>>>
parse_file( some_file_handle );
然后
std::map<std::string, std::unique_ptr<std::bitset<BITSET_SIZE>>>
parse_files( gsl::span<some_file_handle> handles ) {
if (handles.size()==0) return {};
if (handles.size()==1) return parse_file(handles.front());
auto lhs = parse_files( handles.first(handles.size()/2) );
auto rhs = parse_files( handles.last(handles.size()-handles.size()/2) );
return merge_maps(std::move(lhs), std::move(rhs));
}
为我们提供了单线程版本。我们通过以下方式多线程:
std::map<std::string, std::unique_ptr<std::bitset<BITSET_SIZE>>>
parse_files( gsl::span<some_file_handle> handles, executor exec ) {
if (handles.size()==0) return {};
if (handles.size()==1) return parse_file(handles.front());
auto lhs = exec( [handles]{parse_files(handles.first(handles.size()/2) )} );
auto rhs = exec( [handles]{parse_files(handles.last(handles.size()-handles.size()/2) )} );
auto retval = exec( [lhs=std::move(lhs], rhs=std::move[rhs]]()mutable{
return merge_maps(std::move(lhs).get(), std::move(rhs).get() );
}
return std::move(retval).get();
}
其中executor
采用T()
类型的对象并返回future<T>
。天真的执行器只是运行该函数并返回一个准备好的未来。一位发烧友执行者使用std::async
来解决它。一个更加漂亮的人使用一个线程池,等待时使用等待的线程来运行任务(如果它还没有运行)。
现在,像ppl或Intel的TBB这样的并发库提供了很容易实现这一目标的方法。
答案 1 :(得分:0)
第一件事 - const函数不是线程安全的。考虑:
struct A {
int q;
void set(int qq) { q = qq; }
int get() const { return q; }
};
get()不是线程安全的 - 可能会调用另一个线程集,这会修改q。如果你想要线程安全,你必须使用原子结构锁定或更新(还有其他多线程问题,如果你不锁定/使用原子会发生这种情况,但那些超出你的问题范围 - 你绝对需要那些!)。
现在解决方案: 由于您需要明确地同步对地图结构的访问权限,因此您的问题不会出现问题:
std::mutex m; // since c++11
...
{
std::lock_guard _l(m); // since c++11
if (!my_map.emplace(key, bitset_ptr).second)
delete bitset_ptr;
}
这将使用键键和值 bitset_ptr 将元素插入 my_map ,但前提是它不存在。它将返回两个元素的元组 - 首先是迭代器到创建的元素和先前存在的,第二个是布尔标志,如果创建了元素则为true,如果之前存在则为false。所以你只需删除 bitset_ptr ,如果元素已经被插入并且没有内存泄漏。请注意,由于同步量的原因,这可能会很慢。
更新: 你明显需要使用mutex m同步任何访问my_map,只要你在多个线程中不断更新。
UPDATE2: op尝试了最简单的解决方案,发现它不够快。让我们走得更远。 (注意:最佳的行动方案是测量应用程序的性能并找到代码花费大部分时间的地方,但我不能这样做;))。很少有“明显的”(读:可能)减速原因:
我会假设,您不能轻易(廉价地)确定单个文件中的元素数量。首先是代码:
using etype = pair<string, bitset<N>*>;
vector<etype> all_elements;
mutex all_elements_mutex;
void parse_single_file_in_thread(...) {
vector<etype> tmp;
for(auto element : parse_element_from_file())
tmp.push_back(move(element));
lock_guard _l(all_elements_mutex);
for(auto &a : tmp) all_elements.push_back(move(a));
}
map<string, bitset<N>*> parse_all_files() {
// create threads, parse files in them and wait for them to finish
std::sort(all_elements.begin(), all_elements.end(),
[](const etype &a, const etype &b) { return a.first < b.first; });
map<string, bitset<N>*> tmp;
for(auto &a : all_elements) if (!tmp.insert(tmp.end(), etype(move(a.first), a.second)).second) delete a.second;
all_elements.clear();
return tmp;
}
它的作用很少: - 首先将键插入向量(忽略检查重复项),稍后将对其进行排序并插入带有放置提示的映射(它们已排序,因此我们总是知道插入下一个元素的正确位置 - 地图的结尾),这是比直接插入地图要快得多 - 每个文件的项目首先被放入它自己的向量中并在解析整个文件后移动到全局文件中,这样可以最小化锁定
这应该足以提高性能。接下来的事情是用其他东西替换字符串以避免这么多的字符串到字符串排序比较。但这很容易超出范围。 ;)
注意:我已经从内存中编写了完整的代码,因此可能无法编译,可能需要c ++ 17。