C++多线程操作vector不同索引string追加字符偶现coredump问题
问题现象
多线程更新全局vector<string>中不同索引对应的独立string元素,预期无并发数据冲突,但程序运行偶现coredump。
问题复现代码:
extern vector<string> gTestVec; #define NUM 10 void * worker(void * args) { thread_data * p = (thread_data *)args; int i = p->thread_id; for (int j=0; j<100; j++) { gTestVec[i] += "a"; } return NULL; } void do_complete_stage_test::excute() { int i = 0; pthread_t thd[NUM]; thread_data data[NUM]; for (i=0; i<NUM; i++) { gTestVec.push_back(format("%d", i)); data[i].thread_id = i; if (0 != pthread_create(&(thd[i]), NULL, &worker, (void *)&data[i])) { printf("pthread_create failed"); } } for (int i=0; i<NUM; i++) { if (0 != pthread_join(thd[i], NULL)) { printf("pthread_join failed"); } } }
崩溃时捕获的gdb栈信息:
Starting program: /data/settle_script/isp_tran_collect/bin/isp_tran_collect -p 2134234 [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib64/libthread_db.so.1". [New Thread 0x7ffff2623700 (LWP 6316)] [New Thread 0x7fffefb0e700 (LWP 6317)] [New Thread 0x7fffef30d700 (LWP 6318)] Program received signal SIGSEGV, Segmentation fault. [Switching to Thread 0x7fffef30d700 (LWP 6318)] 0x00007ffff6787d67 in ?? () from /lib64/libstdc++.so.6 (gdb) bt #0 0x00007ffff6787d67 in ?? () from /lib64/libstdc++.so.6 #1 0x00007ffff678899b in std::string::reserve(unsigned long) () from /lib64/libstdc++.so.6 #2 0x00007ffff6788bbf in std::string::append(char const*, unsigned long) () from /lib64/libstdc++.so.6 #3 0x000000000044babe in append (__s=0x880492 "a", this=<optimized out>) at /usr/include/c++/4.8.2/bits/basic_string.h:1009 #4 operator+= (__s=0x880492 "a", this=<optimized out>) at /usr/include/c++/4.8.2/bits/basic_string.h:942 #5 worker (args=<optimized out>) at ../src/do_complete_stage_test.cpp:21 #6 0x00007ffff7bc6e25 in start_thread () from /lib64/libpthread.so.0 #7 0x00007ffff5ee635d in clone () from /lib64/libc.so.6
根因分析
这是典型的std::vector并发修改导致的未定义行为,核心问题是线程创建和vector元素插入在同一个循环中执行,但线程启动后会立刻并发访问vector,和主线程后续的push_back操作产生数据竞争:
std::vector采用连续内存存储元素,当push_back检测到当前剩余容量不足时,会申请一块更大的新内存,把已有元素拷贝/移动到新内存后释放旧内存。- 代码中每插入一个元素就立刻创建对应工作线程,线程创建成功后会马上进入
worker函数读取gTestVec[i]的元素地址执行append操作;此时主线程还在继续循环执行后续的push_back,一旦触发vector扩容,之前已经启动的工作线程持有的旧内存地址就变成野指针,后续对野指针指向的string做append、reserve操作就会触发段错误,和gdb栈中崩在string::reserve/string::append的特征完全吻合。 - 哪怕提前给vector reserve了足够容量,
push_back修改vector size成员和工作线程读取vector元素的操作没有同步,依然属于C++标准定义的数据竞争,属于未定义行为,仍可能出现偶发崩溃。
注意:“不同线程操作不同索引元素就无冲突”的假设,成立前提是vector的底层存储结构已经完全固定,不会再发生扩容、元素内存地址变动的情况。
修复方案
- 把vector初始化和线程创建逻辑拆成两个独立循环:先循环完成所有
push_back操作,等vector的大小、元素内存地址完全固定后,再循环创建所有工作线程,从根源上避免主线程修改vector结构和工作线程访问vector的并发冲突。
修复后的核心逻辑示例:void do_complete_stage_test::excute() { int i = 0; pthread_t thd[NUM]; thread_data data[NUM]; // 第一步:单线程完成所有vector元素插入,期间无其他线程访问vector for (i=0; i<NUM; i++) { gTestVec.push_back(format("%d", i)); data[i].thread_id = i; } // 第二步:vector结构固定后,统一创建工作线程 for (i=0; i<NUM; i++) { if (0 != pthread_create(&(thd[i]), NULL, &worker, (void *)&data[i])) { printf("pthread_create failed"); } } for (int i=0; i<NUM; i++) { if (0 != pthread_join(thd[i], NULL)) { printf("pthread_join failed"); } } } - 如果业务场景必须在线程运行过程中动态增删vector元素,必须加互斥锁保护所有对vector的读写操作,不能直接无锁并发访问。
内容的提问来源于stack exchange,提问作者zilliax
相关产品推荐
相关产品推荐

