关于 C++ std::thread 的疑问 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
Sign Up Now
For Existing Member  Sign In
sky2017
V2EX    C

关于 C++ std::thread 的疑问

  •  
  •   sky2017 Dec 19, 2018 5680 views
    This topic created in 2686 days ago, the information mentioned may be changed or developed.

    偶然间在项目里遇到一个问题,问题是这样产生的:在 dll 里写了个类,这个类构造函数里初始化 std::thread 创建了个线程,然后将这个类设为全局变量,最后在一个 exe 里加载这个 dll 居然阻塞了主线程?? 这个问题查了好半天,后来发现用这样的步骤就可以重现。

    1.在一个 DLL 里写一段代码:

    class TestClass { public: TestClass() { m_thread = std::thread([] {}); } ~TestClass() { if (m_thread.joinable()) { m_thread.join(); } } protected: private: std::thread m_thread; }; 

    创建一个空线程什么都不干

    2.定义一个全局变量:

    TestClass tc; 

    3.写个 exe 用 LoadLibrary 加载这个 dll:

    HMODULE hModule = ::LoadLibrary(L"..."); 

    然后就没有然后了,exe 会阻塞在 LoadLibrary()这一句。

    如果把 TestClass 代码拷贝到 exe 里,然后设为全局变量则没有这个问题。 我是用 vs2015 测试的,可能是我用 std::thread 方法不对,但是有高手能分析一下原因吗?奇怪的是网上也搜不到答案。

    27 replies    2018-12-20 14:51:48 +08:00
    jukka
        1
    jukka  
       Dec 19, 2018
    try catch 下 m_thread = std::thread([] {}); 看下有没 exception。
    GeruzoniAnsasu
        2
    GeruzoniAnsasu  
       Dec 19, 2018
    ……………………感觉是个天坑

    强烈建议不要使任何对象实例成为全局对象,用一个工厂方法去获取唯一实例都好得多:

    CSomeClass* getGlobalInstance(){
    static CSomeClass *instance = nullptr;
    if(!instance) instance = new CSomeClass{};
    return instance;
    }

    c/c++里全局对象的初始化时间是不可控的(我是指代码监控不到生命周期),但起码在 exe/elf 里我还知道他起码在_start 之后.init 里调用或者在_WinMainCRTStartup 之后 main 之前(大概)调用,但你说放在 dll 里,它是在 dllmain 之后的什么地方调用的?完全没头绪。

    std::thread 的源码也到_M_start_thread 就结束了,接下来完全是 c++ runtime 的实现,这在不同平台肯定又是不一样的,一个你得在对应平台自己调,一个你调出来了换个平台不一定还会复现,所以何必去踩呢
    sky2017
        3
    sky2017  
    OP
       Dec 19, 2018
    @jukka 没有 exception
    sky2017
        4
    sky2017  
    OP
       Dec 19, 2018
    @GeruzoniAnsasu 确实是个坑,害我浪费了好多时间,发现有人已经踩过坑了: https://blog.csdn.net/norsd/article/details/50409585
    谢谢你的建议!
    arzterk
        5
    arzterk  
       Dec 19, 2018
    dll 加载会有 Loader Lock
    sky2017
        7
    sky2017  
    OP
       Dec 19, 2018
    @arzterk 谢谢,这个解答更详细了
    v2qwsdcv
        8
    v2qwsdcv  
       Dec 19, 2018
    动态链接库不是 C++标准,是不同操作系统的实现。
    我测试了一下,在 Linux 下不能导出自定义的类型为全局变量。没有你说的阻塞的情况,应该就是没有生成这个变量导致。

    楼上说的对, 反对使用全局变量。

    ```

    #include <thread>
    #include <cstdio>
    extern "C"
    {
    class TestClass
    {
    public:
    TestClass()
    {
    printf("construct TestClass\n");
    m_thread = std::thread([] {});
    }
    ~TestClass()
    {
    printf("destruct TestClass\n");
    if (m_thread.joinable())
    {
    m_thread.join();
    }
    }

    protected:
    private:
    std::thread m_thread;
    };

    extern TestClass tc;

    extern int go =1002;
    extern struct my m;

    struct my{
    int a;
    int b;
    };
    }

    //g++ --std=c++11 -fPIC -shared dynamic.cpp -o libdy.so
    ```

    从符号表上看只有 int go 被导出了
    ```
    nm -D libdy.so
    0000000000201024 B __bss_start
    w __cxa_finalize
    0000000000201024 D _edata
    0000000000201028 B _end
    00000000000005c0 T _fini
    w __gmon_start__
    0000000000201020 D go
    0000000000000480 T _init
    w _ITM_deregisterTMCloneTable
    w _ITM_registerTMCloneTable
    w _Jv_RegisterClasses

    ```

    调用的代码

    ```
    #include <stdio.h>
    #include <stdlib.h>
    #include <dlfcn.h>

    int main()
    {
    void *handle = dlopen("./libdy.so", RTLD_NOW);
    if (!handle)
    {
    printf("%s\n", dlerror());
    exit(-1);
    }
    {
    void *ptr = nullptr;
    ptr = dlsym(handle, "tc");
    if (ptr == nullptr)
    {
    printf("tc is null\n");
    printf("%s\n", dlerror());
    }
    else
    {
    printf("find tc\n");
    }
    }

    {
    void *gp = nullptr;
    gp = dlsym(handle, "go");
    if (!gp)
    {
    printf("tc is null\n");
    printf("%s\n", dlerror());
    }
    else
    {
    printf("go is %d\n", *((int *)gp));
    }
    }

    {
    void *ptr = nullptr;
    ptr = dlsym(handle, "m");
    if (ptr == nullptr)
    {
    printf("m is null\n");
    printf("%s\n", dlerror());
    }
    else
    {
    printf("find m\n");
    }
    }
    dlclose(handle);

    return 0;
    }

    //g++ -std=c++11 -rdynamic call_dynamic.cpp -o call_dynamic -ldl
    ```
    changnet
        9
    changnet  
       Dec 19, 2018 via Android
    为什么要在构造函数里加复杂代码?构造函数不可控的
    justou
        10
    justou  
       Dec 19, 2018
    @changnet 想请教一下为什么说构造函数不可控?

    lz 在构造函数里初始化线程的做法我也经常干, 也是在获取资源
    wutiantong
        11
    wutiantong  
       Dec 19, 2018
    @changnet 啥时候连构造函数都不可控了呢?
    changnet
        12
    changnet  
       Dec 19, 2018 via Android
    @justou
    @wutiantong

    构造函数没有返回值,只能抛出异常,按 c++的设定也能处理,但现实很残酷

    一个全局 静态变量,成员变量在构造函数中抛出异常你要怎么写。构造异常时,这个对象已申请的资源怎么释放

    更别说我见过在多数人当 c 写,通常用返回值判断,你这构造失败就是留下个大坑
    xiaottt
        13
    xiaottt  
       Dec 19, 2018 via iPhone
    C++推荐两阶段构造,即构造函数不要太复杂,让它几乎不会构造失败,复杂的初始化逻辑放到 init 函数中去执行。
    innoink
        14
    innoink  
       Dec 19, 2018
    @changnet 构造函数抛出异常是很常见也很正常的事,需要注意的是析构函数不能抛异常。那种为了避免异常而采用额外 init()的做法纯属增加心智负担。c++采用各种方式保证采用 RAII 管理的资源,在异常产生时自动析构,比如基类和初始化列表。只需要编写构造函数时注意在抛出异常之前,手动回收手工申请的资源(你就算不用异常,也不得不这样做)。

    如果是全局变量的构造出现异常,其实也有办法 catch。1,构造函数特殊的 try/catch 写法 A() try {}catch{};2,set_terminate() 。只不过最终都逃不了 abort()
    innoink
        15
    innoink  
       Dec 19, 2018   1
    @xiaottt 你会发现 init 如果失败了,处理方式和直接处理构造异常没什么区别。
    justou
        16
    justou  
       Dec 19, 2018
    @changnet 你说"全局变量构造失败是个坑"或者"main 运行之前的异常"就清晰了, 如果全局变量初始化很可能会抛异常, 要么避免全局变量, 要么像上面提到的用单例, 在工厂函数中处理异常. 构造函数是完全可控的, 不然 RAII 就失去意义了.

    https://wiki.sei.cmu.edu/confluence/display/cplusplus/ERR58-CPP.+Handle+all+exceptions+thrown+before+main()+begins+executing
    zoutie126
        17
    zoutie126  
       Dec 19, 2018
    应该是全局变量构造顺序的问题,可能早于主线程构造,由于这个线程占用全部时间片,导致主线程被阻塞。
    innoink
        18
    innoink  
       Dec 19, 2018
    @zoutie126 不是,这个线程跑空函数,直接构造完后就退了。这是 windows 处理 DLL 的一些问题。
    snnn
        19
    snnn  
       Dec 20, 2018 via Android   1
    前面有人说了,loader lock。
    zwh2698
        20
    zwh2698  
       Dec 20, 2018 via Android
    学 c++怎么也绕不过操作系统,既然绕不过,那就了解,Windows 核心编程可以帮你解决这种情况。真心推荐。Linux 上楼下补。
    zwh2698
        21
    zwh2698  
       Dec 20, 2018 via Android
    另外不要 wait 也没事,简单黑中线程注入都是这么干的
    ZouZhiZhang
        22
    ZouZhiZhang  
       Dec 20, 2018 via iPhone
    @v2qwsdcv --whole-archive
    ZouZhiZhang
        3
    ZouZhiZhang  
       Dec 20, 2018 via iPhone
    dllmain 有锁,然后运行时初始化全局变量也在 dllmain,开线程会调用 dllmain 的 thread attach,几就死锁了…
    macha
        24
    macha  
       Dec 20, 2018
    第一反应就是 dllmain 的 deadlock,没想到现在还有人讨论 Windows 的编程。
    v2qwsdcv
        25
    v2qwsdcv  
       Dec 20, 2018
    @ZouZhiZhang 貌似不行啊 是不是我用错了
    g++ --std=c++11 -fPIC -shared -o libdy.so -Wl,--whole-archive dynamic.o -Wl,--no-whole-archive
    v2qwsdcv
        26
    v2qwsdcv  
       Dec 20, 2018
    @ZouZhiZhang 依然不能导出自定义类型的 全局变量。
    inoki
        27
    inoki  
       Dec 20, 2018 via Android
    看到两段构造,想到 obc
    About     Help     Advertise     Blog     API     FAQ     Solana     3318 Online   Highest 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 102ms UTC 13:15 PVG 21:15 LAX 06:15 JFK 09:15
    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