线程局部存储tls的使用

线程局部存储(Thread Local Storage,TLS)主要用于在多线程中,存储和维护一些线程相关的数据,存储的数据会被关联到当前线程中去,并不需要锁来维护。。

因此也没有多线程间资源竞争问题,那如何去实现TLS存储呢,主要有以下几种方式:

  1. gcc和clang的__thread修饰符
  2. windows下msvc的__declspec(thread)修饰符
  3. pthread库pthread_setspecificpthread_getspecific接口
  4. windows下的TlsSetValueTlsGetValue

__thread和__declspec(thread)的使用

其中__thread和__declspec(thread)用起来最为方便,只需要在static或者全局变量前加上此修饰符,然后在线程里面访问变量就行了

例如:

tb_void_t tb_thread_func(tb_cpointer_t priv)
{
    // 定义一个线程局部变量
    static __thread int a = 0;
        
    // 初始化这个变量,设置为当前线程id
    if (!a) a = tb_thread_self();
}

如果运行多个线程的话,上述代码中每个线程的变量a的值,都是不相同的,值为每个线程的id

__declspec(thread) 用起来也是类似,只需替换下 __thread就行了

虽然这两个修饰符用起来很方便,但是需要编译器支持,虽然现在大部分平台的编译器都已支持,但是作为跨平台开发,这样还是不够的

毕竟还是有不少低版本gcc,不一定支持__thread,尤其是嵌入式开发领域,交叉编译工具链中的编译器支持力度差异还是蛮大的。。

另外,使用__thread进行tls数据维护,需要手动管理相关内存的释放问题,用的不好很容易导致内存泄露。。

pthread接口

pthread 的 tls 相关接口,比较完善,并且支持注册free函数,在线程退出的时候,自动释放相关tls数据,避免内存泄露,但是使用上稍显复杂了些

我们看个简单的例子:

// 测试线程中tls变量存储的key,需定义为全局或者static
static pthread_key_t g_local_key = 0;

static tb_void_t tb_thread_local_free(tb_pointer_t priv)  
{  
    tb_trace_i("thread[%lx]: free: %p", tb_thread_self(), priv);
}  
static tb_void_t tb_thread_local_init(tb_void_t)
{
    // 创建tls的key,并且设置自动释放函数
    pthread_key_create(&g_local_key, tb_thread_local_free);
}
static tb_int_t tb_thread_local_test(tb_cpointer_t priv)
{
    // 在所有线程中,仅执行一次,用于在线程内部初始化 tls 的 key
    static pthread_once_t s_once = PTHREAD_ONCE_INIT;
    pthread_once(&s_once, tb_thread_local_init);

    // 尝试读取当前tls数据
    tb_size_t local;
    if (!(local = (tb_size_t)pthread_getspecific(g_local_key)))
    {
        // 设置tls数据为当前线程id
        tb_size_t self = tb_thread_self();
        if (0 == pthread_setspecific(g_local_key, (tb_pointer_t)self))
            local = self;
    }

    return 0;
}

看上去复杂了些,但是更加灵活,如果不需要在线程内部创建key的话,就不需要调用pthread_once了,直接把创建好的key传入线程内部去访问就好。

TlsSetValue 接口

此套接口(TlsSetValue, TlsGetValue, TlsAlloc, TlsFree),属于windows的tls操作接口,当然不能跨平台了,使用起来和pthread的差不多,但是无法注册自动释放函数,并且也没提供类似pthread_once的接口
在线程内部自创建key,功能上稍显不足。。

static tb_int_t tb_thread_local_test(tb_cpointer_t priv)
{
    // 创建一个tls的key,注:此处非线程安全,最好放到类似pthread_once提供的init函数中去创建
    // 此处就临时先这么写了,仅仅只是为了方便描述api用法,不要照搬哦。。
    static DWORD s_key = 0;
    if (!s_key) s_key = TlsAlloc();

    // 尝试读取当前tls数据
    DWORD local;
    if (!(local = TlsGetValue(s_key))) 
    {
        // 设置tls数据为当前线程id
        tb_size_t self = tb_thread_self();
        if (TlsSetValue(s_key, (LPVOID)self))
            local = self;
    }

    return 0;
}

其实windows上还提供了FlsAlloc, FlsSetValue系列接口,给协程使用,并且支持注册自动释放的回调函数,不过对系统版本有些要求,像xp这些老系统就用不了了。。
这里就不多描述了。

tbox提供的thread_local接口封装

最近对tbox的tls接口进行了改造,并且重构了实现逻辑,在剪口易用性、功能性以及效率上都得到了很大的提升。。

目前支持以下功能:

  • 支持注册自动释放回调,保证在线程退出时,自动释放设置的tls数据
  • 支持在线程内部进行线程安全的key创建
  • tbox退出时会自动销毁所有创建的key,当然也可以提前主动销毁它

用起来也很方便,很pthread很类似,但是内部自动调用了pthread_once,不用想pthread那样显式的去调用它了,例如:

static tb_void_t tb_demo_thread_local_free(tb_cpointer_t priv)
{
    tb_trace_i("thread[%lx]: free: %p", tb_thread_self(), priv);
}
static tb_int_t tb_demo_thread_local_test(tb_cpointer_t priv)
{
    /* 线程安全地初始化一个tls对象,相当于key,并且注册自动free回调
     *
     * 注:虽然所有线程都会执行到这个tb_thread_local_init
     *     但是s_local的tls对象,只会确保初始化一次,内部有类似pthread_once接口来维护
     */
    static tb_thread_local_t s_local = TB_THREAD_LOCAL_INIT;
    if (!tb_thread_local_init(&s_local, tb_demo_thread_local_free)) return -1;

    // 尝试读取当前tls数据
    tb_size_t local;
    if (!(local = (tb_size_t)tb_thread_local_get(&s_local)))
    {
        // 设置tls数据为当前线程id
        tb_size_t self = tb_thread_self();
        if (tb_thread_local_set(&s_local, (tb_cpointer_t)self))
            local = self;
    }

    return 0;
}

在线程退出时,它会自动调用free回调,释放对应残留的tls数据,并且在tb_exit退出后,销毁所有创建的tls对象

当然你可以可以主动调用:tb_thread_local_exit(&s_local) 来销毁它。。

tbox的这套接口,相比pthread减少了一个init的回调函数,比windows那套多了自动释放的机制,并且同时支持跨平台。。

杂谈

之前我看到一些库中,对__thread和pthread接口进行了混用,感到很是莫名,个人感觉是有问题的,例如:

static __thread pthread_key_t g_key;

原本pthread文档中就明确表述key需要全局或者static存储,而这里加上__thread后,其实每个线程访问的key都不是同一个key了哦。。

总结

如果仅仅只是想用来在线程内部存储一些简单的int数据,并且不考虑完备的跨平台支持,那么建议直接使用__thread或者__declspec(thread)j就行了,非常方便易用

如果要考虑跨平台操作的话,tbox的tls接口也是个不错的选择哦。。


个人主页:TBOOX开源工程
原文出处:http://tboox.org/cn/2016/09/28/thread-local/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容