如何填充c ++ 11 std :: map <std :: string,std :: bitset <n> *&gt;并发(线程安全)没有内存泄漏?

时间:2017-06-21 18:24:19

标签: c++ multithreading c++11 memory-leaks mutex

基本上,我需要从数千个并发读取的文件中填充数std::map个数百万个关键条目(订单数量为5000万或更少)。这些键指向的值将从堆(std::bitset类型)中分配。

std::map<std::string,std::bitset<BITSET_SIZE>*> my_map;
  1. 我的第一个担忧是:我不想要两个线程(首先检查是否有一个 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_lockboost::shared_lock的智能组合 并且为了表现而提升:: unique_lock,我很乐意听到你的想法。

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

2 个答案:

答案 0 :(得分:1)

即使没有同步,

conststd::容器(如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尝试了最简单的解决方案,发现它不够快。让我们走得更远。 (注意:最佳的行动方案是测量应用程序的性能并找到代码花费大部分时间的地方,但我不能这样做;))。很少有“明显的”(读:可能)减速原因:

  • 插入到map中 - instrying到有序映射具有O(ln n)运行时性能,并且实际上可能由于缓存不匹配而非常慢。在1个百万元素的平面图中,您需要与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;
}

它的作用很少: - 首先将键插入向量(忽略检查重复项),稍后将对其进行排序并插入带有放置提示的映射(它们已排序,因此我们总是知道插入下一个元素的正确位置 - 地图的结尾),这是比直接插入地图要快得多 - 每个文件的项目首先被放入它自己的向量中并在解析整个文件后移动到全局文件中,这样可以最小化锁定

这应该足以提高性能。接下来的事情是用其他东西替换字符串以避免这么多的字符串到字符串排序比较。但这很容易超出范围。 ;)

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