Linux 多线程编程入门:线程栈、TLS、互斥锁与条件变量详解

Linux 多线程编程入门:线程栈、TLS、互斥锁与条件变量详解

Linux 多线程编程是并发编程的基础,尤其在服务器、游戏、数据处理等场景下,能显著提升程序效率。Linux 主要通过 POSIX Threads(pthread)库 实现多线程(需包含 <pthread.h> 头文件,编译时加 -pthread 选项)。

这篇文章针对入门者,从线程栈(Thread Stack)、TLS(Thread Local Storage,线程局部存储)、互斥锁(Mutex)和条件变量(Condition Variable)四个核心概念入手,结合原理、代码示例和避坑指南。假设你有 C/C++ 基础,建议边读边在 Linux 环境下编译运行代码(用 gcc -o test test.c -pthread)。

1. 线程栈(Thread Stack):每个线程的“私人空间”

原理

  • 每个线程都有独立的栈空间,用于存储局部变量、函数调用帧等。主线程栈大小通常由系统决定(默认 8MB),子线程栈可自定义。
  • 为什么需要?多线程共享进程内存(全局变量、堆),但栈是线程私有的,避免并发干扰。
  • Linux 默认子线程栈大小 2MB(可通过 ulimit -s 查看),太小可能栈溢出(stack overflow),太大浪费内存。
  • 栈大小影响:递归深度、局部数组大小等。

关键函数

函数作用参数示例
pthread_attr_init初始化线程属性对象pthread_attr_t attr; pthread_attr_init(&attr);
pthread_attr_setstacksize设置线程栈大小pthread_attr_setstacksize(&attr, 1024*1024); // 1MB
pthread_create创建线程(可传入 attr)pthread_create(&tid, &attr, func, arg);
pthread_attr_destroy销毁属性对象pthread_attr_destroy(&attr);

代码示例(自定义栈大小创建线程):

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

void* thread_func(void* arg) {
    int local_var = 42;  // 存放在线程栈上
    printf("Thread ID: %lu, Local Var: %d\n", pthread_self(), local_var);
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setstacksize(&attr, 1024 * 1024);  // 设置 1MB 栈

    pthread_create(&tid, &attr, thread_func, NULL);
    pthread_join(tid, NULL);  // 等待线程结束

    pthread_attr_destroy(&attr);
    return 0;
}

避坑指南

  • 栈溢出:避免大局部数组(改用堆 malloc),或增大栈大小。但别太大(>系统限制)。
  • 默认栈:不设置 attr 时,用系统默认(pthread_create(&tid, NULL, func, arg))。
  • 调试:用 gdbvalgrind 检查栈问题。

2. TLS(Thread Local Storage):线程的“私人变量”

原理

  • TLS 允许每个线程有自己的“全局变量”副本,避免共享变量的竞争。适合存储线程私有数据,如 errno、随机种子。
  • Linux 通过 __thread 关键字(GCC 扩展)或 pthread_key_t 实现。
  • 为什么用?多线程下全局变量共享易出问题,TLS 提供线程隔离。
  • 生命周期:线程创建时分配,退出时销毁(可注册析构函数)。

两种实现方式对比

方式描述优缺点
__thread 关键字简单声明(如 __thread int tls_var;高效、静态分配;不支持动态创建
pthread_key_t动态创建键,线程间共享键但值独立灵活、可注册析构;稍慢

关键函数(pthread_key_t 方式):

函数作用示例
pthread_key_create创建 TLS 键(可传析构函数)pthread_key_create(&key, destructor);
pthread_setspecific为当前线程设置值pthread_setspecific(key, value);
pthread_getspecific获取当前线程的值void* val = pthread_getspecific(key);
pthread_key_delete删除键(不销毁值)pthread_key_delete(key);

代码示例(用 pthread_key_t 存储线程 ID):

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

pthread_key_t tls_key;

void destructor(void* arg) {
    printf("Thread %lu: Cleaning up %p\n", pthread_self(), arg);
    free(arg);  // 释放动态分配的值
}

void* thread_func(void* arg) {
    int* tls_val = malloc(sizeof(int));
    *tls_val = (int)(long)arg;  // 模拟线程私有数据
    pthread_setspecific(tls_key, tls_val);

    printf("Thread %lu: TLS value = %d\n", pthread_self(), *(int*)pthread_getspecific(tls_key));
    return NULL;
}

int main() {
    pthread_key_create(&tls_key, destructor);

    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, thread_func, (void*)1);
    pthread_create(&tid2, NULL, thread_func, (void*)2);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    pthread_key_delete(tls_key);
    return 0;
}

避坑指南

  • 内存泄漏:用析构函数释放动态分配的值。
  • 兼容性:__thread 只适合简单类型;复杂场景用 pthread_key_t。
  • 性能:TLS 访问比全局变量慢(涉及线程上下文切换),别滥用。

3. 互斥锁(Mutex):守护共享资源的“门锁”

原理

  • 互斥锁确保同一时间只有一个线程访问共享资源,防止数据竞争(race condition)。
  • Linux pthread_mutex_t 支持递归/非递归锁,默认非递归。
  • 工作流程:加锁(lock) → 操作资源 → 解锁(unlock)。失败时阻塞或返回错误。

关键函数

函数作用示例
pthread_mutex_init初始化锁(可设置属性)pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock加锁(阻塞式)pthread_mutex_lock(&mutex);
pthread_mutex_trylock尝试加锁(非阻塞,返回 EBUSY 若失败)if (pthread_mutex_trylock(&mutex) == 0) {…}
pthread_mutex_unlock解锁pthread_mutex_unlock(&mutex);
pthread_mutex_destroy销毁锁pthread_mutex_destroy(&mutex);

代码示例(多线程计数器,避免竞争):

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

pthread_mutex_t mutex;
int counter = 0;

void* thread_func(void* arg) {
    for (int i = 0; i < 100000; i++) {
        pthread_mutex_lock(&mutex);
        counter++;  // 共享资源
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main() {
    pthread_mutex_init(&mutex, NULL);

    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, thread_func, NULL);
    pthread_create(&tid2, NULL, thread_func, NULL);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    printf("Final counter: %d\n", counter);  // 应为 200000
    pthread_mutex_destroy(&mutex);
    return 0;
}

避坑指南

  • 死锁:避免嵌套锁或反序加锁。用 trylock 检测。
  • 性能:锁粒度要细(只锁必要代码),否则线程争用严重。
  • 属性:用 pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); 支持递归锁。

4. 条件变量(Condition Variable):线程的“信号灯”

原理

  • 条件变量用于线程间同步:一个线程等待条件成立,另一个线程信号通知。
  • 必须与互斥锁结合使用(pthread_cond_wait 会原子解锁+等待)。
  • 典型场景:生产者-消费者模型(队列满/空时等待)。

关键函数

函数作用示例
pthread_cond_init初始化条件变量pthread_cond_init(&cond, NULL);
pthread_cond_wait等待信号(需持锁,原子解锁+等待)pthread_cond_wait(&cond, &mutex);
pthread_cond_signal唤醒一个等待线程pthread_cond_signal(&cond);
pthread_cond_broadcast唤醒所有等待线程pthread_cond_broadcast(&cond);
pthread_cond_destroy销毁条件变量pthread_cond_destroy(&cond);

代码示例(简单生产者-消费者):

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

pthread_mutex_t mutex;
pthread_cond_t cond;
int ready = 0;

void* producer(void* arg) {
    pthread_mutex_lock(&mutex);
    ready = 1;
    printf("Producer: Data ready!\n");
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);
    return NULL;
}

void* consumer(void* arg) {
    pthread_mutex_lock(&mutex);
    while (!ready) {  // 用 while 防 spurious wakeup
        pthread_cond_wait(&cond, &mutex);
    }
    printf("Consumer: Got data!\n");
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);

    pthread_t prod, cons;
    pthread_create(&cons, NULL, consumer, NULL);
    sleep(1);  // 确保消费者先等待
    pthread_create(&prod, NULL, producer, NULL);

    pthread_join(prod, NULL);
    pthread_join(cons, NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    return 0;
}

避坑指南

  • spurious wakeup:总是用 while 循环检查条件,别用 if。
  • 必须持锁调用 wait/signal:否则未定义行为。
  • broadcast vs signal:多消费者用 broadcast,避免饥饿。

总结 & 进阶建议

  • 线程栈 & TLS:处理线程私有数据,避免共享冲突。
  • 互斥锁 & 条件变量:处理共享资源和同步,组合使用是王道。
  • 常见问题:死锁、竞争、内存泄漏——用工具如 helgrind(valgrind 插件)调试。
  • 练习:实现一个多线程队列(用锁+条件变量),或用 TLS 存储线程日志。
  • 进阶:学习 pthread_barrier(屏障)、读写锁(rwlock)、自旋锁(spinlock)。

多线程编程易出错,建议从小例子开始,逐步加复杂性。如果你有具体代码问题或想看某个示例的扩展,直接说,我帮你细化!

文章已创建 4665

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部