使用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);
}

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

  1. 分布式锁和mysql事物扣库存_这个是真的厉害,高并发场景下的订单和库存处理方案,讲的很详细了!...

    前言 之前一直有小伙伴私信我问我高并发场景下的订单和库存处理方案,我最近也是因为加班的原因比较忙,就一直没来得及回复.今天好不容易闲了下来想了想不如写篇文章把这些都列出来的,让大家都能学习到,说一千道 ...

  2. 并发场景下的幂等问题——分布式锁详解

    简介:本文从钉钉实人认证场景的一例数据重复问题出发,分析了其原因是因为并发导致幂等失效,引出幂等的概念.针对并发场景下的幂等问题,提出了一种实现幂等可行的方法论,结合通讯录加人业务场景对数据库幂等问题 ...

  3. java分布式库存系统_这个是真的厉害,高并发场景下的订单和库存处理方案,讲的很详细了!...

    前言 之前一直有小伙伴私信我问我高并发场景下的订单和库存处理方案,我最近也是因为加班的原因比较忙,就一直没来得及回复.今天好不容易闲了下来想了想不如写篇文章把这些都列出来的,让大家都能学习到,说一千道 ...

  4. java currenttimemillis 效率_高并发场景下System.currentTimeMillis()的性能问题的优化

    前言 System.currentTimeMillis()的调用比new一个普通对象要耗时的多(具体耗时高出多少我也不知道,不过听说在100倍左右),然而该方法又是一个常用方法,有时不得不使用,比如生 ...

  5. Redis高并发场景下秒杀超卖解决

    目录 1 什么是秒杀 2 为什么要防止超卖 3 单体架构常规秒杀 3.1 常规减库存代码 3.2 模拟高并发 3.3 超卖现象 3.4 分析原因 4 简单实现悲观乐观锁解决单体架构超卖 4.1 悲观锁 ...

  6. 高并发场景下数据库的常见问题及解决方案

    一.分库分表 (1)为什么要分库分表 随着系统访问量的增加,QPS越来越高,数据库磁盘容量不断增加,一般数据库服务器的QPS在800-1200的时候性能最佳,当超过2000的时候sql就会变得很慢并且 ...

  7. 高并发场景下,到底先更新缓存还是先更新数据库?

    在大型系统中,为了减少数据库压力通常会引入缓存机制,一旦引入缓存又很容易造成缓存和数据库数据不一致,导致用户看到的是旧数据. 为了减少数据不一致的情况,更新缓存和数据库的机制显得尤为重要,接下来带领大 ...

  8. 高并发场景下缓存的常见问题

    作者介绍: 丁浪,非著名架构师.关注高并发.高可用的架构设计,对系统服务化.分库分表.性能调优等方面有深入研究和丰富实践经验.热衷于技术研究和分享. 声明:版权归丁浪作者本人所有,转载请联系作者本人 ...

  9. 本地缓存需要高时效性怎么办_缓存在高并发场景下的常见问题

    缓存一致性问题 当数据时效性要求很高时,需要保证缓存中的数据与数据库中的保持一致,而且需要保证缓存节点和副本中的数据也保持一致,不能出现差异现象.这就比较依赖缓存的过期和更新策略.一般会在数据发生更改 ...

最新文章

  1. 中科院微生物所王军课题组特别研究助理招聘
  2. ubuntu-10.04硬盘安装挫折略记
  3. 【零基础入门数据挖掘】-模型融合
  4. C++:Static修饰变量 vs Static修饰函数
  5. 与python相关计算机基础知识
  6. fastdfs笔记_fastdfs基于group的扩容
  7. 一文读懂5G专网发展现状与挑战
  8. 实现主人领养宠物并带宠物去玩,狗狗叼飞碟,企鹅去南极游泳
  9. Android 项目中依赖项目、依赖库、依赖module中的jar包(第三方库)
  10. 《英雄无敌3》的一个独立的扩展版-英雄无敌3死亡阴影下载
  11. 人工智能管家机器人应当具备哪些功能?拥有家电控制能力是优势
  12. 微信“小程序”要来了,简单点,解释的方式简单点
  13. 我的第一个小程序(Discuz! + 微信小程序)
  14. Vue批量生成二维码并打包下载
  15. 安装Mariadb columnStore(10.3版本)
  16. 房价整体下降已成定局
  17. C++实现高斯坐标的正反算
  18. Python爬虫实战之爬取QQ音乐数据!QQ音乐限制太多了!
  19. 世界是由什么组成的java_世界是由什么组成的?
  20. iPhone4S价格走势平稳 现价5999值得买

热门文章

  1. 数据结构学习(十三)、快速排序
  2. inconfont 字体库应用
  3. 十五天精通WCF——第六天 你必须要了解的3种通信模式
  4. 用sed 给文本文件加行号
  5. 牛腩44 整合登陆页 RequiredFieldValidator 和 ValidationSummary 以及 asp.net 自带的MD5 加密...
  6. 深入.NET DataTable
  7. ORB-SLAM2系统的实时点云地图构建
  8. java语言环境变量_JAVA语言环境变量的设置教程
  9. ubuntu装java环境_Ubuntu安装Java环境
  10. oracle 跑旧的文件,移动datafile以后,旧的datafile是否还被Oracle使用