为何多线程并发写入vector<unordered_map>未出现数据损坏?
注:以下代码使用GCC 11.2.0编译。
我原本想复现复杂代码中的问题,编写了这个简化示例,却发现它能正常运行。现在我更关注其运行原理:代码创建了一个存储std::unordered_map的std::vector,数量与线程数一致,每个线程负责填充vector中的一个map。我原以为vector是连续内存且动态变化,多线程并发写入会导致数据损坏,但实际运行结果却符合预期,请问这是为什么?
#define _GLIBCXX_USE_NANOSLEEP //add it top of c++ code #include <iostream> #include <vector> #include <unordered_map> #include <thread> void add_map(std::unordered_map<int, int>& um, int map_size) { for (int i = 0; i < map_size; i++) { um.insert({i, i}); } } void print_map(std::unordered_map<int, int>& um) { for (auto& u : um) { std::cout << u.first << " " << u.second << std::endl; } } int main(int argc, char* argv[]) { // Get number of threads from user input and set random seed int num_threads = std::stoi(argv[1]); std::srand((unsigned) time(NULL)); // Populate a vector with num_threads maps, so each thread can write to each map inside the vector std::vector<std::unordered_map<int, int>> chunks; for (int i = 0; i < num_threads; i++) { std::unordered_map<int, int> empty_map; chunks.push_back(empty_map); } // Come up with the sizes for each map that will be handled by each thread std::vector<int> chunk_sizes; for (int i = 0; i < num_threads; i++) { int chunk_size = 1 + (std::rand() % 5); chunk_sizes.push_back(chunk_size); } std::cout << "Chunk sizes" << std::endl; for (int i = 0; i < num_threads; i++) { std::cout << chunk_sizes[i] << " "; } std::cout << std::endl; // For each thread, create a <int, int> unordered map so that the parts of the vector 'chunks' gets populated concurrently std::vector<std::thread> threads; for (int i = 0; i < num_threads; i++) { threads.push_back(std::thread(add_map, std::ref(chunks[i]), chunk_sizes[i])); } for (int i = 0; i < threads.size(); i++) { threads[i].join(); } // Print the maps for (int i = 0; i < chunks.size(); i++) { std::cout << "=== chunk: " << i << "; size: " << chunk_sizes[i] << " ===" << std::endl; print_map(chunks[i]); } }
运行参数为5时的输出
Chunk sizes 2 5 5 4 4 === chunk: 0; size: 2 === 1 1 0 0 === chunk: 1; size: 5 === 4 4 3 3 2 2 1 1 0 0 === chunk: 2; size: 5 === 4 4 3 3 2 2 1 1 0 0 === chunk: 3; size: 4 === 3 3 2 2 1 1 0 0 === chunk: 4; size: 4 === 3 3 2 2 1 1 0 0
原因分析
vector的内存布局在启动线程前已固定:在创建线程之前,你已经通过循环将
num_threads个空unordered_map全部添加到了chunks中,此时vector的大小和内存布局已经确定。后续线程仅修改vector中已存在的map对象,不会执行push_back、insert等可能触发vector扩容的操作——vector只有在需要新增元素时才会重新分配内存、移动现有元素,而你的代码完全不会触发这个过程,因此不存在多线程下vector扩容导致的内存冲突。线程操作的是完全独立的对象:vector中的每个
unordered_map都是独立的实例,它们的存储彼此分离(vector直接存储map对象,而map内部的哈希表数据在堆上独立分配)。每个线程仅负责写入chunks[i]对应的那个map,线程之间没有交叉访问同一个对象的情况。根据C++标准,不同线程操作完全独立的对象时,不存在数据竞争,不会产生未定义行为。单线程操作unordered_map是安全的:虽然
std::unordered_map本身不是线程安全的,但每个线程对自己负责的map进行的是单线程写入操作,这种场景下map的内部数据结构不会出现竞争损坏,完全符合标准规范。
内容的提问来源于stack exchange,提问作者The_Questioner

