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

Linux C语言线程局部存储的清理

2024-04-141.4k 阅读

Linux C 语言线程局部存储的清理

线程局部存储(TLS)概述

在多线程编程中,线程局部存储(Thread - Local Storage,TLS)是一种机制,它允许每个线程拥有自己独立的变量实例。这意味着不同线程对同一个 TLS 变量的操作不会相互干扰。在 Linux C 语言编程环境下,TLS 为开发人员提供了一种有效的方式来管理线程特定的数据。例如,在一个多线程的服务器应用程序中,每个线程可能需要维护自己的连接状态、缓存数据等,TLS 就能很好地满足这种需求。

在 Linux 系统中,实现 TLS 主要依赖于 pthread 库。pthread 库提供了一系列函数来创建和管理线程,同时也支持 TLS 的相关操作。对于 TLS 变量,我们可以通过特定的函数来创建、获取其值以及设置其值。

TLS 变量的创建与使用

  1. 创建 TLS 变量 在 Linux C 中,我们使用 pthread_key_create 函数来创建一个 TLS 键。这个键就像是一个标识符,每个线程通过这个键来访问自己的 TLS 变量实例。pthread_key_create 函数的原型如下:
#include <pthread.h>
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));

其中,key 是一个指向 pthread_key_t 类型变量的指针,函数会在成功时将新创建的键值存储在这里。destructor 是一个可选的清理函数指针。当线程终止时,如果该键有与之关联的非空值,系统会自动调用这个清理函数来释放相关资源。

例如,我们可以这样创建一个 TLS 键:

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

pthread_key_t tls_key;

void destructor(void *data) {
    if (data) {
        printf("Cleaning up thread - local data\n");
        free(data);
    }
}

void *thread_function(void *arg) {
    int *thread_specific_data = (int *)malloc(sizeof(int));
    *thread_specific_data = *((int *)arg);
    pthread_setspecific(tls_key, thread_specific_data);
    // 线程执行其他操作
    printf("Thread %ld has set its local data to %d\n", pthread_self(), *thread_specific_data);
    // 线程结束,TLS 数据会由清理函数清理
    return NULL;
}

int main() {
    if (pthread_key_create(&tls_key, destructor) != 0) {
        perror("pthread_key_create");
        return 1;
    }
    pthread_t threads[3];
    int args[3] = {1, 2, 3};
    for (int i = 0; i < 3; i++) {
        if (pthread_create(&threads[i], NULL, thread_function, &args[i]) != 0) {
            perror("pthread_create");
            return 1;
        }
    }
    for (int i = 0; i < 3; i++) {
        if (pthread_join(threads[i], NULL) != 0) {
            perror("pthread_join");
            return 1;
        }
    }
    if (pthread_key_delete(tls_key) != 0) {
        perror("pthread_key_delete");
        return 1;
    }
    return 0;
}

在上述代码中,我们首先创建了一个 TLS 键 tls_key,并指定了一个清理函数 destructor。在每个线程的执行函数 thread_function 中,我们为每个线程分配了一块内存,并将其与 TLS 键关联起来。当线程结束时,清理函数 destructor 会被调用,释放分配的内存。

  1. 获取和设置 TLS 变量的值 一旦创建了 TLS 键,线程可以使用 pthread_setspecific 函数来设置与该键关联的值,使用 pthread_getspecific 函数来获取该值。它们的原型分别为:
int pthread_setspecific(pthread_key_t key, const void *value);
void *pthread_getspecific(pthread_key_t key);

pthread_setspecific 函数将 valuekey 关联起来,而 pthread_getspecific 函数则返回与 key 关联的值。如果没有值与 key 关联,pthread_getspecific 会返回 NULL

在前面的代码示例中,我们在 thread_function 函数中使用了 pthread_setspecific 来设置 TLS 变量的值:

pthread_setspecific(tls_key, thread_specific_data);

并且可以在需要的时候使用 pthread_getspecific 来获取这个值,例如:

void *retrieved_data = pthread_getspecific(tls_key);
if (retrieved_data) {
    int *data = (int *)retrieved_data;
    printf("Retrieved data in thread: %d\n", *data);
}

TLS 清理的重要性

  1. 资源泄漏问题 如果在 TLS 变量中分配了资源(如内存、文件描述符等),而没有正确清理,就会导致资源泄漏。例如,假设一个线程在 TLS 变量中打开了一个文件描述符,但没有在适当的时候关闭它。当这个线程结束时,这个文件描述符就会一直保持打开状态,占用系统资源。随着时间的推移,这种资源泄漏可能会导致系统性能下降,甚至耗尽系统资源。
  2. 数据一致性问题 在多线程环境中,数据一致性是非常重要的。如果 TLS 变量中的数据没有正确清理,可能会导致后续线程使用到无效或不一致的数据。例如,一个线程在 TLS 变量中维护了一个缓存数据结构,当线程结束时没有清理这个结构,另一个线程可能会错误地读取到这个已经无效的缓存数据,从而导致程序逻辑错误。

清理函数的工作原理

  1. 清理函数的触发时机 清理函数在两种情况下会被触发。第一种情况是当线程正常终止时。当一个线程调用 pthread_exit 函数或者线程执行函数返回时,系统会检查该线程所有已设置值的 TLS 键,并调用相应的清理函数。第二种情况是当线程被取消时(通过 pthread_cancel 函数),同样会触发清理函数的调用。

  2. 清理函数的参数传递 清理函数的参数是与 TLS 键关联的值。在前面的代码示例中,我们的清理函数 destructor 接收一个 void * 类型的参数 data,这个 data 就是通过 pthread_setspecific 函数设置的值。在清理函数中,我们可以根据这个值的类型进行相应的资源释放操作。例如,如果这个值是一个指向动态分配内存的指针,我们可以使用 free 函数来释放这块内存;如果是一个文件描述符,我们可以使用 close 函数来关闭文件。

手动清理 TLS 变量

  1. 在适当的位置手动清理 除了依赖系统在特定时机调用清理函数外,我们也可以在代码的适当位置手动清理 TLS 变量。例如,在一个复杂的多线程应用程序中,可能存在一些特定的逻辑分支,在这些分支中,某个线程不再需要使用 TLS 变量,此时就可以手动清理。
void *thread_function(void *arg) {
    int *thread_specific_data = (int *)malloc(sizeof(int));
    *thread_specific_data = *((int *)arg);
    pthread_setspecific(tls_key, thread_specific_data);
    // 线程执行一些操作
    // 假设在某个条件下,不再需要这个 TLS 变量
    if (some_condition) {
        void *data = pthread_getspecific(tls_key);
        if (data) {
            free(data);
            pthread_setspecific(tls_key, NULL);
        }
    }
    // 线程继续执行其他操作
    return NULL;
}

在上述代码中,当 some_condition 满足时,我们手动获取 TLS 变量的值,释放相关资源,并将 TLS 变量设置为 NULL

  1. 注意事项 在手动清理 TLS 变量时,需要注意一些问题。首先,要确保在清理之前,已经获取到了正确的 TLS 变量值,并且这个值确实是需要清理的。如果误清理了其他线程正在使用的资源,可能会导致程序崩溃。其次,清理操作要与系统的自动清理机制相互配合。例如,如果手动清理后,线程又重新设置了 TLS 变量的值,那么当线程正常终止或被取消时,系统仍然会调用清理函数,这就需要保证清理函数在这种情况下能够正确处理。

处理复杂数据结构的 TLS 清理

  1. 嵌套数据结构的清理 在实际应用中,TLS 变量可能指向一个复杂的嵌套数据结构,如链表、树等。对于这种情况,清理函数需要递归地清理整个数据结构。例如,假设我们有一个 TLS 变量指向一个链表:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
    int data;
    struct Node *next;
} Node;

pthread_key_t tls_key;

void list_destructor(void *data) {
    Node *head = (Node *)data;
    Node *current = head;
    Node *next;
    while (current) {
        next = current->next;
        free(current);
        current = next;
    }
}

void *thread_function(void *arg) {
    Node *head = (Node *)malloc(sizeof(Node));
    head->data = *((int *)arg);
    head->next = NULL;
    pthread_setspecific(tls_key, head);
    // 线程执行其他操作
    return NULL;
}

int main() {
    if (pthread_key_create(&tls_key, list_destructor) != 0) {
        perror("pthread_key_create");
        return 1;
    }
    pthread_t threads[3];
    int args[3] = {1, 2, 3};
    for (int i = 0; i < 3; i++) {
        if (pthread_create(&threads[i], NULL, thread_function, &args[i]) != 0) {
            perror("pthread_create");
            return 1;
        }
    }
    for (int i = 0; i < 3; i++) {
        if (pthread_join(threads[i], NULL) != 0) {
            perror("pthread_join");
            return 1;
        }
    }
    if (pthread_key_delete(tls_key) != 0) {
        perror("pthread_key_delete");
        return 1;
    }
    return 0;
}

在上述代码中,list_destructor 函数会遍历整个链表并释放每个节点的内存。

  1. 共享资源的处理 如果 TLS 变量中的数据结构包含一些共享资源(如共享内存段、信号量等),清理时需要特别小心。首先要确保在清理共享资源之前,所有相关的线程都已经完成对该资源的操作。可以使用同步机制(如互斥锁、条件变量等)来实现这一点。例如,假设 TLS 变量指向一个包含共享内存段指针的结构体:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

typedef struct SharedData {
    int *shared_mem;
    pthread_mutex_t mutex;
} SharedData;

pthread_key_t tls_key;

void shared_data_destructor(void *data) {
    SharedData *sd = (SharedData *)data;
    pthread_mutex_destroy(&sd->mutex);
    munmap(sd->shared_mem, sizeof(int));
    free(sd);
}

void *thread_function(void *arg) {
    SharedData *sd = (SharedData *)malloc(sizeof(SharedData));
    int fd = shm_open("/shared_mem", O_CREAT | O_RDWR, 0666);
    if (fd == -1) {
        perror("shm_open");
        return NULL;
    }
    if (ftruncate(fd, sizeof(int)) == -1) {
        perror("ftruncate");
        close(fd);
        return NULL;
    }
    sd->shared_mem = (int *)mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (sd->shared_mem == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return NULL;
    }
    close(fd);
    pthread_mutex_init(&sd->mutex, NULL);
    pthread_setspecific(tls_key, sd);
    // 线程执行其他操作
    return NULL;
}

int main() {
    if (pthread_key_create(&tls_key, shared_data_destructor) != 0) {
        perror("pthread_key_create");
        return 1;
    }
    pthread_t threads[3];
    for (int i = 0; i < 3; i++) {
        if (pthread_create(&threads[i], NULL, thread_function, NULL) != 0) {
            perror("pthread_create");
            return 1;
        }
    }
    for (int i = 0; i < 3; i++) {
        if (pthread_join(threads[i], NULL) != 0) {
            perror("pthread_join");
            return 1;
        }
    }
    if (pthread_key_delete(tls_key) != 0) {
        perror("pthread_key_delete");
        return 1;
    }
    shm_unlink("/shared_mem");
    return 0;
}

在这个示例中,shared_data_destructor 函数在清理时,首先销毁互斥锁,然后解除映射并释放共享内存,最后释放结构体本身。

异常情况下的 TLS 清理

  1. 线程取消时的清理 当线程被取消(通过 pthread_cancel 函数)时,清理函数同样会被调用。但是,线程取消有不同的类型,分为异步取消和延迟取消。在延迟取消模式下,线程会在到达取消点时才会真正被取消,而在异步取消模式下,线程可能在任何时刻被取消。对于这两种情况,清理函数都需要正确处理。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

pthread_key_t tls_key;

void destructor(void *data) {
    if (data) {
        printf("Cleaning up on thread cancellation\n");
        free(data);
    }
}

void *thread_function(void *arg) {
    int *thread_specific_data = (int *)malloc(sizeof(int));
    *thread_specific_data = *((int *)arg);
    pthread_setspecific(tls_key, thread_specific_data);
    // 设置线程为延迟取消模式
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
    pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);
    // 模拟一些工作
    for (int i = 0; i < 1000000; i++);
    // 这里是一个取消点
    pthread_testcancel();
    // 线程继续执行
    return NULL;
}

int main() {
    if (pthread_key_create(&tls_key, destructor) != 0) {
        perror("pthread_key_create");
        return 1;
    }
    pthread_t thread;
    int arg = 42;
    if (pthread_create(&thread, NULL, thread_function, &arg) != 0) {
        perror("pthread_create");
        return 1;
    }
    sleep(1);
    if (pthread_cancel(thread) != 0) {
        perror("pthread_cancel");
        return 1;
    }
    if (pthread_join(thread, NULL) != 0) {
        perror("pthread_join");
        return 1;
    }
    if (pthread_key_delete(tls_key) != 0) {
        perror("pthread_key_delete");
        return 1;
    }
    return 0;
}

在上述代码中,我们将线程设置为延迟取消模式,并在适当的位置设置了取消点 pthread_testcancel。当线程被取消时,清理函数 destructor 会被调用。

  1. 信号处理中的 TLS 清理 在多线程程序中,信号处理也是一个需要考虑 TLS 清理的场景。当一个线程接收到信号时,可能需要清理相关的 TLS 变量。但是,由于信号处理函数的执行环境比较特殊,需要注意避免在信号处理函数中调用一些不安全的函数。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

pthread_key_t tls_key;

void destructor(void *data) {
    if (data) {
        printf("Cleaning up on signal\n");
        free(data);
    }
}

void signal_handler(int signum) {
    void *data = pthread_getspecific(tls_key);
    if (data) {
        // 这里可以进行一些简单的清理操作
        // 注意不要调用不安全的函数
        free(data);
        pthread_setspecific(tls_key, NULL);
    }
}

void *thread_function(void *arg) {
    int *thread_specific_data = (int *)malloc(sizeof(int));
    *thread_specific_data = *((int *)arg);
    pthread_setspecific(tls_key, thread_specific_data);
    struct sigaction sa;
    sa.sa_handler = signal_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if (sigaction(SIGTERM, &sa, NULL) == -1) {
        perror("sigaction");
        return NULL;
    }
    // 线程执行其他操作
    while (1);
    return NULL;
}

int main() {
    if (pthread_key_create(&tls_key, destructor) != 0) {
        perror("pthread_key_create");
        return 1;
    }
    pthread_t thread;
    int arg = 42;
    if (pthread_create(&thread, NULL, thread_function, &arg) != 0) {
        perror("pthread_create");
        return 1;
    }
    sleep(5);
    if (pthread_kill(thread, SIGTERM) != 0) {
        perror("pthread_kill");
        return 1;
    }
    if (pthread_join(thread, NULL) != 0) {
        perror("pthread_join");
        return 1;
    }
    if (pthread_key_delete(tls_key) != 0) {
        perror("pthread_key_delete");
        return 1;
    }
    return 0;
}

在上述代码中,我们为 SIGTERM 信号设置了处理函数 signal_handler,在信号处理函数中,我们手动清理了 TLS 变量。

总结与最佳实践

  1. 总结 在 Linux C 语言多线程编程中,TLS 清理是一个至关重要的环节。正确的 TLS 清理能够避免资源泄漏、保证数据一致性,从而提高程序的稳定性和可靠性。我们需要理解 TLS 变量的创建、使用以及清理函数的工作原理,掌握手动清理和处理复杂数据结构清理的方法,同时要考虑异常情况下(如线程取消、信号处理)的 TLS 清理。
  2. 最佳实践
    • 始终设置清理函数:在创建 TLS 键时,尽可能设置清理函数,以确保在默认情况下,线程结束时相关资源能够得到正确释放。
    • 小心手动清理:如果需要手动清理 TLS 变量,要确保清理操作的正确性和安全性,避免误操作导致程序崩溃。
    • 测试异常情况:对线程取消、信号处理等异常情况进行充分测试,确保 TLS 清理在这些情况下也能正常工作。
    • 文档化清理逻辑:对于复杂的数据结构和清理逻辑,要进行详细的文档说明,以便其他开发人员理解和维护代码。

通过遵循这些最佳实践,我们能够更好地利用 TLS 机制,编写出健壮、高效的多线程 Linux C 程序。