V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
sky2017
V2EX  ›  C

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

  •  
  •   sky2017 · 2018-12-19 15:53:37 +08:00 · 4816 次点击
    这是一个创建于 2200 天前的主题,其中的信息可能已经有所发展或是发生改变。

    偶然间在项目里遇到一个问题,问题是这样产生的:在 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 条回复    2018-12-20 14:51:48 +08:00
    jukka
        1
    jukka  
       2018-12-19 16:49:09 +08:00
    try catch 下 m_thread = std::thread([] {}); 看下有没 exception。
    GeruzoniAnsasu
        2
    GeruzoniAnsasu  
       2018-12-19 16:56:18 +08:00
    ……………………感觉是个天坑

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

    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
       2018-12-19 17:13:42 +08:00
    @jukka 没有 exception
    sky2017
        4
    sky2017  
    OP
       2018-12-19 17:15:48 +08:00
    @GeruzoniAnsasu 确实是个坑,害我浪费了好多时间,发现有人已经踩过坑了: https://blog.csdn.net/norsd/article/details/50409585
    谢谢你的建议!
    arzterk
        5
    arzterk  
       2018-12-19 17:16:31 +08:00
    dll 加载会有 Loader Lock
    arzterk
        6
    arzterk  
       2018-12-19 17:17:13 +08:00
    sky2017
        7
    sky2017  
    OP
       2018-12-19 17:43:54 +08:00
    @arzterk 谢谢,这个解答更详细了
    v2qwsdcv
        8
    v2qwsdcv  
       2018-12-19 18:03:36 +08:00
    动态链接库不是 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  
       2018-12-19 18:24:48 +08:00 via Android
    为什么要在构造函数里加复杂代码?构造函数不可控的
    justou
        10
    justou  
       2018-12-19 18:54:39 +08:00
    @changnet 想请教一下为什么说构造函数不可控?

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

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

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

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

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

    https://wiki.sei.cmu.edu/confluence/display/cplusplus/ERR58-CPP.+Handle+all+exceptions+thrown+before+main()+begins+executing
    zoutie126
        17
    zoutie126  
       2018-12-19 22:32:22 +08:00
    应该是全局变量构造顺序的问题,可能早于主线程构造,由于这个线程占用全部时间片,导致主线程被阻塞。
    innoink
        18
    innoink  
       2018-12-19 22:40:06 +08:00
    @zoutie126 不是,这个线程跑空函数,直接构造完后就退了。这是 windows 处理 DLL 的一些问题。
    snnn
        19
    snnn  
       2018-12-20 00:59:47 +08:00 via Android   ❤️ 1
    前面有人说了,loader lock。
    zwh2698
        20
    zwh2698  
       2018-12-20 08:59:19 +08:00 via Android
    学 c++怎么也绕不过操作系统,既然绕不过,那就了解,Windows 核心编程可以帮你解决这种情况。真心推荐。Linux 上楼下补。
    zwh2698
        21
    zwh2698  
       2018-12-20 09:01:54 +08:00 via Android
    另外不要 wait 也没事,简单黑中线程注入都是这么干的
    ZouZhiZhang
        22
    ZouZhiZhang  
       2018-12-20 09:24:53 +08:00 via iPhone
    @v2qwsdcv --whole-archive
    ZouZhiZhang
        23
    ZouZhiZhang  
       2018-12-20 09:26:42 +08:00 via iPhone
    dllmain 有锁,然后运行时初始化全局变量也在 dllmain,开线程会调用 dllmain 的 thread attach,几就死锁了…
    macha
        24
    macha  
       2018-12-20 10:08:14 +08:00
    第一反应就是 dllmain 的 deadlock,没想到现在还有人讨论 Windows 的编程。
    v2qwsdcv
        25
    v2qwsdcv  
       2018-12-20 13:51:12 +08:00
    @ZouZhiZhang 貌似不行啊 是不是我用错了
    g++ --std=c++11 -fPIC -shared -o libdy.so -Wl,--whole-archive dynamic.o -Wl,--no-whole-archive
    v2qwsdcv
        26
    v2qwsdcv  
       2018-12-20 13:52:09 +08:00
    @ZouZhiZhang 依然不能导出自定义类型的 全局变量。
    inoki
        27
    inoki  
       2018-12-20 14:51:48 +08:00 via Android
    看到两段构造,想到 obc
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2842 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 11:28 · PVG 19:28 · LAX 03:28 · JFK 06:28
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.