一、线程分离属性(Detached Thread)
1.1 什么是线程分离?
每个线程在创建时有两种状态:
- 可结合(joinable)线程 (默认):线程终止后,其资源(如栈、寄存器状态)不会被自动释放,必须由其他线程调用
pthread_join()来回收。如果忘记调用,就会产生僵尸线程,造成资源泄漏。 - 分离(detached)线程 :线程终止时,系统会自动回收其所有资源,无需
pthread_join()。
关键点:一个线程在生命周期内要么是可结合的,要么是分离的,不能同时是两者。
1.2 为什么需要分离线程?
在实际开发中,很多线程扮演"即用即弃"的角色------执行完特定任务后,我们不需要获取其返回值,也不关心其退出状态。对于这类线程:
- 如果使用可结合方式,必须显式调用
pthread_join(),增加代码复杂度; - 如果忘记
pthread_join(),会造成资源泄漏。
分离线程正是为解决这个问题而设计:让线程退出时自动清理资源,简化代码,避免僵尸线程。
1.3 如何设置线程分离?
有两种方式:
方式一:通过线程属性对象(创建时设置)
这是标准且推荐的方法。使用 pthread_attr_t 设置分离状态,然后将属性传入 pthread_create()。
相关函数:
c
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
detachstate参数:PTHREAD_CREATE_DETACHED:创建分离线程PTHREAD_CREATE_JOINABLE:创建可结合线程(默认)
示例代码:
c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *thread_func(void *arg) {
printf("分离线程 TID=%lu 开始执行\n", pthread_self());
sleep(1);
printf("分离线程退出,资源由系统自动回收\n");
return NULL;
}
int main() {
pthread_t tid;
pthread_attr_t attr;
int ret;
// 1. 初始化属性对象
pthread_attr_init(&attr);
// 2. 设置为分离状态
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// 3. 创建线程
ret = pthread_create(&tid, &attr, thread_func, NULL);
if (ret != 0) {
perror("pthread_create");
pthread_attr_destroy(&attr);
return -1;
}
// 4. 属性对象不再需要,销毁
pthread_attr_destroy(&attr);
// 主线程不需要 pthread_join,等待子线程执行
sleep(2);
printf("主线程结束\n");
return 0;
}
方式二:线程创建后调用 pthread_detach()
如果线程已经以默认(可结合)状态创建,可以在运行时调用 pthread_detach() 将其转为分离状态。
c
int pthread_detach(pthread_t thread);
两种用法:
- 其他线程分离目标线程 :
pthread_detach(tid); - 线程自己分离自己 :
pthread_detach(pthread_self());
示例:
c
void *thread_func(void *arg) {
pthread_detach(pthread_self()); // 自己分离自己
// ... 执行任务
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
// 也可以由主线程分离:pthread_detach(tid);
sleep(1);
return 0;
}
1.4 分离线程 vs 可结合线程 对比
| 特性 | 可结合 (joinable) | 分离 (detached) |
|---|---|---|
| 资源回收 | 必须调用 pthread_join() |
线程退出时系统自动回收 |
| 能否获取返回值 | 可以(通过 pthread_join) |
不能 |
| 默认状态 | ✅ 是 | ❌ 否,需显式设置 |
| 僵尸线程风险 | 有(忘记回收时) | 无 |
| 适用场景 | 需要知道线程执行结果或退出状态 | 后台任务、无需关注结果的服务线程 |
二、线程间通信(Inter-thread Communication)
2.1 线程间通信的必要性
线程位于同一进程空间内,共享进程的数据段 (全局变量、静态变量)、堆区 、文件描述符 等。因此,线程间天然可以通过共享变量进行通信。例如:
c
int shared_data = 0; // 全局变量,所有线程共享
void *thread1(void *arg) { shared_data = 100; }
void *thread2(void *arg) { printf("%d\n", shared_data); }
但这种方式会引入严重的 资源竞争(Race Condition)问题。当多个线程同时读写同一共享变量时,由于CPU时间片调度的不确定性,可能导致数据不一致。
2.2 临界区与原子操作
- 临界区(Critical Section):访问共享资源的代码片段。
- 原子操作(Atomic Operation):不可被中断的操作,即执行过程中不会被线程调度打断。
我们通常需要将临界区"保护"起来,确保同一时刻只有一个线程进入,这就是互斥锁(Mutex) 的作用。
三、互斥锁(Mutex)
互斥锁是最基本的同步机制,通过 pthread_mutex_t 类型实现。
3.1 基本函数接口
c
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex,
const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex); // 加锁(阻塞)
int pthread_mutex_unlock(pthread_mutex_t *mutex); // 解锁
int pthread_mutex_trylock(pthread_mutex_t *mutex); // 尝试加锁(非阻塞)
pthread_mutex_init:初始化锁,attr通常为NULL(使用默认属性)。pthread_mutex_lock:如果锁已被占用,则阻塞等待;否则获取锁。pthread_mutex_unlock:释放锁,唤醒等待线程。pthread_mutex_trylock:尝试加锁,若锁已被占用则立即返回错误,不阻塞。
3.2 使用示例(保护共享变量)
c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int counter = 0; // 共享变量
pthread_mutex_t mutex; // 互斥锁
void *increment(void *arg) {
for (int i = 0; i < 100000; ++i) {
pthread_mutex_lock(&mutex); // 加锁
counter++; // 临界区
pthread_mutex_unlock(&mutex); // 解锁
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_mutex_init(&mutex, NULL);
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&mutex);
printf("counter = %d\n", counter); // 正确结果 200000
return 0;
}
注意:锁的粒度要适中,既不能太大(降低并发),也不能太小(无法有效保护)。
3.3 死锁(Deadlock)
死锁是指多个线程因竞争资源而相互等待,导致所有线程无法继续执行的状态。
死锁产生的四个必要条件:
- 互斥条件:资源只能被一个线程占用。
- 不可剥夺条件:资源只能由占用者主动释放。
- 请求与保持条件:线程持有部分资源,同时等待其他资源。
- 循环等待条件:多个线程形成循环等待链。
避免死锁的常用方法:
- 使用
pthread_mutex_trylock替代pthread_mutex_lock,加锁失败时释放已持有的锁并重试。 - 保持一致的加锁顺序:所有线程按相同顺序获取多把锁。
- 减少锁的数量,尽量将资源合并。
四、信号量(Semaphore)
信号量是一种更通用的同步机制,可以用于线程间或进程间同步。在Linux中,信号量通过 sem_t 类型实现(POSIX 无名信号量)。
4.1 核心函数
c
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
int sem_wait(sem_t *sem); // 申请资源(P操作)
int sem_post(sem_t *sem); // 释放资源(V操作)
int sem_trywait(sem_t *sem); // 非阻塞申请
sem_init:初始化信号量,value为初始资源数;pshared为 0 表示线程间共享,非 0 表示进程间共享。sem_wait:如果信号量值 > 0,则减 1 并立即返回;否则阻塞等待。sem_post:将信号量值加 1,并唤醒等待的线程(若有)。
4.2 信号量 vs 互斥锁
- 互斥锁:只有 0 和 1 两种状态(二元信号量),用于保护临界区。
- 信号量:可以有多个资源计数,可控制同时访问的线程数量(例如限流)。也可作为二进制信号量使用。
4.3 示例:使用信号量控制线程并发数
c
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#define MAX_CONCURRENT 3
sem_t sem;
void *worker(void *arg) {
int id = *(int *)arg;
sem_wait(&sem); // 申请资源,若并发数已满则阻塞
printf("线程 %d 开始工作\n", id);
sleep(2);
printf("线程 %d 结束工作\n", id);
sem_post(&sem); // 释放资源
return NULL;
}
int main() {
pthread_t threads[10];
int ids[10];
sem_init(&sem, 0, MAX_CONCURRENT); // 最多3个线程同时工作
for (int i = 0; i < 10; ++i) {
ids[i] = i;
pthread_create(&threads[i], NULL, worker, &ids[i]);
}
for (int i = 0; i < 10; ++i)
pthread_join(threads[i], NULL);
sem_destroy(&sem);
return 0;
}
五、总结
| 内容 | 核心要点 |
|---|---|
| 线程分离 | 设置 PTHREAD_CREATE_DETACHED 或调用 pthread_detach(),避免僵尸线程 |
| 互斥锁 | 保护临界区,防止资源竞争,注意死锁问题,保持加锁顺序一致 |
| 信号量 | 更灵活的同步工具,可控制资源数量,适用于生产者-消费者等场景 |
| 线程间通信本质 | 共享内存(全局变量、堆区)配合同步机制,确保数据一致性 |
多线程编程既带来了性能提升,也引入了同步复杂性。合理使用分离属性 简化资源管理,借助互斥锁 和信号量保障数据安全,是编写可靠多线程程序的关键。
📌 所有函数接口的详细手册可通过
man 3 函数名查询,建议读者在实际开发中多实践、多调试。