C++ 如何填充c++;11标准::地图<;标准::字符串,标准::位集<;N>*&燃气轮机;并发(线程安全)而无内存泄漏?

C++ 如何填充c++;11标准::地图<;标准::字符串,标准::位集<;N>*&燃气轮机;并发(线程安全)而无内存泄漏?,c++,multithreading,c++11,memory-leaks,mutex,C++,Multithreading,C++11,Memory Leaks,Mutex,基本上,我需要用数千个并发读取的文件中的数百万个关键条目(大约5000万个)填充std::map。这些键指向的值将从堆中分配(std::bitsettype) std::map my\u map; 我的第一个顾虑是:我不想要两个线程(即首先检查 键存在,如果不存在,则从堆中分配空间。 因为我只能持有一个指针,而其他分配将 导致内存泄漏,因为我无法跟踪它们 //count should be thread-safe, since it's defined as const in <map&

基本上,我需要用数千个并发读取的文件中的数百万个关键条目(大约5000万个)填充
std::map
。这些键指向的值将从堆中分配(
std::bitset
type)

std::map my\u map;
  • 我的第一个顾虑是:我不想要两个线程(即首先检查 键存在,如果不存在,则从堆中分配空间。 因为我只能持有一个指针,而其他分配将 导致内存泄漏,因为我无法跟踪它们

    //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
    }
    
    //计数应该是线程安全的,因为它在头文件中定义为const
    if(my_map.count(key)==0){
    //其他一些线程可能同时初始化了密钥
    my_map[key]=new std::bitset();
    //现在,我将丢失指向其他线程上一次堆分配的指针
    }
    
    一种解决方案是使用某种互斥机制,如
    boost::unique_lock
    boost::shared_lock
    和boost::为了性能而提供独特的锁定,我很高兴听到您的想法

  • 想象我完成了第一部分,意思;在不并发内存泄漏的情况下初始化my_映射的键,任务的第二部分是并发操作值(
    std::bitset
    )。为此,我假设不会有任何问题,因为根据我的设置,保证不会有两个线程同时在同一个密钥上工作。(任何线程都不会为my_map的键添加新键或从基础树结构中删除键)

  • 首先,First-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;
    }
    
    这将使用key和value位集ptr将元素插入my_映射,但仅当它不存在时。它将返回两个元素的元组——第一个是已创建元素和以前存在的元素的迭代器,第二个是布尔标志,若元素已创建,则为true;若元素以前存在,则为false。所以,若元素已经被插入并且并没有内存泄漏,那个么只需删除位集_ptr。请注意,由于同步量的原因,这可能会很慢

    更新: 很明显,只要您在多个线程中不断更新,您就需要使用互斥对象m同步对my_map的任何访问

    更新2: op尝试了最简单的解决方案,但发现速度不够快。让我们深入一点。(注意:最理想的做法是测量应用程序的性能,并找出代码花费大部分时间的地方,但我不能这样做;))。减速的“明显”(即可能)原因很少:

    • 插入到映射中-插入到排序映射中具有O(ln n)运行时性能,实际上可能由于缓存不匹配而非常慢。平均而言,在一百万个元素映射中,您将需要比较10个不同的字符串,这些字符串(可能)位于完全不同的内存区域,从而一直从处理器缓存中相互清除
    • 从文件中读取-从多个文件中读取小数据块可能(也可能不是!)对总体速度不健康
    • 多个分配-一般来说,内存分配很慢。此外,大量的分配会增加内存碎片并减少局部性
    • 同步锁定-这对任何事情都不健康
    我假设,您无法轻松(廉价)确定单个文件中的元素数量以及元素总数。首先是守则:

    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;
    }
    
    使用etype=pair;
    向量所有_元素;
    互斥所有元素互斥;
    无效解析线程中的单个文件(…){
    向量tmp;
    for(自动元素:从\u文件()解析\u元素)
    tmp.推回(移动(元素));
    锁定保护(所有元素互斥);
    对于(auto&a:tmp)所有元素。向后推(move(a));
    }
    映射解析所有文件(){
    //创建线程,解析其中的文件并等待它们完成
    排序(所有元素.begin(),所有元素.end(),
    [](const-etype&a,const-etype&b){返回a.first
    它所做的事情很少: -首先将关键点插入向量(忽略重复项的检查),之后将对其进行排序,并使用放置提示将其插入地图(它们已排序,所以我们始终知道插入下一个元素的正确位置-地图的末尾),这比直接插入地图快得多 -每个文件的项首先放入它自己的向量中,并在解析整个文件后移动到全局向量中,这将最小化锁定

    这应该足以提高性能。下一件事是用其他东西替换字符串,以避免如此多的字符串到字符串排序比较。但这很容易超出范围


    注意:我从内存中编写了完整的代码,因此它可能无法编译,并且可能需要c++17。

    const
    std::
    容器(如
    map
    )的访问即使没有同步,也保证来自不同线程的访问是合法的

    任何不同步的非
    常量
    访问都会使任何其他访问(
    常量
    或非
    常量
    )非法(程序行为变得未定义)

    有些操作不是const,而是syn
    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;
    }
    
    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();
    }