关于std::string 在 并发场景下 __grow_by_and_replace free was not allocated 的异常问题

news/2024/7/3 16:25:59

使用string时发现了一些坑。
我们知道stl 容器并不是线程安全的,所以在使用它们的过程中往往需要一些同步机制来保证并发场景下的同步更新。

应该踩的坑还是一个不拉的踩了进去,所以还是记录一下吧。
string作为一个容器,随着我们的append 或者 针对string的+ 操作都会让string内部的数据域动态增加,而动态增加的过程则伴随着一些局部指针变量的创建和释放,而当我们并发对同一个string进行操作的时候(测试很明显,写工程项目因为专注于各个模块细节,这一些问题因为代码功底不够,还是没有办法注意到位),就会出现一些double-free 这样的异常问题(double-free 即 对同一个地址释放了两次,第一次对一个地址free的时候这段内存已经还给了操作系统,当第二次访问这个地址则就是非法访问了)。

看看下面这个测试代码,大体逻辑就是多线程从一个已有的string数组中并发将数组中的内容取出编码到一个全局的string里面。

#include <iostream>#include <string.h>
#include <thread>
#include <unistd.h>
#include <vector>#include <assert.h>using namespace std;std::vector<std::string> data_vec;
std::string dst;char* EncodeVarint32(char* dst, uint32_t v) {// Operate on characters as unsignedsuint8_t* ptr = reinterpret_cast<uint8_t*>(dst);static const int B = 128;if (v < (1 << 7)) {*(ptr++) = v;} else if (v < (1 << 14)) {*(ptr++) = v | B;*(ptr++) = v >> 7;} else if (v < (1 << 21)) {*(ptr++) = v | B;*(ptr++) = (v >> 7) | B;*(ptr++) = v >> 14;} else if (v < (1 << 28)) {*(ptr++) = v | B;*(ptr++) = (v >> 7) | B;*(ptr++) = (v >> 14) | B;*(ptr++) = v >> 21;} else {*(ptr++) = v | B;*(ptr++) = (v >> 7) | B;*(ptr++) = (v >> 14) | B;*(ptr++) = (v >> 21) | B;*(ptr++) = v >> 28;}return reinterpret_cast<char*>(ptr);
}inline void PutVarint32(std::string* dst, uint32_t v) {char buf[5];char* ptr = EncodeVarint32(buf, v);// If the v is a negative number, we need store the last byte.dst->append(buf, static_cast<size_t>(ptr - buf));
}inline void PutLengthPrefixedSlice(std::string* dst, const std::string& value) {PutVarint32(dst, static_cast<uint32_t>(value.size()));dst->append(value.data(), value.size());
}void EncodeTo(std::string* dst, std::string key) {PutLengthPrefixedSlice(dst, key);
}void EncodeDataVec() {std::cout << "Encode data_vec" << std::endl;for (int i = 0;i < data_vec.size(); i ++) {EncodeTo(&dst, data_vec[i]);}
}void ConstructDataVec() {std::cout << "Construct data_vec" << std::endl;for (int i = 0;i < 10; i++) {data_vec.emplace_back(std::to_string(i));}
}int main(int argc, char* argv[]) {int threads = 1;if (argc == 2) {threads = atoi(argv[1]);}std::cout << "threads : " << threads << std::endl;ConstructDataVec();for (int i = 0;i < threads; i++) {new std::thread(EncodeDataVec);}return 0;
}

当我设置并发数为20的时候,很明显出现如下问题

./concurrent_test 20## 异常问题,大体就是释放了一个不存在的地址
concurrent_test(5008,0x700008738000) malloc: *** error for object 0x7fefab504080: pointer being freed was not allocated
concurrent_test(5008,0x700008738000) malloc: *** set a breakpoint in malloc_error_break to debug
[1]    5008 abort      ./concurrent_test 20

lldb一下看看:

lldb ./concurrent_test 20
(lldb) target create "./concurrent_test"
Current executable set to '/Users/zhanghuigui/Desktop/work/source/cpp_practice/data_structure/string/concurrent_test' (x86_64).
(lldb) settings set -- target.run-args  "20"
(lldb) r
Process 5828 stopped
* thread #4, stop reason = signal SIGABRTframe #0: 0x00007fff2030c462 libsystem_kernel.dylib`__pthread_kill + 10
libsystem_kernel.dylib`__pthread_kill:
->  0x7fff2030c462 <+10>: jae    0x7fff2030c46c            ; <+20>0x7fff2030c464 <+12>: movq   %rax, %rdi0x7fff2030c467 <+15>: jmp    0x7fff203066a1            ; cerror_nocancel0x7fff2030c46c <+20>: retq  
(lldb) bt

异常栈的信息如下
在这里插入图片描述
core在了grow_by_and_replace中,这个函数被string::append函数调用,也就是我们上面测试代码底层的Encode逻辑会调用这个append,然后grow_by_and_replace就是为了对容器进行扩容。

基本实现代码如下:
在这里插入图片描述
我们可以发现在grow_by_and_replace 在实现扩容的逻辑过程中需要分配新的地址空间,将旧的数据拷贝到新的地址,这个过程需要借用临时指针,并且在完成拷贝之后释放老的地址old_p
很明显,我们并发append全局string的时候,这里的old_p 的释放并不是线程安全的,两个线程同时append,且都需要进行扩容,则一个扩容完成释放旧指针,但是旧指针还在被另一个线程引用,则第二个线程扩容完成释放旧指针,显然是访问了一个空的地址了。

除了并发问题之外,使用string 不断得append的时候 还会有性能问题,因为append扩容期间 会不断得有数据拷贝,而内存拷贝是很浪费时间的,所以string使用时如果能够预知容量,建议reserve 足够的空间,能够避免动态分配空间造成的性能问题,当然,如果提前reserve的话 也不会有 grow_by_and_replace 这个问题的。

main函数中,调用线程逻辑之前增加dst.reserve(10000),则并发100线程 跑100轮都不会有问题了。

但是在我们实际的应用过程中想要解决 这个string 并发扩容时造成的内存泄漏问题,我们还需要有其他的办法。

局部构造好之后赋值给一个全局变量std::string即可:

void EncodeDataVec() {std::cout << "Encode data_vec" << std::endl;std::string tmp_dst;for (int i = 0;i < data_vec.size(); i ++) {EncodeTo(&tmp_dst, data_vec[i]);}dst = std::string(tmp_dst);
}

http://lihuaxi.xjx100.cn/news/219014.html

相关文章

在Blender中创建真实的汽车CGI视觉动画效果

Blender VFX Tutorial Rig & Animate a Realistic Car in Real 大小&#xff1a;1.18G 时长1h 包含项目文件 1280X720 MP4 语言&#xff1a;英语中英文字幕&#xff08;根据原英文字幕机译更准确&#xff09; Blender VFX教程绑定&动画真实的汽车 云桥网络 平台获取教程…

子网掩码

子网掩码 from: http://baike.baidu.com/view/878.htm 子网掩码(subnet mask) 又叫网络掩码、地址掩码、子网络遮罩&#xff0c;它是一种用来指明一个IP地址 的哪些位标识的是主机所在的子网以及哪些位标识的是主机的位掩码。子网掩码不能单独存在&#xff0c;它必须结合…

关于 智能指针 的线程安全问题

先说结论&#xff0c;智能指针都是非线程安全的。 多线程调度智能指针 这里案例使用的是shared_ptr&#xff0c;其他的unique_ptr或者weak_ptr的结果都是类似的&#xff0c;如下多线程调度代码&#xff1a; #include <memory> #include <thread> #include <v…

C++中 public,protected, private 访问标号小结

第一&#xff1a;private, public, protected 访问标号的访问范围。 private&#xff1a; 只能由1.该类中的函数、2.其友元函数访问。 不能被任何其他访问&#xff0c;该类的对象也不能访问。 protected&#xff1a; 可以被1.该类中的函数、2.子类的函数、以及3.其友元函数…

做片子留着备用 超级游戏影视配乐音效库36套合集

做片子留着备用 超级游戏影视配乐音效库36套合集 Epic Stock Media 创造数字媒体产品,改变你听到和看到的音频方式在游戏、电影、电视、演出和世界上任何地方的人们消费媒体。 影视配乐音效素材大全 百度一下 云桥网络 平台huo取 教程&#xff01; 所有采样均为&#xff1a;…

Rocksdb Ribbon Filter : 结合 XOR-filter 以及 高斯消元算法 实现的 高效filter

文章目录前言XOR-filter 实现原理xor filter 的构造原理xor filter 构造总结XOR-filter 和 ADD-filter对比XOR-filter 在计算上的优化Ribbon filter高斯消元法总结参考前言 还是起源于前几天的Rocksdb meetup&#xff0c;其中Peter C. Dillinger 这位大佬分享了自己为rocksdb实…

Revit和Unreal Engine真实的建筑可视化视频教程

Revit和Unreal Engine真实的建筑可视化视频教程 Lynda – Revit and Unreal Engine: Real-Life Architectural Visualizations Lynda–Revit和Unreal Engine&#xff1a;真实的建筑可视化 时长 3小时 25分 | 1.15 GB |含项目练习文件|使用的软件&#xff1a;Revit&#xff0c…

哈佛结构和冯诺依曼结构区别。

哈佛结构是一种将程序指令存储和数据存储分开的存储器结构。中央处理器首先到程序指令存储器中读取程序指令内容&#xff0c;解码后得到数据地址&#xff0c;再到相应的数据存储 器中读取数据&#xff0c;并进行下一步的操作&#xff08;通常是执行&#xff09;。程序指令存储和…