【问题标题】:Is std::map::end thread-safe and is guaranteed that it always the same for the same container?std::map::end 是线程安全的,并且保证它对于同一个容器总是相同的吗?
【发布时间】:2016-07-05 23:28:30
【问题描述】:

我使用std::map 并获得一个我可以使用的元素:http://www.cplusplus.com/reference/map/map/

也:lower_bound()equal_range() - 在这种情况下与 find() 相同。

我无法使用

  • at() - 因为它抛出了一个异常,我测量了 10 倍的性能下降
  • operator[] - 因为它插入一个不存在的元素,这种行为是不可接受的

find() - 这就是我想要的。但是我在多线程程序中使用std::map并通过锁std::mutex保护它。

还有从其他线程对std::map 的插入和删除。

我应该保护std::map::end 还是保证它对于一个分配的容器始终相同?

我可以使用像static auto const map_it_end = map1.end(); 这样不受std::mutex 保护的东西吗?

http://ideone.com/tATn0H

#include <iostream>
#include <string>
#include <mutex>
#include <thread>
#include <map>

std::map<std::string, std::string> map1 ( {{"apple","red"},{"lemon","yellow"}} );
static auto const map_it_end = map1.end();
std::mutex mtx1;

void func() {
    std::lock_guard<std::mutex> lock1(mtx1);

    auto it1 = map1.find("apple");
    if(it1 != map_it_end)   // instead of: if(it1 != map1.end())
        std::cout << it1->second << ", ";
}

int main ()
{
    std::thread t1(func);
    std::thread t2(func);
    t1.join();
    t2.join();

    return 0;
}

http://www.cplusplus.com/reference/map/map/end/

数据竞争 容器被访问(既不是 const 也不是 非常量版本修改容器)。没有包含的元素 通过调用访问,但返回的迭代器可用于访问 或修改元素。同时访问或修改不同的 元素是安全的。

【问题讨论】:

  • 你有没有尝试在地图末尾插入一个新元素并测试 map::end() 是否改变了?
  • 无论如何,与map_it_end 而不是map1.end() 相比,您可能不会节省任何可衡量的性能。
  • 地图会改变吗?如果没有,并且您只进行查找而不插入或删除元素,那么您不需要互斥锁。并发非修改访问是线程安全的。
  • @Tim Straubinger map::end() 没有改变。但这将适用于所有编译器吗? ideone.com/tATn0H
  • @Jonathan Wakely 还有其他线程的插入和删除。

标签: c++ multithreading c++11 concurrency c++14


【解决方案1】:

我应该保护std::map::end 还是保证它对于一个分配的容器始终相同?

从技术上讲,任何对成员函数的调用都必须由互斥锁保护,如果它可能与任何非常量成员函数同时发生的话。因此,如果任何线程可以插入或擦除元素,那么在不锁定互斥锁的情况下调用 end() 是不安全的。

我可以使用像static auto const map_it_end = map1.end(); 这样不受std::mutex 保护的东西吗?

在某些情况下,您可以缓存过去的迭代器,因为 std::map 的过去的迭代器不会因插入和擦除而失效,只有通过交换或移动地图才可能失效。

但你为什么要这样做?缓慢的操作是find() 而不是end(),所以如果您在仍然持有互斥锁的同时调用end(),那么它肯定可以工作。

如果其他线程可能正在擦除元素,那么您需要在取消引用 find() 返回的迭代器时保持互斥锁,以确保它不会被另一个线程擦除它所引用的元素而无效。同样,当您已经锁定互斥锁时,拨打end() 不会有问题。

【讨论】:

  • 谢谢! “9 insert 和 emplace 成员不应影响迭代器的有效性和对容器的引用,而擦除成员应仅使迭代器和对被擦除元素的引用无效。”第 763 页工作草案,标准 C++ 2014-11-19 open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4296.pdf
【解决方案2】:

我在23.2 Container requirements 中没有发现任何东西表明end() 总是返回相同的值,也不是线程安全的。 end() 定义如下。

begin() 返回一个迭代器,指向第一个元素 容器。 end() 返回一个迭代器,它是过去的值 为容器。如果容器是空的,那么 begin() == end();

该规范似乎涵盖了所有容器的end()。我在23.4.4 Class template map 中找不到任何可以取代这个一般容器要求的东西。实际上,“Past-the-end value”的措辞是这样的,因此可以合理地解释为 end() 的值可能会根据容器中最后一个元素的内容/位置而改变。

这将是std::vector 的典型情况。一个典型的std::vectorend() 值会根据向量中元素的数量而变化,原因很明显。没有任何规定必须这样做,但通常情况就是这样。回到std::map,人们可能会期望给定地图的end() 总是相同的值,但也没有任何说明它必须如此。

我想说对std::map 的所有访问都必须受到互斥锁的保护。一旦一个互斥体被释放,关于映射的任何内容都不再有效。不能假设 end() 在互斥锁被释放后仍然是一个有效的迭代器。

【讨论】:

  • 对于关联容器,例如std::map,标准规定插入和擦除迭代器不会使过去的迭代器([associative.reqmts]/9)无效。交换或移动地图可能会使结束迭代器无效。
猜你喜欢
  • 1970-01-01
  • 2020-05-08
  • 2017-04-12
  • 1970-01-01
  • 2011-02-10
  • 1970-01-01
  • 2020-08-03
相关资源
最近更新 更多