这篇文章汇总了我最近踩的一个莫名其妙的坑: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搜到一个类似的问题,但还是不一样。

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.soliba.so则使用动态链接的方式引用libc.so。如果libc.so中使用了pthread来支持c++11的线程,则可执行程序b也可能出现segment fault的case。一种方法是让可执行程序使用动态链接的方式直接链接到liba.so,另一种,则是让bliba.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)