c++中多线程操作 string 引发的 coredump,栈中比较奇怪的一点 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
blacksmith
V2EX    C++

c++中多线程操作 string 引发的 coredump,栈中比较奇怪的一点

  •  1
     
  •   blacksmith 2021-02-03 20:23:24 +08:00 4657 次点击
    这是一个创建于 1718 天前的主题,其中的信息可能已经有所发展或是发生改变。

    源代码如下:

    #include <iostream> #include <string> #include <vector&t; #include <thread> struct A { std::string name = "blacksmith"; int age = 100; }; struct B1 { std::string local_name = "jd-B1"; void func1(A* a) { while (true) { a->name = local_name; std::string key = local_name + "**"; } } }; struct B2 { std::string local_name = "jd-B2"; void func1(A* a) { while (true) { a->name = local_name; std::string key = local_name + "**"; } } }; int main() { /** * 探测是否支持 COW */ std::string* test = new std::string("blacksmith"); std::string name = *test; std::cout << "test:" << test->data() << ", name=" << name.data() << std::endl; if (test->data() == name.data()) { std::cout << "COW(Copy On Write) support!" << std::endl; } else { std::cout << "COW(Copy On Write) NOT support!" << std::endl; } delete test; /** * 多线程操作 */ std::vector<std::thread> th_vec; int thread_count = 4; A a; B1 b1; B2 b2; for (int i = 0; i < thread_count; i++) { th_vec.emplace_back([&](){ b1.func1(&a); }); th_vec.emplace_back([&](){ b2.func1(&a); }); } for (auto& item : th_vec) { item.join(); } std::cout << "=========END==========" << std::endl; return 0; } 

    编译:

    g++ --std=c++11 string-test.cc -g -lpthread 

    查看 coredump 栈:

    (gdb) bt #0 0x00007f5613350e20 in __memcpy_ssse3 () from /usr/lib64/libc.so.6 #1 0x00007f5613ba8650 in std::string::_Rep::_M_clone(std::allocator<char> const&, unsigned long) () from /usr/lib64/libstdc++.so.6 #2 0x00007f5613ba86d4 in std::string::reserve(unsigned long) () from /usr/lib64/libstdc++.so.6 #3 0x00007f5613ba893f in std::string::append(char const*, unsigned long) () from /usr/lib64/libstdc++.so.6 #4 0x0000000000402808 in std::operator+<char, std::char_traits<char>, std::allocator<char> > ( __lhs="jd-B2", '\000' <repeats 11 times>, "!\000\000\000\000\000\000\000@9@\000\000\000\000\000(I\213\071\375\177\000\000\060I\213\071\375\177\000\000Q\002\000\000\000\000\000\000\"", '\000' <repeats 15 times>, "\001", '\000' <repeats 15 times>, "\377\377\377\377\377\377\377\377\000\000\000\000\000\000\000\000\377\377\377\377\377\377\377\377", '\000' <repeats 88 times>..., __rhs=0x40386a "**") at /opt/rh/devtoolset-7/root/usr/include/c++/7/bits/basic_string.h:5917 #5 0x000000000040263f in B2::func1 (this=0x7ffd398b4920, a=0x7ffd398b4930) at string-test.cc:28 #6 0x0000000000401108 in <lambda()>::operator()(void) const (__closure=0x19a5368) at string-test.cc:62 #7 0x0000000000401fde in std::__invoke_impl<void, main()::<lambda()> >(std::__invoke_other, <lambda()> &&) (__f=...) at /opt/rh/devtoolset-7/root/usr/include/c++/7/bits/invoke.h:60 #8 0x0000000000401cb0 in std::__invoke<main()::<lambda()> >(<lambda()> &&) (__fn=...) at /opt/rh/devtoolset-7/root/usr/include/c++/7/bits/invoke.h:95 #9 0x0000000000402392 in std::thread::_Invoker<std::tuple<main()::<lambda()> > >::_M_invoke<0>(std::_Index_tuple<0>) (this=0x19a5368) at /opt/rh/devtoolset-7/root/usr/include/c++/7/thread:234 #10 0x000000000040233f in std::thread::_Invoker<std::tuple<main()::<lambda()> > >::operator()(void) (this=0x19a5368) at /opt/rh/devtoolset-7/root/usr/include/c++/7/thread:243 #11 0x00000000004022fe in std::thread::_State_impl<std::thread::_Invoker<std::tuple<main()::<lambda()> > > >::_M_run(void) (this=0x19a5360) at /opt/rh/devtoolset-7/root/usr/include/c++/7/thread:186 #12 0x000000000040343f in execute_native_thread_routine () #13 0x00007f5613df8dd5 in start_thread () from /usr/lib64/libpthread.so.0 #14 0x00007f5613302ead in clone () from /usr/lib64/libc.so.6 

    比较疑惑的一点是,多线程写 string,为什么不是在写入那一行 core,而是在后面拼接成员变量?

    a->name = local_name; // 我理解应该是这一行报 core

    std::string key = local_name + "**"; // 实际在操作 local_name 的时候 core,并且看栈,local_name 内存乱了

    辛苦各位大佬,有时间的帮忙看看,很是疑惑。 谢谢。

    25 条回复    2021-02-09 21:21:02 +08:00
    yianing
        1
    yianing  
       2021-02-03 20:57:58 +08:00 via Android
    a->name 写入的时候只是拷贝了 header 部分,虽然也是多写但是写入都是同一份数据,没报错只能说运气好吧,下面 appen 的地方就是多个线程操作一份指针数据了
    yianing
        2
    yianing  
       2021-02-03 21:05:06 +08:00
    @yianing golang 里面的 string 是不可变的,我用 int 测试一下
    ```go
    package main

    import "time"

    type A struct {
    age int
    }

    type B struct {
    age int
    }

    func (b *B) Op(a *A) {
    for {
    a.age++
    b.age++
    }
    }

    func main() {
    b := &B{}
    a := &A{}
    for i := 0; i < 3; i++ {
    go b.Op(a)
    }
    time.Sleep(100 * time.Millisecond)
    }
    ```
    go run -race 的时候两个++都是会报错的
    secondwtq
        3
    secondwtq  
       2021-02-03 21:35:28 +08:00 via iPhone
    多线程写入,结果就是写入的数据不可靠,不是直接给你报错。
    只有你再使用写入的数据时才会把问题暴露出来,而在实际程序中很难知道是谁什么时候写入的数据,这是并发错误难调试的原因之一,报错的点不一定是 data race 发生的点。

    有时候也会故意这么做,性能会好一些,
    hxndg
        4
    hxndg  
       2021-02-03 22:46:21 +08:00
    首先,你这个应该不会只出现一种 core 的结果
    27 行应该也可能出现 core,但 core 的原因应该是多个线程 free 同一个地方导致的。

    另外 28 行出现 core 的原因看流程像是拷贝的时候生成的临时变量都是在 local_name 上,然后不同线程操作导致拷贝的长度无效导致的。

    当然以上结论需要事实+观察寄存器传参确定,我忘了 X86_64 位下寄存器的值代表的含义了,不做任何正确性保证。
    matrixji
        5
    matrixji  
       2021-02-04 00:18:32 +08:00
    楼主你确定这个问题不是混用了 devtoolset-7 和系统的 libstdc++导致的。
    从 C++11 本省来讲 a->name = local_name; 走的是 operator= 由于 local_name 的长度 大于 A::name 实际上这里都不会发生内存的释放和申请,只会有 Copy 操作。所以这里应该不会有内存错误才对。
    至于 std::string key = local_name + "**"; 就应该更加没有问题了。
    imjamespond
        6
    imjamespond  
       2021-02-04 00:42:15 +08:00 via Android
    string 本质上好像是个 vector
    Wirbelwind
        7
    Wirbelwind  
       2021-02-04 01:12:22 +08:00
    升级一下编译器

    msvc 没能复现出来

    每个线程读取的都是线程内 local_name,而且 local_name 没有被写入过,
    应该不会有这种情况
    blacksmith
        8
    blacksmith  
    OP
       2021-02-04 09:36:03 +08:00
    @yianing 谢谢详细的讲解。在 append 的地方,都是只读的成员变量 local_name,并没有去写。但是栈中显示的这个变量内存乱了,比较让人诧异。按说应该是 a->name 的内存有问题才对。
    blacksmith
        9
    blacksmith  
    OP
       2021-02-04 09:38:35 +08:00
    @secondwtq
    是的,我开始也认为写入会导致数据不准确。但是 local_name 变量是一个成员变量,并没有去修改它。开始怀疑是 cow 的一些机制导致的,但是我找不到任何的证据。线上发生了类似的 core,栈的地方和实际的操作有问题的地方不一致,导致排查的时候需要通览一下代码,我在想有没有什么方法可以直接定位到写错误的地方?
    blacksmith
        10
    blacksmith  
    OP
       2021-02-04 09:41:00 +08:00
    @hxndg 非常感谢。确实会有两种 coredump 发生。
    第一种 27 行的那个,比较好理解。
    发生在 28 行的这个 core 其实不太符合预期,如果拷贝的临时变量不是存储在左边的值,而是右边的值,那么可以说的通。但是我确实没有找到类似的证据,证明这一点。
    谢谢了。
    blacksmith
        11
    blacksmith  
    OP
       2021-02-04 09:42:26 +08:00
    @matrixji 应该不是的,我开始的版本没有使用 devtooset-7,也有问题,后面想升级 gcc 版本,发现也是类似的问题。
    coredump 的内容确实如我帖子里的。很是奇怪为啥 std::string key = local_name + "**";这一行会有问题。
    谢谢回复。
    blacksmith
        12
    blacksmith  
    OP
       2021-02-04 09:43:14 +08:00
    @imjamespond 怀疑是 cow 做了什么动作,可是我没有证据:)
    谢谢回复。
    blacksmith
        13
    blacksmith  
    OP
       2021-02-04 09:44:47 +08:00
    @Wirbelwind
    可能跟编译器有关吧。我用 4.8.5 和 7 的版本都试了下,都是有问题的。目前我这没有 msvc 的环境。
    这个现象确实在 linux 下发生了。所以百思不得其解。
    谢谢回复。
    Monad
        14
    Monad  
       2021-02-04 10:21:02 +08:00


    我这边是在 operator=的时候,g++4.8.5
    Monad
        15
    Monad  
       2021-02-04 10:54:30 +08:00
    @Monad #14 不一定会在这里 上面的图不对 补一个图
    hxndg
        16
    hxndg  
       2021-02-04 16:41:51 +08:00
    建议还是上 libc 源码看看吧,这个明显跟编译器行为有关了。

    不过没明白干嘛要干这种事情呢?一般这种多线程操作都是极度小心的。
    blacksmith
        17
    blacksmith  
    OP
       2021-02-05 09:46:31 +08:00
    @Monad 会有两种 core 。一种是你尝试的这个,还有一种是我发的那种。
    blacksmith
        18
    blacksmith  
    OP
       2021-02-05 09:48:56 +08:00
    @hxndg 线上系统有个类似的问题被发现了,不过栈看着比较奇怪,我按照那个逻辑写了这个来复现。问题已经修复了,但是还是没能找到一个比较信服的解释,来说明 std::string key = local_name + "**";这行会 core 的原因。
    确实多线程操作不小心导致的问题。
    matrixji
        19
    matrixji  
       2021-02-05 10:28:25 +08:00
    @blacksmith 重新看了一下 libstdc++的源码。baseic_string::operator=的 实现,不同版本不一样。所以我的环境永远不会 codedump 。

    https://github.com/gcc-mirror/gcc/blob/releases/gcc-4.8.5/libstdc%2B%2B-v3/include/bits/basic_string.h 是 Centos 对应的版本,实现很简单:
    basic_string&
    operator=(const basic_string& __str)
    { return this->assign(__str); }
    无条件地去 assign 新的内容,assign 里面的逻辑就是 free 老的,clone 新的。

    https://github.com/gcc-mirror/gcc/blob/releases/gcc-9.3.0/libstdc++-v3/include/bits/basic_string.h 你可以找下新版的实现就不一样了,如果当前的长度够了,就不会去 free,而是直接在当前 buffer 上 Copy 。

    由于是多线程操作,所以会造成两个线程同时执行 assign 的操作。
    那么有可能出现:
    同一个地址被 free 两次,照成 double free,那就是 @Monad 提到的第一个错误。
    被 free 掉了继续使用,那就是你出现的这种情况:
    线程 1:Free -> New -> 使用(实际已经被 Free 掉了)
    线程 2:..........................Free.............

    所以 coredump 的时机也就不一定了。如果楼主要细究,可以用 valgrind 跑一下就清楚了。
    hxndg
        20
    hxndg  
       2021-02-05 11:30:31 +08:00
    local_name + "**"和 key 必然是放在栈上申请的临时变量,按照道理来说不应该有问题,所以我做了个尝试:

    我把你 B1.func1 核 B2.func1 里面的`a->name = local_name`去掉以后,试了下就一直没出现 core 的现象了。

    估计又是编译器做的一些“好事”导致的问题,感觉还是跟利用了 a->name 有关系,和生命周期什么的有关。

    我司之所以不用 C++,用 C 一部分原因也是因为避免编译器的操作。。。。
    hxndg
        21
    hxndg  
       2021-02-05 11:37:16 +08:00
    @matrixji
    不,你这个说法没有解释清楚为什么 local_name + "**"为什么会 core,
    每个线程都是在自己的栈上操作,local_name+"**"的结构应该是在本地栈上分配的,即使 assign 的是直接这个地址也不应该 core 才对。
    Wirbelwind
        22
    Wirbelwind  
       2021-02-06 05:03:22 +08:00
    @hxndg 上面 a->name = local_name 之后,编译器一定程度上可能会直接使用保存了 a->name 的寄存器(应该是 a)来替代 local_name 的寄存器
    hxndg
        23
    hxndg  
       2021-02-06 11:12:22 +08:00 via Android
    @Wirbelwind 嗯,我也怀疑是这个,但是没把代码下下来看并不确定
    YouLMAO
        24
    YouLMAO  
       2021-02-07 19:59:12 +08:00
    @blacksmith sse3 很明显异常呀, 因为内存没对齐呀, 不是一个个字节拷贝是一块块拷贝的
    vduang
        25
    vduang  
       2021-02-09 21:21:02 +08:00
    @blacksmith 堆内存只要一乱,程序可能在任何使用堆内存的地方崩溃,崩溃的地方和 bug 的地方可能没有任何关联,这个现象是正常的,也是这样的问题难以排查的原因。

    你这段代码的问题在于多个线程中 a->name 被并发赋值,导致 a->name (同时也是 local_name )指向的原来的堆内存被多次释放了,如果这段内存在被释放后又被重新分配出去被写入的话,local_name 指向的就是一堆垃圾了,所以即使你是在读取 localname,并没有修改 localname,程序也会在这里崩溃。

    所以这段代码什么时候崩溃在哪崩溃纯看运气。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1157 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 27ms UTC 17:55 PVG 01:55 LAX 10:55 JFK 13:55
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86