线程局部存储漫谈

线程局部存储漫谈

引子

前段时间写代码, 发现一个很有趣的 core

(gdb) bt
#0 0x00007fd60206b4e0 in ?? ()
#1 0x00007fd618431b19 in __nptl_deallocate_tsd () from /lib64/libpthread.so.0
#2 0x00007fd61843278b in start_thread () from /lib64/libpthread.so.0
#3 0x00007fd617d219ad in clone () from /lib64/libc.so.6

gdb 输出可以看出, 程序 core 在了系统库 libc.so.6 中,没什么头绪, 只好求助万能的google, 果然有所收获.

Crashes in __nptl_deallocate_tsd() are symptomatic of invalid dangling thread key cleanup function (the callback to pthread_key_create() ). Most probably a library registered a thread-local variable with a callback, then failed to delete the thread-local variable and got unloaded ( dlclose() ).

因为代码中确实动态加载的so, 并且在不少地方启用了线程局部变量. 所以在我看来上述描述比较清楚.

  1. 多线程应用程序 main 通过 dlopen() 动态加载 so ,
  2. 在线程 thread1 中调用了 so 中的方法 foo() ;
  3. foo() 通过 pthread_key_create() 启用了线程局部存储.
  4. pthread_key_create(&key, destructor) 通过注册销毁函数 destructor 确保线程退出的时候能析构线程局部变量
  5. main 调用 dlclose() 卸载 so , 于是so中的符号不能再被引用, destructor 变成未定义符号
  6. thread1 退出, 调用 destructor 销毁线程局部变量, 发现符号未定义, coredump

解决这个 bug , 只能通过 gdb 设置断点在 pthread_key_create , 然后逐个排查 backtrace 栈帧信息, 直到找到处于未定义的符号.

为了重现这个问题, 我编写了一段小程序.
main.cpp 做了三件事情

  1. 动态加载so
  2. 启动线程, 运行so中的代码
  3. 等待so的代码被执行完毕, sleep()
  4. 回收线程

程序例子

// main.cpp
#include 
#include 
#include 
#include 

typedef void (*FuncType)(void);
static void *thread_fn(void *arg)
{
    FuncType func = (FuncType)arg;
    func();
    sleep(2);
    return (NULL);
}
int main()
{
    void *handler = dlopen("./libfirst.so", RTLD_NOW);
    if(!handler)
    {
        printf("%s\n", dlerror());
        return -1;
    }
    FuncType func = reinterpret_cast(dlsym(handler, "process"));
    if(NULL == func)
    {
        printf("first func is null\n");
        return -2;
    }
    int i = 0; int n = 10;
    pthread_t tids[10];
    for (i = 0; i < n; i++)
    {
        pthread_create(&tids[i], NULL, thread_fn, (void *)func);
    }
    sleep(1);
    dlclose(handler);
    for (i = 0; i < n; i++)
    {
        pthread_join(tids[i], NULL);
    }
    return 0;
}

first.cpp 用于生成so( libfirst.so ), 代码中, 它启用了线程局部变量,并注册相应的销毁函数 destructor()

程序例子

// first.cpp
#include 
#include 
#include 

pthread_key_t key;
pthread_once_t once = PTHREAD_ONCE_INIT;

static void destructor(void *ptr)
{
    free(ptr);
}

void init_once(void)
{
    pthread_key_create(&key, destructor);
}

extern "C" void process()
{
    pthread_once(&once, init_once);

    void *ptr;
    if ((ptr = pthread_getspecific(key)) == NULL)
    {
    fprintf(stdout, "malloc 1024 byte\n");
    ptr = malloc(1024);
    pthread_setspecific(key, ptr);
    }
    return;
}

编译并运行

$ g++ -g -o main main.cpp -ldl -lpthread
$ g++ -g -fPIC -c -o first.o first.cpp
$ g++ -shared -o libfirst.so first.o
$ ulimit -c unlimited
$ ./main
Segmentation fault (core dumped)

查看core dump

$ gdb -c core.15780 main
(gdb) bt
#0  0x00002b70bd102912 in ?? ()
#1  0x00000032f2405ac9 in __nptl_deallocate_tsd () from /lib64/libpthread.so.0
#2  0x00000032f24064b5 in start_thread () from /lib64/libpthread.so.0
#3  0x00000032f18d3c2d in clone () from /lib64/libc.so.6

建议

从上问中, 我得到的教训是:

  • 动态加载的so中尽量不用线程局部存储.

历史问题

我查阅了相关的资料, 发现线程局部存储(TLS)是一个后来者, 产生于多线程概念之后.而在软件发展的早期, 全局变量经常用在库函数中, 用于存储全局信息, 比如 errno , 多线程程序产生之后, 全局变量 errno 就成为所有线程都共享的一个变量, 而实际上, 每个线程都想维护一份自己的 errno , 隔离于其他线程.

这个时候, 没人愿意去修改库函数的接口. 于是线程局部存储就诞生了, 根据 wikipedia 的介绍

Thread-local storage (TLS) is a computer programming method that uses static or global memory local to a thread.

为了在各个平台上都能用上线程局部变量, POSIX Thread 定义了一组接口, 用于显式构造使用线程局部存储.

#include 

int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
int pthread_key_delete(pthread_key_t key);

void *pthread_getspecific(pthread_key_t key);
int pthread_setspecific(pthread_key_t key, const void *value);

显式构造线程局部变量的方法, 有一个显著优点就是能注册各种类型的对象, 包括内置对象和自定义对象. 而且对象的销毁方式 destructor 可以显式告诉 pthread_key_create , 这样线程退出的时候, 线程局部变量就可以正常销毁, 不至于造成内存泄露.

优点再多, 也禁不住它太难用了, 于是有人就想在编译器添加新功能, 支持特定关键字 __thread , 隐式构造线程局部变量

 __thread int i;
 extern __thread struct state s;
 static __thread char *p;

这样的方式, 使用起来是很方便, 但是需要操作系统, 编译器, 连接器, glibc 要相应做出修改, 甚至 ELF 文件格式都需要调整, 这个Ulrich Drepper在 tls.pdf 中做了详细的介绍.

另一方面, __thread 只支持 POD 类型, 不能用于定义 STL 中的容器和类, 比如 std::string . 非要这么做, 编译器会报错:

main.cpp:8: error: ‘a’ cannot be thread-local because it has non-POD type ‘std::string’
main.cpp:8: error: ‘a’ is thread-local and so cannot be dynamically initialized

gcc 也在 文档 中专门谈到了 Thread-Local , 提到了 _thread 修饰的变量只能做static initialize

In C++, if an initializer is present for a thread-local variable, it must be a constant-expression, as defined in 5.19.2 of the ANSI/ISO C++ standard.

既然线程局部存储有两种使用方式, 而且各有优缺点, 就有人提出结合二者, 开发一个使用更方便, 又能支持 non-POD 类型的实现库. 比如 blog 线程局部变量与 __thread

C++11 也意识到这个问题, 于是在 C++11 中引入了新的关键字 thread_local , Destructor support for thread_local variables 介绍说:

One of the key features is that they support non-trivial constructors and destructors that are called on first-use and thread exit respectively.

除了支持 non-POD 类型的线程局部变量, 它还提到了上文提到的线程局部变量和动态加载so的问题

The compiler can handle constructors, but destructors need more runtime context. It could be possible that a dynamically loaded library defines and constructs a thread_local variable, but is dlclose()’d before thread exit. The destructor of this variable will then have the rug pulled from under it and crash.

解决的思路是实现函数 __cxa_thread_atexit_impl() , 供 libstdc++ 在构造对象的时候调用

int __cxa_thread_atexit_impl (void (*dtor) (void *), void *obj,
                          void *dso_symbol);

连接器( ld )为 dso_symbol 所属的 so 维护一个引用计数, 维护由它定义的线程局部变量个数. 如果某个线程局部变量被析构, 那引用计数相应减1, 只有引用计数等于0, dlclose() 才能卸载 so .

其他视角

Walter Bright 在文章 It’s Not Always Nice To Share 中认为现有的线程局部变量实现都不友好, 在多线程环境下, staticglobal 的变量应该默认就是TLS的, 而不是 shared , 这样单核时代的代码, 比如C运行库,不用改动就可以运行在多线程环境中; 如果应用程序非要全局 shared 的变量, 那应该加上 shared 关键字以明确指明.

总结

再回头看看上文的 建议 , 如果非要在动态加载so中使用线程局部变量.

  1. 显式线程局部变量:
    • pthread_key_create 注册了 destructor , 在 dlclose() 调用之前, 确保调用 pthread_key_delete() 删除线程局部变量
    • pthread_key_create() 中的 destructor 置为NULL.
  2. 隐式线程局部变量: 因为只支持 POD 类型, 所以可以用在动态加载so中.

参考目录

  1. http://goog-perftools.sourceforge.net/doc/tcmalloc.html
  2. 程序员的自我修养
  3. http://en.wikipedia.org/wiki/Thread-local_storage
  4. http://www.akkadia.org/drepper/tls.pdf
  5. http://zh.wikipedia.org/wiki/POD_(%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1)
  6. http://stackoverflow.com/questions/5945897/what-is-dynamic-intialization-of-object-in-c
  7. http://gcc.gnu.org/onlinedocs/gcc-4.3.2/gcc/Thread_002dLocal.html
  8. http://www.drdobbs.com/cpp/its-not-always-nice-to-share/217600495
我来评几句
登录后评论

已发表评论数()