偶然间在项目里遇到一个问题,问题是这样产生的:在 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 方法不对,但是有高手能分析一下原因吗?奇怪的是网上也搜不到答案。
1
jukka 2018-12-19 16:49:09 +08:00
try catch 下 m_thread = std::thread([] {}); 看下有没 exception。
|
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 的实现,这在不同平台肯定又是不一样的,一个你得在对应平台自己调,一个你调出来了换个平台不一定还会复现,所以何必去踩呢 |
4
sky2017 OP @GeruzoniAnsasu 确实是个坑,害我浪费了好多时间,发现有人已经踩过坑了: https://blog.csdn.net/norsd/article/details/50409585
谢谢你的建议! |
5
arzterk 2018-12-19 17:16:31 +08:00
dll 加载会有 Loader Lock
|
6
arzterk 2018-12-19 17:17:13 +08:00
|
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 ``` |
9
changnet 2018-12-19 18:24:48 +08:00 via Android
为什么要在构造函数里加复杂代码?构造函数不可控的
|
11
wutiantong 2018-12-19 18:55:07 +08:00
@changnet 啥时候连构造函数都不可控了呢?
|
12
changnet 2018-12-19 20:18:37 +08:00 via Android
@justou
@wutiantong 构造函数没有返回值,只能抛出异常,按 c++的设定也能处理,但现实很残酷 一个全局 静态变量,成员变量在构造函数中抛出异常你要怎么写。构造异常时,这个对象已申请的资源怎么释放 更别说我见过在多数人当 c 写,通常用返回值判断,你这构造失败就是留下个大坑 |
13
xiaottt 2018-12-19 21:35:57 +08:00 via iPhone
C++推荐两阶段构造,即构造函数不要太复杂,让它几乎不会构造失败,复杂的初始化逻辑放到 init 函数中去执行。
|
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() |
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 |
17
zoutie126 2018-12-19 22:32:22 +08:00
应该是全局变量构造顺序的问题,可能早于主线程构造,由于这个线程占用全部时间片,导致主线程被阻塞。
|
19
snnn 2018-12-20 00:59:47 +08:00 via Android 1
前面有人说了,loader lock。
|
20
zwh2698 2018-12-20 08:59:19 +08:00 via Android
学 c++怎么也绕不过操作系统,既然绕不过,那就了解,Windows 核心编程可以帮你解决这种情况。真心推荐。Linux 上楼下补。
|
21
zwh2698 2018-12-20 09:01:54 +08:00 via Android
另外不要 wait 也没事,简单黑中线程注入都是这么干的
|
22
ZouZhiZhang 2018-12-20 09:24:53 +08:00 via iPhone
@v2qwsdcv --whole-archive
|
23
ZouZhiZhang 2018-12-20 09:26:42 +08:00 via iPhone
dllmain 有锁,然后运行时初始化全局变量也在 dllmain,开线程会调用 dllmain 的 thread attach,几就死锁了…
|
24
macha 2018-12-20 10:08:14 +08:00
第一反应就是 dllmain 的 deadlock,没想到现在还有人讨论 Windows 的编程。
|
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 |
26
v2qwsdcv 2018-12-20 13:52:09 +08:00
@ZouZhiZhang 依然不能导出自定义类型的 全局变量。
|
27
inoki 2018-12-20 14:51:48 +08:00 via Android
看到两段构造,想到 obc
|