这篇文章汇总了我最近踩的一个莫名其妙的坑:Linux下CMake中使用pthread支持多线程编程。
# 问题描述
问题的代码可以参考lanphon/test_thread_dlopen。总的来说,我需要建立一个动态链接库,a
,然后在一个测试的可执行程序b
中去调用a
所提供的功能。一般而言,使用库有两种链接方式,静态链接和动态链接。动态链接则分为直接连接和使用API的方式打开库的方式链接。 问题就出现在最后一种情况。
程序b
使用Linux下的dlopen()
函数打开动态链接库liba.so
,然后用dlsym()
获取函数的地址,这里只有一个初始化函数init()
,创建并detach了一个线程。但是,测试过程中发现创建线程的时候总是报SIGSEGV,段错误。用GDB调试也能发现callstack中解引用了一个0,问题是,从callstack上根本看不出来这个0是怎么传进去的。
库代码本身没问题,无论是静态链接,还是直接的动态链接,都可以正常初始化。唯独使用dlopen()
这种打开库文件的方式,无法初始化。
# Bug追踪
GDB调试此时已经无能为力,因为callstack上怎么都看不出空指针是怎么传进去的。测试环境是Ubuntu 18.04,GCC 7.4和8.4都测试过了,问题仍旧,但clang-9下没有BUG。网上有人说GCC-9.3版本无法复现,推测是GCC低版本的bug。去GCC bug trace搜到一个类似的问题,但还是不一样。又搜索了一圈,终于在using std::thread in a library loaded with dlopen leads to a sigsev找到了解释。
C++11以来,C++引入了标准线程库std::thread
。标准线程库的实现由各平台自行决定。在C++有标准线程库之前,Linux下已经存在了一个广受好评(或者说,不得不用)的一个线程库,pthread。所以Linux上的std::thread
其实就是对之前存在的pthread的一层包装。 Linux下一般使用的C++实现是libstdc++,由于设计原因,线程库以单独的libpthread提供,并且libstdc++使用弱链接的方式引用libpthread。
如果使用dlopen()
的方式打开动态链接库,但可执行程序没有对pthread
的依赖,那么相关的线程函数都会为空。这样尽管所打开的动态链接库已经链接了pthread
,程序仍然会执行出错:解引用了空的函数指针。
# Hack解决方法
既然是libstdc++的bug,一个简单方法就是换,可选的还有libc++。当然这种方法太暴力了。另一种选择就是升级GCC版本,或者使用CLANG编译器替代GCC。
GCC的这个BUG,目前还没有直接的解决方法。不过为了work-around, GCC提供了一个额外的命令行选项-pthread
来解决。这个选项-pthread很容易和链接选项-lpthread混淆,这一点需要额外注意。-lpthread是个链接器选项,显式指明生成的对象(无论是库还是可执行程序)依赖的库(这里指明依赖pthread库)。然而-pthread不仅仅是一个链接选项,还是一个编译选项,指明需要定义一些宏来使用pthread。
# CMake的解决方法
CMake中,可以使用
set_target_properties(${TARGET} PROPERTIES
COMPILE_FLAGS "-pthread"
LINK_FLAGS "-pthread")
的方式,强制为编译和链接增加选项-pthread
。注意这部分代码不能用
target_link_libraries(${TARGET} pthread)
代替,因为后者会扩展为-lpthread
,并且仅对链接阶段生效。
这样的方法不适合跨平台的使用,并且在目标比较多的时候,添加起来比较麻烦。CMAKE中提供了单独的Threads
库来解决这个问题。
add_library(test ${src})
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
target_link_libraries(test PUBLIC Threads::Threads)
这里我们将目标test
对线程库Threads::Threads
的链接属性设置为PUBLIC
,这样随后如果有目标静态链接 test
或者动态链接test
,都可以隐式地加入对Threads::Threads
的依赖。那些不直接依赖动态链接库,而是使用dlopen()
的方式打开动态链接库的目标,则必须手动添加对Threads::Threads
的依赖。
# 注意事项
这个隐藏很深的BUG,在多个动态链接库中也会存在。例如,可执行程序b
使用dlopen()
的方式引用liba.so
,liba.so
则使用动态链接的方式引用libc.so
。如果libc.so
中使用了pthread
来支持c++11的线程,则可执行程序b
也可能出现segment fault的case。一种方法是让可执行程序使用动态链接的方式直接链接到liba.so
,另一种,则是让b
和liba.so
都链接到pthread
库上去。
如果出现这个bug,请用ldd
工具检查调用序列上每个动态链接库的依赖库,看是否是那一层缺少了对pthread
的依赖。目前的经验是,如果一个目标使用dlopen()
打开一个动态链接库,而这个动态链接库本身依赖pthread
,或者动态链接库所依赖的链接库依赖pthread
,则这个目标自己也必须链接到pthread
上,否则可能会出现空指针的引用错误。
b.cpp
#include "a.h"
#include<thread>
#include<chrono>
#include<dlfcn.h>
#include<iostream>
using namespace std;
using func_init = decltype(&init);
int main(int argc, char **argv)
{
const char *name = "./liba.so";
void *handle = dlopen(name, RTLD_NOW);;
if (handle == nullptr)
{
std::cerr << dlerror() << std::endl;
}
func_init l_init;
#ifdef _MSC_VER
#define GetFunc(FUNC_NAME) l_##FUNC_NAME = (func_##FUNC_NAME)GetProcAddress(handle, #FUNC_NAME)
#else
#define GetFunc(FUNC_NAME) l_##FUNC_NAME = (func_##FUNC_NAME)dlsym(handle, #FUNC_NAME)
#endif
GetFunc(init);
if (!l_init)
{
std::cerr << "fail to load library" << std::endl;
}
l_init();
#undef GetFunc
dlclose(handle);
}
a.h
extern "C" void init();
a.cpp
#include<thread>
#include<unistd.h>
#include<iostream>
#include "a.h"
class test_t
{
public:
test_t(){}
static test_t& get_instance()
{
static test_t t;
return t;
}
void process()
{
}
void detach()
{
auto t = std::thread([]{
auto& d = test_t::get_instance();
while(true)
{
d.process();
}
});
std::cout << t.get_id() << std::endl;
t.detach();
}
private:
};
extern "C" void init()
{
test_t::get_instance().detach();
}
CMakeLists.txt
Cmake_minimum_required(VERSION 3.10)
include_directories(.)
add_definitions(-ggdb)
add_library(a SHARED a.cpp)
target_link_libraries(a PUBLIC pthread)
add_executable(b b.cpp)
target_link_libraries(b a dl)