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

2018-12-19 15:53:37 +08:00
 sky2017

偶然间在项目里遇到一个问题,问题是这样产生的:在 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 方法不对,但是有高手能分析一下原因吗?奇怪的是网上也搜不到答案。

4809 次点击
所在节点    C
27 条回复
jukka
2018-12-19 16:49:09 +08:00
try catch 下 m_thread = std::thread([] {}); 看下有没 exception。
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
2018-12-19 17:13:42 +08:00
@jukka 没有 exception
sky2017
2018-12-19 17:15:48 +08:00
@GeruzoniAnsasu 确实是个坑,害我浪费了好多时间,发现有人已经踩过坑了: https://blog.csdn.net/norsd/article/details/50409585
谢谢你的建议!
arzterk
2018-12-19 17:16:31 +08:00
dll 加载会有 Loader Lock
arzterk
2018-12-19 17:17:13 +08:00
sky2017
2018-12-19 17:43:54 +08:00
@arzterk 谢谢,这个解答更详细了
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
2018-12-19 18:24:48 +08:00
为什么要在构造函数里加复杂代码?构造函数不可控的
justou
2018-12-19 18:54:39 +08:00
@changnet 想请教一下为什么说构造函数不可控?

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

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

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

更别说我见过在多数人当 c 写,通常用返回值判断,你这构造失败就是留下个大坑
xiaottt
2018-12-19 21:35:57 +08:00
C++推荐两阶段构造,即构造函数不要太复杂,让它几乎不会构造失败,复杂的初始化逻辑放到 init 函数中去执行。
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
2018-12-19 21:48:51 +08:00
@xiaottt 你会发现 init 如果失败了,处理方式和直接处理构造异常没什么区别。
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
2018-12-19 22:32:22 +08:00
应该是全局变量构造顺序的问题,可能早于主线程构造,由于这个线程占用全部时间片,导致主线程被阻塞。
innoink
2018-12-19 22:40:06 +08:00
@zoutie126 不是,这个线程跑空函数,直接构造完后就退了。这是 windows 处理 DLL 的一些问题。
snnn
2018-12-20 00:59:47 +08:00
前面有人说了,loader lock。
zwh2698
2018-12-20 08:59:19 +08:00
学 c++怎么也绕不过操作系统,既然绕不过,那就了解,Windows 核心编程可以帮你解决这种情况。真心推荐。Linux 上楼下补。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/519019

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX