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))。 - 调试:用
gdb或valgrind检查栈问题。
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)。
多线程编程易出错,建议从小例子开始,逐步加复杂性。如果你有具体代码问题或想看某个示例的扩展,直接说,我帮你细化!