linux中的线程局部存储(TLS)实现线程间的同步和互斥pthread_key_create、pthread_setspecific、pthread_getspecific

目录

是什么

实现方式:

linux中声明线程局部存储变量

使用__thread关键字

使用pthread_key_t类型

线程的局部存储具有以下特点:

线程局部存储的限制

__thread:(使用简单,这里只给出例子)

线程特定键pthread_key_t

pthread_key_create函数

原型:

参数:

返回值:

destructor函数指针:

注意:

pthread_setspecific函数

原型:

参数:

返回值:

注意:

pthread_getspecific函数

原型:

参数:

返回值:

注意:

pthread_key_delete函数

原型:

参数:

返回值:

注意:

示例:

简单使用:

运行结果:

分析:

使用和传递析构函数:

运行结果:

分析:


是什么

线程的局部存储(Thread-Local Storage,TLS)是线程特定存储(Thread-Specific Storage,TSS)的更常用的名称,是一种内存区域,每个线程都可以存储自己的数据,使得不同的线程可以拥有不同的内存空间,相当于进程地址空间的全局变量区,只不过是线程私有的。线程局部存储在操作系统级别提供了一种方式,允许线程独立存储空间,为每个线程维护一份数据副本。这种特性在多线程编程中非常有用,因为它允许每个线程独立地存储自己的数据,避免了线程间的数据冲突和竞态条件。这使得编程更简单、更安全,并且减少了错误的可能性。

线程局部存储主要适用于需要高效处理大量并发线程的环境。它在很多高级语言中(如C++)也有实现,比如通过智能指针或者RAII(Resource Acquisition Is Initialization)来管理线程特定的数据。

实现方式:
  1. Thread Local Storage (TLS) API:操作系统提供了TLS的API,可以让程序员创建和管理每个线程的局部存储变量。这些API通常由操作系统库或线程库提供,并可以在程序中直接使用。

  2. 编程语言支持:一些编程语言提供了内置的机制来支持线程的局部存储。例如,在Java中,可以使用ThreadLocal类来创建线程本地变量,每个线程都有其自己的副本;在C++中,可以使用thread_local关键字来声明线程局部存储变量。

  3. 动态链接库:有些动态链接库(DLL)或共享对象(SO)提供了自己的TLS支持,可以让程序员将数据关联到线程中。这种方式通常需要使用库提供的特定函数来设置和获取线程局部存储的值。

linux中声明线程局部存储变量
使用__thread关键字

要声明一个线程局部存储变量,可以在变量声明前加上__thread关键字,例如:

__thread int my_variable;

这将声明一个线程局部存储的整数变量my_variable,每个线程都有自己的副本。在多线程环境中,不同线程可以访问和修改自己的副本,而不会影响其他线程的数据。

使用pthread_key_t类型

声明一个线程局部存储的整数变量tls_key

pthread_key_t tls_key;

线程的局部存储具有以下特点:
  1. 线程隔离:每个线程都有它自己的局部存储空间,可以在该空间中存储线程特定的数据,不同线程之间的数据互相隔离,避免了数据冲突和竞态条件。

  2. 数据共享:线程的局部存储允许不同线程之间共享代码,而不是对整个进程进行数据共享。这样可以减少线程间的通信开销,提高并发性能。他的作用类似于在普通进程中的全局变量,进程中的所有函数都可以直接调用这个变量而不用传参导致麻烦。

  3. 线程安全:线程的局部存储可以提供一种线程安全的方式来处理数据,每个线程都可以独立地修改和访问自己的局部存储变量,不会对其他线程造成影响。

线程局部存储的限制
  1. 由于每个线程都有自己的存储空间,因此如果线程之间需要共享数据,那么就需要额外的机制来同步访问。

  2. 线程局部存储的空间通常有限制,尤其是在资源受限的环境中。因此,在使用线程局部存储时,需要仔细考虑数据的大小和数量,以确保线程之间的数据不会造成过多的内存占用。不能对自定义的类进行局部存储。

  3. 由于线程局部存储是基于操作系统的特性,因此它可能会受到操作系统实现的影响。不同的操作系统可能会有不同的线程局部存储实现和限制。

__thread:(使用简单,这里只给出例子)
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
​
// 声明线程局部存储变量
__thread int tls_variable;
​
// 线程函数
void* thread_func(void* arg) {
    // 设置线程局部存储变量的值
    tls_variable = pthread_self();
    
    // 打印线程局部存储变量的值
    printf("Thread local variable: %ld\n", tls_variable);
      
    pthread_exit(NULL);
}
​
int main() {
    pthread_t thread1, thread2;
​
    pthread_create(&thread1, NULL, thread_func, NULL);
    pthread_create(&thread2, NULL, thread_func, NULL);
​
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
​
    return 0;
}

在这个例子中,我们声明了一个线程局部存储变量tls_variable,它在每个线程中都有自己的副本。在thread_func线程函数中,我们将线程的ID(通过pthread_self()函数获取)赋值给线程局部存储变量tls_variable。然后,在每个线程中打印该变量的值。

main函数中,我们创建了两个线程,并等待它们结束。每个线程在运行过程中都具有自己的线程局部存储变量,并且变量的值不会被其他线程所修改。

线程特定键pthread_key_t

pthread_key_t 是 POSIX 线程库(pthreads)的一部分,它提供了一种机制,使得每个线程都可以拥有自己的数据,而这些数据不会被其他线程访问。这种机制在多线程程序中非常有用,因为它可以避免全局变量的竞争条件,同时也无需在每个线程中复制数据。

/* Keys for thread-specific data */
typedef unsigned int pthread_key_t;

在 Linux 系统中,pthread_key_t 是一个无符号整数类型,通常在 <pthread.h> 头文件中。你可以使用 pthread_key_create() 函数来创建一个新的线程特定键,使用 pthread_key_delete() 函数来销毁一个线程特定键。

pthread_key_create函数

pthread_key_create 是 POSIX 线程库中的一个函数,用于在多线程环境中创建线程特定数据键。这个函数在调用之后会返回一个键值,该键值用于之后通过 pthread_setspecific 函数将线程特定数据与键关联起来。线程特定数据是一个在多线程环境中保持私有的数据存储空间,每个线程都可以有自己独立的数据副本。

原型:
#include <pthread.h>
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
参数:
  • key 是一个指向指针的指针,该指针会被初始化为新的线程特定数据键的标识符。

  • destructor 是一个指向函数的指针,该函数会在每个线程结束时被调用,用来释放与线程特定数据键关联的内存。

返回值:

这个函数成功时返回0,失败时返回一个非零错误码。(可以在错误宏中查看

destructor函数指针:

当线程结束时,如果指定了 destructor 函数,那么这个函数会被调用,用来清理与键关联的数据。这通常是释放动态分配的内存或执行其他清理工作。如果 destructor 函数为 NULL,则默认情况下,当线程结束时,与键关联的数据不会被自动清理。

如果在程序中创建了多个键,每个键都应该有一个唯一的 destructor 函数,以便在适当的时候释放各自的数据。如果没有为某个键指定 destructor 函数,而且该键又没有关联数据,那么在键被销毁时,不会发生任何事情。

注意:

一旦创建了线程特定数据键,就不能再将其用于其他线程特定数据的存储。此外,pthread_setspecificpthread_getspecific 只能用于创建后设置的键。也就是说,如果一个线程在创建键之前调用了这些函数,那么它们将无法正确工作。

pthread_setspecific函数

这个函数会将线程特定数据与指定的键关联起来,使得之后可以通过 pthread_getspecific 来获取线程特定数据。如果当前线程之前已经关联了指定键的线程特定数据,那么该数据会被新的数据替换。

原型:
#include <pthread.h>
int pthread_setspecific(pthread_key_t key, const void *value);
参数:
  • key 是一个线程特定数据键,通过 pthread_key_create 创建得到。

  • value 是一个指向要关联的线程特定数据的指针。

返回值

这个函数成功时返回 0,失败时返回一个非零错误码。(可以在error宏中查看

注意:
  1. 如果当前线程之前已经关联了指定键的线程特定数据,那么该数据会被新的数据替换。

  2. pthread_setspecific 只能用于创建后设置的键。如果一个线程在创建键之前调用了 pthread_setspecific,该函数将无法正确工作。

pthread_getspecific函数

用于获取与指定键关联的线程特定数据。如果当前线程与指定键关联了线程特定数据,那么该数据会被存储在 value 指向的内存位置中。当前线程没有与指定键关联的线程特定数据,那么 value 将被设置为 NULL

原型:
#include <pthread.h>
int pthread_getspecific(pthread_key_t key, void *value);
参数:
  • key 是一个线程特定数据键,通过 pthread_key_create 得到。

  • value 是一个指向接收线程特定数据的指针。

返回值:

这个函数成功时返回 0,失败时返回一个非零错误码。

注意:
  1. 当前线程没有与指定键关联的线程特定数据,那么 value 将被设置为 NULL

  2. p_getspecific 只能用于创建后设置的键。如果一个线程在创建键之前调用了 pthread_getspecific,该函数将无法正确工作。

pthread_key_delete函数

这个函数可以释放与键相关联的资源,例如键 ,destructor 函数的存储空间。当调用 pthread_key_delete 时,如果键存在,则与该键关联的所有线程特定数据将被清除,且 destructor 函数(如果指定了的话)不会被调用。如果键不再使用,建议调用此函数来释放资源。

原型:
#include <pthread.h>
int pthread_key_delete(pthread_key_t key);
参数:
  • key 是一个线程特定数据键,该键是通过 pthread_key_create 函数创建得到的。

返回值:

这个函数成功时返回 0,失败时返回一个非零错误码。如果尝试删除一个不存在的键,函数也会返回一个非零错误码。

注意:

pthread_key_delete函数会执行destructor函数嘛

pthread_key_delete 函数用于删除一个之前通过 pthread_key_create 函数创建的线程特定数据键。当您删除一个键时,如果该键之前关联了一个 destructor 函数,那么这个 destructor 函数会在所有线程释放该键之前被调用。

destructor 函数是在创建键时通过 pthread_key_create 的第二个参数指定的,它用于在键不再使用时释放与键关联的数据。这个函数只会在键被删除且所有线程都已经释放了该键之后才会被调用。

因此,pthread_key_delete 函数本身不会立即执行 destructor 函数,而是在键被彻底删除且所有线程都已经退出或释放了该键之后,destructor 函数才会被调用。这是为了确保 destructor 函数有机会处理所有线程的线程特定数据,即使在多线程,也有可能某个线程在键被删除之后仍然存活。

示例:
简单使用:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
// 定义一个线程局部存储变量的键
pthread_key_t tls_key;
​
// 线程函数
void *thread_func(void *arg)
{
    // 获取线程局部存储变量的值
    int *data = (int *)pthread_getspecific(tls_key);
    if (data == NULL)
    {
        // 如果这是第一次访问,则为线程分配存储空间
        data = (int *)malloc(sizeof(int));
        pthread_setspecific(tls_key, data);
    }
​
    // 修改线程局部存储变量的值
    *data = pthread_self();
    // 打印线程局部存储变量的值
    printf("Thread local variable: 0x%lld,tls_key : %d\n", *data, tls_key);
​
    pthread_exit(NULL);
}
​
int main()
{
    // 创建线程特定键
    pthread_key_create(&tls_key, NULL);
​
    // 创建线程
    pthread_t thread;
    int *data = (int *)pthread_getspecific(tls_key);
    if (data == NULL)
    {
        data = (int *)malloc(sizeof(int));
        pthread_setspecific(tls_key, data);
    }
    *data = pthread_self();
    pthread_create(&thread, NULL, thread_func, NULL);
    printf("Main Thread local variable: 0x%lld,tls_key : %d\n", *data, tls_key);
    // 等待线程结束
    pthread_join(thread, NULL);
​
    // 销毁线程特定键
    pthread_key_delete(tls_key);
​
    return 0;
}

在这个示例中,我们首先创建了一个线程局部存储变量的键tls_key。在thread_func函数中,我们尝试获取与tls_key关联的数据。如果这是第一次访问,我们为线程分配存储空间,并将其与tls_key关联。然后,我们修改了存储在键中的数据,并打印它的值。

main函数中,我们创建了一个线程,并等待它结束。最后,我们销毁了tls_key,释放了与之关联的所有资源。

运行结果:
[lzh@MYCAT Thread]$ g++ -o myexe --std=c++11 -lpthread thread_5.cc 
[lzh@MYCAT Thread]$ ./myexe 
Main Thread local variable: 0x3483662144,tls_key : 0
Thread local variable: 0x3466811136,tls_key : 0
​
分析:
  1. 线程特定键的键值从1开始分配。

  2. 在使用过程中,可以把这个键值看作是数据的载体的指针,键值虽然一样,不同线程中的数据却不一样。使用时候,我们要用函数来获取,同时用线程中的变量来接受。

  3. 使用pthread_key_t需要注意的是,你需要确保在使用完毕后正确地销毁线程特定键,以免造成内存泄漏。此外,当线程退出时,与之关联的数据并不会自动释放,因此需要手动调用free或其他适当的内存释放函数来避免内存泄漏。

当创建线程特定数据键时,可以通过第二个参数传递一个析构函数指针。这个析构函数会在每个线程退出时自动调用,用于释放与线程特定数据相关联的资源。

使用和传递析构函数:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
pthread_key_t tls_key;
​
// 线程局部存储数据结构
typedef struct
{
    pthread_t id;
    char *name;
} ThreadData;
​
// 析构函数
void releaseThreadData(void *data)
{
    ThreadData *threadData = (ThreadData *)data;
    printf("Releasing thread data for thread %lld\n", threadData->id);
    free(threadData->name);
    free(threadData);
}
​
// 线程函数
void *threadFunc(void *arg)
{
    // 创建线程局部存储数据
    ThreadData *threadData = (ThreadData *)malloc(sizeof(ThreadData));
    threadData->id = pthread_self();
    threadData->name = (char *)malloc(40 * sizeof(char));
    snprintf(threadData->name, 40, "Thread%d", threadData->id);
​
    // 将线程局部存储数据与键关联
    // pthread_setspecific(*(pthread_key_t *)arg, threadData);
    pthread_setspecific(tls_key, threadData);
​
    ThreadData *data = (ThreadData *)pthread_getspecific(tls_key);
    // 打印线程局部存储数据
    printf("Thread local data for thread %lld: %s\n", data->id, data->name);
    printf("Thread want to delete tls_key : %d\n", tls_key);
    // pthread_key_delete(tls_key);
    sleep(2);
    pthread_exit(NULL);
}
​
int main()
{
    // 创建线程特定数据键并指定析构函数
    pthread_key_create(&tls_key, releaseThreadData);
    pthread_t thread1;
    pthread_t thread2;
    ThreadData *threadData = (ThreadData *)malloc(sizeof(ThreadData));
    threadData->id = pthread_self();
    threadData->name = (char *)malloc(40 * sizeof(char));
    snprintf(threadData->name, 40, "Thread%d", threadData->id);
    pthread_setspecific(tls_key, threadData);
​
    
    pthread_create(&thread1, NULL, threadFunc, &tls_key);
    pthread_create(&thread2, NULL, threadFunc, &tls_key);
    printf("Main Thread will sleep 5s\n");
    sleep(5);
    ThreadData *data = (ThreadData *)pthread_getspecific(tls_key);
    printf("Main Thread  data for thread %lld: %s\n", data->id, data->name);
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    printf("Main Thread want to delete tls_key : %d\n", tls_key);
    // 删除线程特定数据键(不会立即调用析构函数)
    pthread_key_delete(tls_key);
    return 0;
}

在这个例子中,我们定义了一个线程局部存储数据结构 ThreadData,其中包含一个 id 和一个 name 字段。然后,我们定义了一个 releaseThreadData 函数作为析构函数。在线程函数 threadFunc 中,我们首先为线程分配一个 ThreadData 结构体,并将其与线程特定数据键关联。在 main 函数中,我们使用 pthread_key_create 创建线程特定数据键,并传递 releaseThreadData 函数作为第二个参数。最后,我们在 main 函数结束之前使用 pthread_key_delete 删除线程特定数据键。

运行结果:
[lzh@MYCAT Thread]$ g++ -o myexe --std=c++11 -lpthread thread_5_1.cc 
[lzh@MYCAT Thread]$ ./myexe 
Main Thread will sleep 5s
Thread local data for thread 140681029367552: Thread-624412928
Thread want to delete tls_key : 0
Thread local data for thread 140681020974848: Thread-632805632
Thread want to delete tls_key : 0
Releasing thread data for thread 140681029367552
Releasing thread data for thread 140681020974848
Main Thread  data for thread 140681046218560: Thread-607561920
Main Thread want to delete tls_key : 0
​

分析:
  1. 如果在子线程中pthread_key_delete(tls_key)我们会发现出现段错误

    [lzh@MYCAT Thread]$ ./myexe 
    Main Thread will sleep 5s
    Thread local data for thread 140060714845952: Thread1831323392
    Thread want to delete tls_key : 0
    Segmentation fault (core dumped)
    ​

    这个错误是立马发生的,说明是在第二个子线程中出现了段错误。

    这里我们能得出删除键是对于所有的线程有效的,只要删除了,所有在使用特定键的线程都会发生错误。

  2. 如果我们没有创建第二个子线程,仍然在子线程中pthread_key_delete(tls_key),那么也会出现段错误。

    [lzh@MYCAT Thread]$ ./myexe 
    Main Thread will sleep 5s
    Thread local data for thread 140211882907392: Thread-1619437824
    Thread want to delete tls_key : 0
    Segmentation fault (core dumped)
    ​

    这个错误是将近五秒后发生的,说明是在主线程中出现了段错误。

    得以验证上边的结论。得出: 一个create ,一个delete 不要随便delete

  3. 正常情况下,我们观察到两个子线程都调用了析构函数,然而主线程却没有调用。而且,两个子线程的析构函数是在子线程结束时候自动调用的,不是主线程delete时候调用的。主线程做了delete动作,如果我们不做这个动作,也是不会调用析构函数的。得出:delete操作不会调用析构函数,析构函数需要显式调用,或者通过子线程正常退出自动调用,一旦delete所有的有关这个特定键的所有数据都会失败,这时候如果没有调用析构函数,就会内存泄露。

    我们可以通过把主函数中的代码改成下边这样。来验证发生了内存泄露。

    //把data的赋值放到创建进程上方   
    ThreadData *data = (ThreadData *)pthread_getspecific(tls_key);
        
        pthread_create(&thread1, NULL, threadFunc, &tls_key);
        printf("Main Thread will sleep 5s\n");
        sleep(5);
        printf("Main Thread  data for thread %lld: %s\n", data->id, data->name);

    我们把data的赋值放到创建进程上方 ,同时只创建一个子进程,在子进程中delete特定键。观察现象

    [lzh@MYCAT Thread]$ ./myexe 
    Main Thread will sleep 5s
    Thread local data for thread 140497944049408: Thread973862656
    Thread want to delete tls_key : 0
    Main Thread  data for thread 140497960900416: Thread990713664
    Main Thread want to delete tls_key : 0
    ​

    发现能输出,然而data的赋值放到原来地方是直接报错(段错误)的。如果不在主线程中显式调用析构函数,是一定会发生内存泄露的。

  • 18
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值