关于std::string 在 并发场景下 __grow_by_and_replace free was not allocated 的异常问题
使用string時(shí)發(fā)現(xiàn)了一些坑。
 我們知道stl 容器并不是線程安全的,所以在使用它們的過程中往往需要一些同步機(jī)制來保證并發(fā)場景下的同步更新。
應(yīng)該踩的坑還是一個(gè)不拉的踩了進(jìn)去,所以還是記錄一下吧。
 string作為一個(gè)容器,隨著我們的append 或者 針對(duì)string的+ 操作都會(huì)讓string內(nèi)部的數(shù)據(jù)域動(dòng)態(tài)增加,而動(dòng)態(tài)增加的過程則伴隨著一些局部指針變量的創(chuàng)建和釋放,而當(dāng)我們并發(fā)對(duì)同一個(gè)string進(jìn)行操作的時(shí)候(測試很明顯,寫工程項(xiàng)目因?yàn)閷W⒂诟鱾€(gè)模塊細(xì)節(jié),這一些問題因?yàn)榇a功底不夠,還是沒有辦法注意到位),就會(huì)出現(xiàn)一些double-free 這樣的異常問題(double-free 即 對(duì)同一個(gè)地址釋放了兩次,第一次對(duì)一個(gè)地址free的時(shí)候這段內(nèi)存已經(jīng)還給了操作系統(tǒng),當(dāng)?shù)诙卧L問這個(gè)地址則就是非法訪問了)。
看看下面這個(gè)測試代碼,大體邏輯就是多線程從一個(gè)已有的string數(shù)組中并發(fā)將數(shù)組中的內(nèi)容取出編碼到一個(gè)全局的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;
}
 
當(dāng)我設(shè)置并發(fā)數(shù)為20的時(shí)候,很明顯出現(xiàn)如下問題
./concurrent_test 20## 異常問題,大體就是釋放了一個(gè)不存在的地址
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中,這個(gè)函數(shù)被string::append函數(shù)調(diào)用,也就是我們上面測試代碼底層的Encode邏輯會(huì)調(diào)用這個(gè)append,然后grow_by_and_replace就是為了對(duì)容器進(jìn)行擴(kuò)容。
基本實(shí)現(xiàn)代碼如下:
 
 我們可以發(fā)現(xiàn)在grow_by_and_replace 在實(shí)現(xiàn)擴(kuò)容的邏輯過程中需要分配新的地址空間,將舊的數(shù)據(jù)拷貝到新的地址,這個(gè)過程需要借用臨時(shí)指針,并且在完成拷貝之后釋放老的地址old_p。
 很明顯,我們并發(fā)append全局string的時(shí)候,這里的old_p 的釋放并不是線程安全的,兩個(gè)線程同時(shí)append,且都需要進(jìn)行擴(kuò)容,則一個(gè)擴(kuò)容完成釋放舊指針,但是舊指針還在被另一個(gè)線程引用,則第二個(gè)線程擴(kuò)容完成釋放舊指針,顯然是訪問了一個(gè)空的地址了。
除了并發(fā)問題之外,使用string 不斷得append的時(shí)候 還會(huì)有性能問題,因?yàn)閍ppend擴(kuò)容期間 會(huì)不斷得有數(shù)據(jù)拷貝,而內(nèi)存拷貝是很浪費(fèi)時(shí)間的,所以string使用時(shí)如果能夠預(yù)知容量,建議reserve 足夠的空間,能夠避免動(dòng)態(tài)分配空間造成的性能問題,當(dāng)然,如果提前reserve的話 也不會(huì)有 grow_by_and_replace 這個(gè)問題的。
在main函數(shù)中,調(diào)用線程邏輯之前增加dst.reserve(10000),則并發(fā)100線程 跑100輪都不會(huì)有問題了。
但是在我們實(shí)際的應(yīng)用過程中想要解決 這個(gè)string 并發(fā)擴(kuò)容時(shí)造成的內(nèi)存泄漏問題,我們還需要有其他的辦法。
局部構(gòu)造好之后賦值給一個(gè)全局變量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);
}
                            總結(jié)
以上是生活随笔為你收集整理的关于std::string 在 并发场景下 __grow_by_and_replace free was not allocated 的异常问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: 比格多少钱啊?
 - 下一篇: 我爸说存3000块钱60年定期,60年后