MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Linux C语言线程局部存储的使用技巧

2023-04-104.4k 阅读

什么是线程局部存储(TLS)

在多线程编程中,多个线程可能会共享一些数据。然而,有时候我们希望每个线程都有自己独立的一份数据副本,这就是线程局部存储(Thread - Local Storage,TLS)的作用。线程局部存储允许每个线程拥有自己独立的变量实例,各个线程对该变量的操作互不干扰。

在 Linux C 语言编程环境下,线程局部存储为开发人员提供了一种强大的机制来管理线程特定的数据。这种机制在许多场景下都非常有用,比如在多线程环境中,每个线程可能需要维护自己的状态信息、日志记录等,而不需要担心与其他线程的数据冲突。

TLS 的底层原理

从操作系统和编译器的角度来看,线程局部存储的实现依赖于特定的机制。在 Linux 系统中,通常使用特定的段(如 .tls 段)来存储线程局部变量。当一个线程启动时,操作系统会为该线程分配一个独立的 TLS 区域,这个区域的地址对于每个线程都是唯一的。

编译器在编译阶段会对线程局部变量进行特殊处理。例如,它会生成相应的指令来访问 TLS 区域中的变量。对于全局的线程局部变量,编译器会确保在每个线程启动时,该变量被正确初始化并放置在各自的 TLS 区域中。

在运行时,当线程访问一个线程局部变量时,CPU 会根据线程的上下文找到对应的 TLS 区域,并从该区域中读取或写入变量的值。这种机制保证了每个线程都能独立地操作自己的变量副本,而不会影响其他线程。

Linux C 语言中 TLS 的使用方式

在 Linux C 语言中,使用线程局部存储主要有两种方式:通过 __thread 关键字和使用 POSIX 线程库(pthread)提供的函数。

使用 __thread 关键字

__thread 是 GCC 编译器提供的一个关键字,用于声明线程局部变量。这种方式非常简洁明了,直接在变量声明前加上 __thread 关键字即可。

#include <stdio.h>
#include <pthread.h>

// 声明一个线程局部变量
__thread int thread_local_variable = 0;

void* thread_function(void* arg) {
    // 每个线程独立地修改和访问该变量
    for (int i = 0; i < 5; i++) {
        thread_local_variable++;
        printf("Thread %ld: thread_local_variable = %d\n", (long)pthread_self(), thread_local_variable);
    }
    return NULL;
}

int main() {
    pthread_t thread1, thread2;

    // 创建两个线程
    pthread_create(&thread1, NULL, thread_function, NULL);
    pthread_create(&thread2, NULL, thread_function, NULL);

    // 等待线程结束
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    return 0;
}

在上述代码中,__thread int thread_local_variable = 0; 声明了一个线程局部变量 thread_local_variable,并初始化为 0。每个线程在 thread_function 函数中对该变量进行独立的自增操作,并打印出自己线程中变量的值。由于每个线程都有自己独立的变量副本,所以不同线程对该变量的操作不会相互影响。

使用 POSIX 线程库函数

POSIX 线程库提供了一组函数来管理线程局部存储。主要涉及的函数有 pthread_key_create()pthread_setspecific()pthread_getspecific()pthread_key_delete()

  1. pthread_key_create():该函数用于创建一个线程局部存储的键。这个键是一个唯一的标识符,用于在每个线程中关联一个特定的线程局部变量。
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));

key 是一个指向 pthread_key_t 类型变量的指针,函数会在这个变量中存储新创建的键。destructor 是一个可选的清理函数指针。当线程结束时,如果设置了这个清理函数,系统会调用该函数来清理与该键关联的线程局部数据。

  1. pthread_setspecific():用于将一个值与当前线程的特定键关联起来。
int pthread_setspecific(pthread_key_t key, const void *value);

key 是之前通过 pthread_key_create() 创建的键,value 是要与该键关联的值。这个值通常是一个指针,可以指向任何类型的数据。

  1. pthread_getspecific():用于获取与当前线程特定键关联的值。
void* pthread_getspecific(pthread_key_t key);

key 同样是之前创建的键,函数返回与该键关联的值。

  1. pthread_key_delete():用于删除一个线程局部存储的键。
int pthread_key_delete(pthread_key_t key);

当不再需要某个线程局部存储键时,可以使用这个函数删除它。在删除键之前,系统会自动调用之前设置的清理函数(如果有)来清理与该键关联的线程局部数据。

下面是一个使用 POSIX 线程库函数实现线程局部存储的示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// 线程局部存储键
pthread_key_t key;

// 清理函数
void cleanup(void* data) {
    printf("Cleaning up data in thread %ld\n", (long)pthread_self());
    free(data);
}

void* thread_function(void* arg) {
    // 为每个线程分配独立的数据
    int* thread_specific_data = (int*)malloc(sizeof(int));
    if (thread_specific_data == NULL) {
        perror("malloc");
        pthread_exit(NULL);
    }
    *thread_specific_data = 0;

    // 将数据与线程局部存储键关联
    pthread_setspecific(key, thread_specific_data);

    // 每个线程独立地修改和访问数据
    for (int i = 0; i < 5; i++) {
        int* data = (int*)pthread_getspecific(key);
        (*data)++;
        printf("Thread %ld: data = %d\n", (long)pthread_self(), *data);
    }

    return NULL;
}

int main() {
    // 创建线程局部存储键,并设置清理函数
    if (pthread_key_create(&key, cleanup) != 0) {
        perror("pthread_key_create");
        return 1;
    }

    pthread_t thread1, thread2;

    // 创建两个线程
    pthread_create(&thread1, NULL, thread_function, NULL);
    pthread_create(&thread2, NULL, thread_function, NULL);

    // 等待线程结束
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    // 删除线程局部存储键
    pthread_key_delete(key);

    return 0;
}

在上述代码中,首先通过 pthread_key_create() 创建了一个线程局部存储键 key,并设置了清理函数 cleanup。在 thread_function 中,每个线程为自己分配独立的数据 thread_specific_data,并通过 pthread_setspecific() 将其与键 key 关联。然后,线程通过 pthread_getspecific() 获取与键关联的数据,并进行独立的操作。当线程结束时,系统会自动调用清理函数 cleanup 来释放分配的内存。

TLS 的应用场景

日志记录

在多线程应用程序中,每个线程可能需要记录自己的日志信息。使用线程局部存储可以为每个线程分配独立的日志缓冲区,避免不同线程的日志信息相互干扰。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>

#define LOG_BUFFER_SIZE 1024

// 线程局部存储键
pthread_key_t key;

// 清理函数
void log_cleanup(void* buffer) {
    free(buffer);
}

void* thread_function(void* arg) {
    // 为每个线程分配独立的日志缓冲区
    char* log_buffer = (char*)malloc(LOG_BUFFER_SIZE);
    if (log_buffer == NULL) {
        perror("malloc");
        pthread_exit(NULL);
    }
    pthread_setspecific(key, log_buffer);

    // 模拟日志记录
    for (int i = 0; i < 3; i++) {
        char* buffer = (char*)pthread_getspecific(key);
        snprintf(buffer, LOG_BUFFER_SIZE, "Thread %ld: Log message %d", (long)pthread_self(), i);
        printf("%s\n", buffer);
    }

    return NULL;
}

int main() {
    // 创建线程局部存储键,并设置清理函数
    if (pthread_key_create(&key, log_cleanup) != 0) {
        perror("pthread_key_create");
        return 1;
    }

    pthread_t thread1, thread2;

    // 创建两个线程
    pthread_create(&thread1, NULL, thread_function, NULL);
    pthread_create(&thread2, NULL, thread_function, NULL);

    // 等待线程结束
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    // 删除线程局部存储键
    pthread_key_delete(key);

    return 0;
}

在这个示例中,每个线程都有自己独立的日志缓冲区,通过线程局部存储来管理。这样每个线程的日志记录操作不会影响其他线程。

数据库连接管理

在多线程应用程序中,每个线程可能需要与数据库建立独立的连接。使用线程局部存储可以为每个线程保存自己的数据库连接句柄,避免连接冲突。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// 模拟数据库连接结构体
typedef struct {
    int connection_id;
    // 其他连接相关信息
} DatabaseConnection;

// 线程局部存储键
pthread_key_t key;

// 清理函数
void db_connection_cleanup(void* connection) {
    printf("Closing database connection in thread %ld\n", (long)pthread_self());
    free(connection);
}

void* thread_function(void* arg) {
    // 为每个线程分配独立的数据库连接
    DatabaseConnection* connection = (DatabaseConnection*)malloc(sizeof(DatabaseConnection));
    if (connection == NULL) {
        perror("malloc");
        pthread_exit(NULL);
    }
    connection->connection_id = (int)(long)arg;
    pthread_setspecific(key, connection);

    // 模拟数据库操作
    DatabaseConnection* current_connection = (DatabaseConnection*)pthread_getspecific(key);
    printf("Thread %ld: Using database connection %d\n", (long)pthread_self(), current_connection->connection_id);

    return NULL;
}

int main() {
    // 创建线程局部存储键,并设置清理函数
    if (pthread_key_create(&key, db_connection_cleanup) != 0) {
        perror("pthread_key_create");
        return 1;
    }

    pthread_t thread1, thread2;

    // 创建两个线程,并传递不同的连接 ID
    pthread_create(&thread1, NULL, thread_function, (void*)(long)1);
    pthread_create(&thread2, NULL, thread_function, (void*)(long)2);

    // 等待线程结束
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    // 删除线程局部存储键
    pthread_key_delete(key);

    return 0;
}

在这个示例中,每个线程都有自己独立的数据库连接实例,通过线程局部存储进行管理,确保了多线程环境下数据库操作的独立性。

线程特定的状态管理

在一些复杂的多线程应用中,每个线程可能需要维护自己特定的状态信息。例如,在一个多线程的网络服务器中,每个线程处理一个客户端连接,每个线程可能需要记录当前客户端连接的状态(如已认证、未认证、正在传输数据等)。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// 定义线程状态枚举
typedef enum {
    STATE_UNAUTHENTICATED,
    STATE_AUTHENTICATED,
    STATE_TRANSFERRING_DATA
} ThreadState;

// 线程局部存储键
pthread_key_t key;

// 清理函数
void state_cleanup(void* state) {
    free(state);
}

void* thread_function(void* arg) {
    // 为每个线程分配独立的状态变量
    ThreadState* thread_state = (ThreadState*)malloc(sizeof(ThreadState));
    if (thread_state == NULL) {
        perror("malloc");
        pthread_exit(NULL);
    }
    *thread_state = STATE_UNAUTHENTICATED;
    pthread_setspecific(key, thread_state);

    // 模拟状态转换
    for (int i = 0; i < 3; i++) {
        ThreadState* current_state = (ThreadState*)pthread_getspecific(key);
        if (*current_state == STATE_UNAUTHENTICATED) {
            *current_state = STATE_AUTHENTICATED;
            printf("Thread %ld: Authenticated\n", (long)pthread_self());
        } else if (*current_state == STATE_AUTHENTICATED) {
            *current_state = STATE_TRANSFERRING_DATA;
            printf("Thread %ld: Starting data transfer\n", (long)pthread_self());
        }
    }

    return NULL;
}

int main() {
    // 创建线程局部存储键,并设置清理函数
    if (pthread_key_create(&key, state_cleanup) != 0) {
        perror("pthread_key_create");
        return 1;
    }

    pthread_t thread1, thread2;

    // 创建两个线程
    pthread_create(&thread1, NULL, thread_function, NULL);
    pthread_create(&thread2, NULL, thread_function, NULL);

    // 等待线程结束
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    // 删除线程局部存储键
    pthread_key_delete(key);

    return 0;
}

在这个示例中,每个线程通过线程局部存储维护自己的状态信息,不同线程的状态转换操作互不干扰。

TLS 使用中的注意事项

初始化问题

  1. __thread 变量的初始化:使用 __thread 关键字声明的变量,其初始化必须是编译时常量表达式。例如,__thread int num = 5; 是合法的,但 __thread int num = get_value();get_value 是一个函数)是不合法的,因为函数调用不是编译时常量表达式。
  2. POSIX 线程库方式的初始化:当使用 POSIX 线程库函数来管理线程局部存储时,需要注意在每个线程中及时初始化与键关联的数据。如果在获取数据之前没有进行初始化,可能会导致程序崩溃或未定义行为。

内存管理

  1. 动态分配内存的清理:如果在每个线程中为线程局部变量动态分配了内存(如使用 malloc),在使用 POSIX 线程库时,必须设置正确的清理函数(通过 pthread_key_createdestructor 参数)来释放这些内存。否则,会导致内存泄漏。对于 __thread 变量,如果其类型是需要手动释放资源的(如指针指向动态分配的内存),则需要在适当的时候手动释放资源。
  2. 避免重复释放:要注意避免对同一块内存进行多次释放。在清理函数中,确保只对已分配且未释放的内存进行释放操作。例如,可以在分配内存后设置一个标志位,在清理函数中先检查标志位,避免重复释放。

性能影响

虽然线程局部存储提供了线程间数据隔离的便利,但也可能带来一定的性能开销。访问线程局部变量需要额外的指令来定位 TLS 区域,尤其是在使用 POSIX 线程库函数时,函数调用也会带来一定的开销。在性能敏感的应用中,需要仔细评估 TLS 的使用对性能的影响。如果可能,可以通过优化数据结构或算法来减少对 TLS 的依赖,或者在关键代码段避免频繁访问线程局部变量。

跨平台兼容性

__thread 关键字是 GCC 编译器特有的扩展,在其他编译器(如 Clang 或 Visual C++)中可能不支持。如果需要编写跨平台的代码,建议使用 POSIX 线程库函数来实现线程局部存储,因为 POSIX 标准在许多操作系统上都有广泛的支持。但即使使用 POSIX 线程库,不同操作系统的实现细节可能也会略有不同,在跨平台开发时需要进行充分的测试。

TLS 与其他多线程技术的结合使用

与互斥锁的结合

在多线程编程中,互斥锁(mutex)用于保护共享资源,防止多个线程同时访问导致数据竞争。而线程局部存储提供了线程间的数据隔离。有时候,我们可能需要在使用线程局部存储的同时,结合互斥锁来保护一些共享的初始化操作或全局资源。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// 线程局部存储键
pthread_key_t key;

// 互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 共享资源
int shared_resource = 0;

// 清理函数
void cleanup(void* data) {
    free(data);
}

void* thread_function(void* arg) {
    // 为每个线程分配独立的数据
    int* thread_specific_data = (int*)malloc(sizeof(int));
    if (thread_specific_data == NULL) {
        perror("malloc");
        pthread_exit(NULL);
    }
    *thread_specific_data = 0;

    // 将数据与线程局部存储键关联
    pthread_setspecific(key, thread_specific_data);

    // 模拟对共享资源的操作,需要加锁
    pthread_mutex_lock(&mutex);
    shared_resource++;
    printf("Thread %ld: Incremented shared_resource to %d\n", (long)pthread_self(), shared_resource);
    pthread_mutex_unlock(&mutex);

    // 每个线程独立地修改和访问自己的数据
    for (int i = 0; i < 5; i++) {
        int* data = (int*)pthread_getspecific(key);
        (*data)++;
        printf("Thread %ld: data = %d\n", (long)pthread_self(), *data);
    }

    return NULL;
}

int main() {
    // 创建线程局部存储键,并设置清理函数
    if (pthread_key_create(&key, cleanup) != 0) {
        perror("pthread_key_create");
        return 1;
    }

    pthread_t thread1, thread2;

    // 创建两个线程
    pthread_create(&thread1, NULL, thread_function, NULL);
    pthread_create(&thread2, NULL, thread_function, NULL);

    // 等待线程结束
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    // 删除线程局部存储键
    pthread_key_delete(key);

    // 销毁互斥锁
    pthread_mutex_destroy(&mutex);

    return 0;
}

在上述代码中,shared_resource 是一个共享资源,通过互斥锁 mutex 来保护对其的访问。每个线程仍然有自己独立的 thread_specific_data,通过线程局部存储进行管理。

与条件变量的结合

条件变量(condition variable)用于线程间的同步,通常与互斥锁一起使用。在使用线程局部存储的场景中,也可以结合条件变量来实现更复杂的线程同步逻辑。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// 线程局部存储键
pthread_key_t key;

// 互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

// 共享标志
int shared_flag = 0;

// 清理函数
void cleanup(void* data) {
    free(data);
}

void* thread_function(void* arg) {
    // 为每个线程分配独立的数据
    int* thread_specific_data = (int*)malloc(sizeof(int));
    if (thread_specific_data == NULL) {
        perror("malloc");
        pthread_exit(NULL);
    }
    *thread_specific_data = 0;

    // 将数据与线程局部存储键关联
    pthread_setspecific(key, thread_specific_data);

    // 等待共享标志被设置
    pthread_mutex_lock(&mutex);
    while (shared_flag == 0) {
        pthread_cond_wait(&cond, &mutex);
    }
    pthread_mutex_unlock(&mutex);

    // 每个线程独立地修改和访问自己的数据
    for (int i = 0; i < 5; i++) {
        int* data = (int*)pthread_getspecific(key);
        (*data)++;
        printf("Thread %ld: data = %d\n", (long)pthread_self(), *data);
    }

    return NULL;
}

int main() {
    // 创建线程局部存储键,并设置清理函数
    if (pthread_key_create(&key, cleanup) != 0) {
        perror("pthread_key_create");
        return 1;
    }

    pthread_t thread1, thread2;

    // 创建两个线程
    pthread_create(&thread1, NULL, thread_function, NULL);
    pthread_create(&thread2, NULL, thread_function, NULL);

    // 模拟一些操作后设置共享标志
    sleep(2);
    pthread_mutex_lock(&mutex);
    shared_flag = 1;
    pthread_cond_broadcast(&cond);
    pthread_mutex_unlock(&mutex);

    // 等待线程结束
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    // 删除线程局部存储键
    pthread_key_delete(key);

    // 销毁互斥锁和条件变量
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);

    return 0;
}

在这个示例中,线程通过条件变量 cond 等待共享标志 shared_flag 被设置。每个线程仍然使用线程局部存储来管理自己的独立数据。这种结合方式可以在多线程环境中实现更灵活的同步机制。

总结 TLS 在 Linux C 语言多线程编程中的重要性

线程局部存储在 Linux C 语言多线程编程中是一个非常重要的工具。它为开发人员提供了一种简单而有效的方式来管理线程特定的数据,避免了多线程环境下的数据冲突问题。通过 __thread 关键字和 POSIX 线程库函数,我们可以根据具体的需求选择合适的方式来实现线程局部存储。

在实际应用中,TLS 广泛应用于日志记录、数据库连接管理、线程状态管理等多个场景。然而,在使用 TLS 时,我们需要注意初始化、内存管理、性能影响以及跨平台兼容性等问题,以确保程序的正确性和高效性。

同时,TLS 还可以与互斥锁、条件变量等其他多线程技术结合使用,进一步丰富多线程编程的功能和灵活性。通过合理运用这些技术,开发人员可以编写出健壮、高效的多线程应用程序,充分发挥多核处理器的性能优势。

希望通过本文的介绍和示例代码,读者能够对 Linux C 语言中线程局部存储的使用技巧有更深入的理解,并在实际项目中能够熟练运用这一强大的技术。