【linux 多线程并发】线程本地数据存储的两种方式,每个线程可以有同名全局私有数据,以及两种方式的性能分析

本文介绍了Linux系统中线程本地数据(TLS)的两种实现方式:__thread关键字和线程API,探讨了它们的生命周期、内存布局、性能差异以及在多任务应用中的使用。作者还比较了两种方法的优缺点,强调了在多线程开发中的注意事项和实践应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程本地数据(TLS)

专栏内容

  • 参天引擎内核架构
    本专栏一起来聊聊参天引擎内核架构,以及如何实现多机的数据库节点的多读多写,与传统主备,MPP的区别,技术难点的分析,数据元数据同步,多主节点的情况下对故障容灾的支持。

  • 手写数据库toadb
    本专栏主要介绍如何从零开发,开发的步骤,以及开发过程中的涉及的原理,遇到的问题等,让大家能跟上并且可以一起开发,让每个需要的人成为参与者。
    本专栏会定期更新,对应的代码也会定期更新,每个阶段的代码会打上tag,方便阶段学习。

开源贡献

个人主页我的主页
管理社区开源数据库
座右铭:天行健,君子以自强不息;地势坤,君子以厚德载物.

前言

现代的CPU都是多core处理器,而且在intel处理器中每个core又可以多个processor,形成了多任务并行处理的硬件架构,在服务器端的处理器上架构又有一些不同,传统的采用SMP,也就是对称的多任务处理架构,每个任务都可以对等的访问所有内存,外设等,而如今在ARM系列CPU上,多采用NUMA架构,它将CPU核分了几个组,给每个组的CPU core分配了对应的内存和外设,CPU访问对应的内存和外设时速度最优,跨组访问时性能会降底一些。

随着硬件技术的持续发展,它们对一般应用的性能优化能力越来越强,同时对于服务器软件的开发,提出更高要求,要想达到极高的并发和性能,就需要充分利用当前硬件架构的特点,对它们进行压榨。那么,我们的应用至少也是要采用多任务架构,不管是多线程还是多进程的多任务架构,才可以充分利用硬件的资源,达到高效的处理能力。

当然多任务框架的采用,不仅仅是多线程的执行,需要对多任务下带来的问题进行处理,如任务执行返回值获取,任务间数据的传递,任务执行次序的协调;当然也不是任务越多处理越快,要避免线程过多导致操作系统夯住,也要防止任务空转过快导致CPU使用率飙高。

本专栏主要介绍使用多线程与多进程模型,如何搭建多任务的应用框架,同时对多任务下的数据通信,数据同步,任务控制,以及CPU core与任务绑定等相关知识的分享,让大家在实际开发中轻松构建自已的多任务程序。

概述

linux 系统中 线程是一种经量级的任务,同一进程的多个线程是共享进程内存的;当我们定义一个全局变量时,它可以被当前进程下的所有线程访问,如何来定义一个线程本地的变量呢?

TLS方式

在linux 系统下一般有两种方式来定义线程本地变量,这一技术叫做Thread Local Storage, TLS。

  • GCC的__thread关键字
  • 键值对API

TLS生命周期

线程本地变量的生命周期与线程的生命周期一样,当线程结束时,线程本地变量的内存就会被回收。

当然这里需要特别注意,当线程本地变量为指针类型时,动态分配的内存空间,系统并不会自动回收,只是将指针变量置为NULL,为了避免内存泄漏,需要在线程退出时主动进行清理动作,这将在后面的博文中介绍。

线程pthread结构内存

在介绍线程本地变量存储时,就不得不介绍一下pthread结构的内存,它定义了线程的重要数据结构,描述了用户状态线程的完整信息。

pthread 结构非常复杂,通过 specific_1stblock 数组和特定的辅助数组与 TLS 相关。

#define PTHREAD_KEY_2NDLEVEL_SIZE       32
#define PTHREAD_KEY_1STLEVEL_SIZE \
  ((PTHREAD_KEYS_MAX + PTHREAD_KEY_2NDLEVEL_SIZE - 1) \
   / PTHREAD_KEY_2NDLEVEL_SIZE)

struct pthread
{
    union
  {
#if !TLS_DTV_AT_TP
    /* This overlaps the TCB as used for TLS without threads (see tls.h).  */
    tcbhead_t header;
#else
    struct
    {
      int multiple_threads;
      int gscope_flag;
    } header;
#endif

    void *__padding[24];
  };

  list_t list;
  pid_t tid;

  ...
  struct pthread_key_data
  {
    /* Sequence number.  We use uintptr_t to not require padding on
       32- and 64-bit machines.  On 64-bit machines it helps to avoid
       wrapping, too.  */
    uintptr_t seq;

    /* Data pointer.  */
    void *data;
  } specific_1stblock[PTHREAD_KEY_2NDLEVEL_SIZE];

  /* Two-level array for the thread-specific data.  */
  struct pthread_key_data *specific[PTHREAD_KEY_1STLEVEL_SIZE];

  /* Flag which is set when specific data is set.  */
  bool specific_used;
  ...
}

__thread 关键字

该关键字可用于在 GCC/Clang 编译环境中声明 TLS 变量, 该关键字不是 C 标准,并且因编译器不同而有差异;

原理介绍

使用 __thread关键字声明的变量存储在线程的pthred 结构与堆栈空间之间,也就是说,在内存布局方面,从高地址到底层地址的内存分布为:pthred结构、可变区和堆栈区(堆栈的底部和可变区的顶部是连续的);

在这种方式下的线程本地变量,变量的类型不能是复杂的类型,如C++的class类型,而且动态申请的变量空间,需要主动释放,线程结束时,只是对变量空间回收,而对应的动态内存则会泄漏。

代码举例

/* 
 * created by senllang 2024/1/1 
 * mail : study@senllang.onaliyun.com 
 * Copyright (C) 2023-2024, senllang
 */
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#define THREAD_NAME_LEN 32
__thread char threadName[THREAD_NAME_LEN];
__thread int delay = 0;

typedef struct ThreadData 
{
    char name[THREAD_NAME_LEN];
    int delay;
}ThreadData;

void *threadEntry(void *arg) 
{
    int ret = 0;
    int i = 0;
    ThreadData * data = (ThreadData *)arg;

    printf("[%lu] thread entered \n", pthread_self());

    strncpy(threadName, data->name, THREAD_NAME_LEN);
    delay = data->delay;

    for(i = 0; i < delay; i++)
    {
        usleep(10);
    }
    printf("[%lu] %s exiting after delay %d.\n", pthread_self(), threadName, delay);
    pthread_exit(&ret);
}

int main(int argc, char *argv[]) 
{
    pthread_t thid1,thid2,thid3;
    void *ret;
    ThreadData args1 = {"thread 1", 50000}, args2 = {"thread 2", 25000}, args3 = {"thread 3", 12500};

    strncpy(threadName, "Main Thread", THREAD_NAME_LEN);

    if (pthread_create(&thid1, NULL, threadEntry, &args1) != 0) 
    {
        perror("pthread_create() error");
        exit(1);
    }

    if (pthread_create(&thid2, NULL, threadEntry, &args2) != 0) 
    {
        perror("pthread_create() error");
        exit(1);
    }

    if (pthread_create(&thid3, NULL, threadEntry, &args3) != 0) 
    {
        perror("pthread_create() error");
        exit(1);
    }

    if (pthread_join(thid1, &ret) != 0) 
    {
        perror("pthread_create() error");
        exit(3);
    }

    if (pthread_join(thid2, &ret) != 0) 
    {
        perror("pthread_create() error");
        exit(3);
    }

    if (pthread_join(thid3, &ret) != 0) 
    {
        perror("pthread_create() error");
        exit(3);
    }

    printf("[%s]all thread exited delay:%d .\n", threadName, delay);
}

每个线程定义了两个线程本地变量 threadName, delay,在线程处理函数中,对它们赋值后,再延迟一段时间,然后输出这两个变量值,结果可以看到每个线程的本地变量值都不一样,可以独立使用。

运行结果:

[senllang@hatch example_04]$ gcc -lpthread threadLocalStorage_gcc.c 
[senllang@hatch example_04]$ ./a.out 
[139945977145088] thread entered 
[139945960359680] thread entered 
[139945968752384] thread entered 
[139945960359680] thread 3 exiting after delay 12500.
[139945968752384] thread 2 exiting after delay 25000.
[139945977145088] thread 1 exiting after delay 50000.
[Main Thread]all thread exited delay:0 .

线程API方式

另一种使用线程本地变量的方式,是使用线程key相关的API,它分为两类,一是创建和销毁接口,另一类是变量的设置与获取接口。

这种方式下,线程的本地数据存储在 pthread结构中,其中specific_1stblock,specific两个数组按key值索引,并存储对应的线程本地数据;

线程本地数据的数量,在这种方式下是有限的。

创建与销毁接口

#include <pthread.h>  
int pthread_key_create(pthread_key_t *key, void(*destructor)(void*));
int pthread_key_delete(pthread_key_t key);

创建接口,获取一个 pthread_key_t变量的值,其实就是内存获取一个键值来存储数据,第二个参数destructor传递一个销毁数据的方法,当本地数据为复杂数据类型,或者动态申请内存时,在线程退出时进行清理调用。

在线程使用完后,需要释放对应的key。

设置本地变量值接口

#include <pthread.h>  
int pthread_setspecific(pthread_key_t key, const void * value);
void * pthread_getspecific(pthread_key_t key);

这里设置线程的本地变量值,和获取线程本地变量值;

在不同线程中设置时,就会只设置当前线程的本地变量,不影响其它线程。

代码示例

/* 
 * created by senllang 2024/1/1 
 * mail : study@senllang.onaliyun.com 
 * Copyright (C) 2023-2024, senllang
 */

#include <stdio.h>  
#include <pthread.h>  
  
// 定义一个 TLS 键  
pthread_key_t tls_key;  

void ShowThreadLocalData(char *prompt, pthread_t thid)
{
    // 获取 TLS 存储的值  
    int *value = (int *) pthread_getspecific(tls_key);  
    if (value == NULL) 
    {  
        printf("[%s]Thread: %ld, Value: NULL\n", prompt, thid);  
    } else 
    {  
        printf("[%s]Thread: %ld, Value: %d\n", prompt, thid, *value);  
    }  
}

// 线程函数  
void *thread_func(void *arg) 
{  
    ShowThreadLocalData("pre", pthread_self());

    pthread_setspecific(tls_key, (void *) arg);

    ShowThreadLocalData("after", pthread_self());
    return NULL;  
}  
  
int main() 
{  
    // 创建 2 个线程  
    pthread_t thread1, thread2;  
    int args1 = 100, args2=200;

    pthread_key_create(&tls_key, NULL); 

    // 设置 TLS 值  
    pthread_setspecific(tls_key, (void *) 500);  

    pthread_create(&thread1, NULL, thread_func, &args1);  
    pthread_create(&thread2, NULL, thread_func, &args2);  
  
    // 等待线程结束  
    pthread_join(thread1, NULL);  
    pthread_join(thread2, NULL);  

    pthread_key_delete(tls_key);
    return 0;  
}

在主线程和两个子线程中都设置了本地变量值,运行后,可以看到每个线程中的值都不一样。

[senllang@hatch example_04]$ gcc -lpthread threadLocalStorage_key.c 
[senllang@hatch example_04]$ ./a.out 
[pre]Thread: 140252914022144, Value: NULL
[after]Thread: 140252914022144, Value: 100
[pre]Thread: 140252905629440, Value: NULL
[after]Thread: 140252905629440, Value: 200

在线程开始时,获取本地变量值,都没有获取到主线程设置的值。

两种方式比较

  • 不同的存储区域/寻址方法
    API 方式定义的数据由 specific_1stblock 数组和结构的特定辅助数组寻址,而__thread存储类型变量由栈空间地址偏移量寻址。

  • 性能/效率差异
    由于__thread由栈地址偏移量解决,因此性能高于 API方式。

  • 可以存储不同的数据
    __thread只能修改常规的POD类型变量,对于指针类型数据,动态申请的内存,需要主动销毁;而 API方式 支持传入销毁方法并支持所有数据类型。

  • 支持的数据数量不同
    理论上,只要堆栈不满,__thread类型的变量就可以无限期定义;而API 方式只能创建PTHREAD_KEYS_MAX个键,但可以使用一个键通过结构体等方式存储多个值。

总结

本文所涉及的代码已经上传到工程hatchCode, 在multipleThreads/example_04目录下;

线程本地变量的使用,使得线程并发时,与进程并发更加相似,都有自己的私有全局数据,当然线程的特别之处在于,线程的本地变量的空间取决于线程栈的大小,当然也可以是结构指针,再动态申请空间,那么空间也就不存在问题了。

结尾

非常感谢大家的支持,在浏览的同时别忘了留下您宝贵的评论,如果觉得值得鼓励,请点赞,收藏,我会更加努力!

作者邮箱:study@senllang.onaliyun.com
如有错误或者疏漏欢迎指出,互相学习。

<think>嗯,用户的问题是关于在Android中多个APK加载同一个SO库时,该库中的方法是否支持并发调用,以及多进程访问的并发安全性。我需要先理解这个问题的背景和相关知识点,然后逐步分析可能的答案。 首先,用户提到的多个APK共享同一个SO库的情况。根据引用[2],重复的SO库可能会导致包体积增加和运行时风险,比如多个SO静态链接的问题。但这里用户关注的是并发调用的问题。接下来,我需要考虑SO库在内存中的加载情况。每个APK在安装时会将SO文件解压到自己的私有目录,如/data/app/包名/lib下。不过,如果不同APK加载的是同一个物理文件(比如系统库),系统可能会共享内存映射,但如果是不同路径下的同名SO,可能不会共享。 然后,关于并发调用,需要考虑SO库本身的线程安全性。如果SO库内部没有使用全局变量或静态变量,或者正确使用了同步机制(如互斥锁),那么并发调用是安全的。但如果库内部有共享资源且未加锁,多线程同时访问可能导致竞态条件。此外,多进程的情况更复杂,因为每个进程有独立的地址空间,同一SO库在不同进程中的实例是隔离的,全局变量不会共享。这时候如果涉及跨进程的共享资源(如文件、共享内存),需要额外的同步机制。 用户还提到多进程访问的并发安全性。这里需要考虑SO库是否设计为支持多进程环境。如果库依赖于进程内的静态变量,那么多进程同时调用可能导致各自进程内的数据不一致,但不会跨进程影响。但如果库使用了跨进程的共享资源,比如操作同一个文件,就需要使用进程间同步机制,如文件锁。 另外,引用[3]提到系统在加载SO时根据ABI选择目录,如果不同APK中的SO路径不同,可能实际加载的是不同的文件,这时候并发调用实际上是在不同实例中,互不影响。但如果多个APK加载的是同一个物理文件(比如系统库),则可能存在共享代码段,但数据段各自独立。 总结来说,是否支持并发调用取决于SO库的实现和加载方式。需要检查库的线程安全性和进程隔离情况。如果库是线程安全且无共享资源依赖,那么多线程和多进程调用是可行的;否则需要额外处理同步问题。 可能用户的实际场景是多个应用需要调用同一个SO库的功能,担心并发时的稳定性。需要建议他们检查SO库的设计,确保关键部分有同步机制,并测试多进程环境下的行为。</think>### 多APK共享SO库的并发调用分析 #### 1. **SO库的内存加载机制** 在Android系统中,每个APK加载的SO库文件虽然可能**物理路径不同**(如`/data/app/包名A/lib`和`/data/app/包名B/lib`),但若这些SO库文件**内容完全一致**,系统可能会通过内存映射(mmap)机制共享代码段,但数据段(如全局变量)会独立分配给每个进程[^2][^3]。 例如,若两个APK均包含`libfoo.so`且MD5值相同,但安装后位于不同路径,则系统会为每个APK的进程独立加载该库的实例。 #### 2. **并发调用的线程安全性** - **线程安全条件**: SO库中方法的并发调用是否安全,取决于其内部实现: - 若方法**无共享状态**(如仅使用局部变量或线程安全的数据结构),则支持多线程并发调用。 - 若方法依赖**全局变量/静态变量**,则需通过锁(如`pthread_mutex`)或原子操作保证同步。否则可能导致数据竞争(Data Race)或逻辑错误。 - **示例代码(非线程安全)**: ```c static int counter = 0; // 全局变量 void unsafe_increment() { counter++; // 非原子操作,多线程调用会导致竞争 } ``` #### 3. **多进程访问的隔离性** - **进程间内存隔离**: 不同APK进程加载的SO库实例**数据段完全独立**,即使代码段共享。例如,APK A和APK B调用`libfoo.so`中的全局变量`int x`,实际访问的是各自进程内的独立内存区域。 - **跨进程共享资源**: 若SO库操作**外部共享资源**(如文件、共享内存),需通过进程间同步机制(如文件锁、Binder)协调访问。 #### 4. **并发场景下的建议** 1. **验证SO库设计**: - 使用`nm`工具检查符号表,确认是否存在全局变量(如`BSS`段或`DATA`段)。 - 若库提供方声明为“线程安全”,需通过压力测试验证。 2. **优化同步机制**: - 对关键代码段添加互斥锁(参考`pthread_mutex_lock`)。 - 避免跨进程共享非线程安全资源。 3. **动态加载验证**: 通过`dlopen`和`dlsym`动态加载SO库,结合日志监控并发调用行为。 #### 5. **系统限制与风险** - **重复SO的风险**: 若不同APK包含同名但不同版本的SO库,可能导致符号冲突或内存泄漏。 - **ABI兼容性**: 若APK未正确声明支持的ABI(如armeabi-v7a与arm64-v8a混合),可能触发兼容性异常。 ---
评论 115
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

韩楚风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值