Linux系统编程之线程详解-C语言版
在进程之后,线程是Linux系统编程中另一个核心的并发执行单元。相比进程,线程更轻量、资源开销更低,是实现多任务并发的重要手段。本文将从线程的基本概念、核心操作、同步机制到实际应用,全面解析Linux线程编程。
一、线程的基本概念
什么是线程?
线程(Thread)是进程内的一个执行单元,是操作系统调度的基本单位。一个进程可以包含多个线程,所有线程共享进程的资源(如内存空间、文件描述符等),但拥有独立的执行路径(程序计数器、栈、寄存器)。
简单来说:进程是资源分配的基本单位,线程是调度执行的基本单位。
线程与进程的核心区别
| 对比项 | 进程(Process) | 线程(Thread) |
|---|---|---|
| 资源分配 | 拥有独立的地址空间、文件描述符等资源 | 共享所属进程的资源,不独立分配资源 |
| 开销 | 创建/销毁开销大(需分配资源) | 创建/销毁开销小(仅需栈和寄存器) |
| 通信方式 | 依赖IPC机制(管道、共享内存等) | 可直接访问共享内存(全局变量等) |
| 调度粒度 | 较粗(进程切换需保存完整资源状态) | 较细(仅需保存线程私有状态) |
| 独立性 | 进程崩溃不影响其他进程 | 线程崩溃可能导致整个进程崩溃 |
线程的优势
- 轻量级并发:创建和销毁线程的开销远小于进程,适合频繁创建销毁的场景。
- 高效通信:线程共享进程内存空间,通过全局变量即可通信,无需复杂IPC机制。
- 资源利用率高:多线程可充分利用多核CPU,提高程序吞吐量(如服务器同时处理多个请求)。
- 响应性提升:在IO密集型任务中,一个线程等待IO时,其他线程可继续执行(如GUI程序避免界面卡顿)。
二、线程的核心操作(基于POSIX线程库)
Linux下线程编程主要依赖POSIX线程库(pthread),提供了创建、终止、同步等核心接口。使用时需包含头文件 <pthread.h>,编译时需链接线程库(-lpthread 选项)。
1. 线程创建(pthread_create)
函数原型
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
参数说明
thread:传出参数,用于存储新创建线程的ID。attr:线程属性(如分离态、栈大小),NULL表示使用默认属性。start_routine:线程入口函数(函数指针),线程创建后会执行该函数。arg:传递给线程入口函数的参数。
返回值
- 成功:返回
0;失败:返回非0错误码(注意:不是设置errno)。
示例代码
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
// 线程入口函数
void *thread_func(void *arg) {
char *msg = (char *)arg;
printf("子线程:%s(线程ID:%lu)\n", msg, pthread_self()); // pthread_self()获取当前线程ID
sleep(2); // 模拟线程工作
return (void *)123; // 线程退出返回值
}
int main() {
pthread_t tid; // 线程ID
char *msg = "Hello from main";
// 创建线程
int ret = pthread_create(&tid, NULL, thread_func, msg);
if (ret != 0) {
fprintf(stderr, "线程创建失败:%d\n", ret);
return 1;
}
printf("主线程:已创建子线程(ID:%lu)\n", tid);
sleep(3); // 等待子线程执行完毕
return 0;
}
编译运行
gcc thread_demo.c -o thread_demo -lpthread # 必须链接pthread库
./thread_demo
输出
主线程:已创建子线程(ID:140561126704896)
子线程:Hello from main(线程ID:140561126704896)
2. 线程终止与资源回收
线程终止有三种常见方式:
- 线程入口函数执行完毕并返回(正常终止)。
- 线程调用
pthread_exit()主动退出。 - 其他线程调用
pthread_cancel()强制终止该线程。
(1)线程主动退出(pthread_exit)
#include <pthread.h>
void pthread_exit(void *retval);
retval:线程退出返回值,可被pthread_join()捕获。
示例:
void *thread_func(void *arg) {
printf("子线程:执行中...\n");
pthread_exit((void *)456); // 主动退出,返回456
}
(2)等待线程终止并回收资源(pthread_join)
类似于进程的 wait(),pthread_join() 用于主线程等待子线程终止并获取返回值,避免线程资源泄漏。
函数原型:
int pthread_join(pthread_t thread, void **retval);
thread:要等待的线程ID。retval:传出参数,用于存储线程的退出返回值(pthread_exit或函数返回值)。
示例:
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
void *ret;
pthread_join(tid, &ret); // 阻塞等待子线程结束
printf("子线程退出返回值:%d\n", (int)ret); // 输出456
return 0;
}
注意:若不调用 pthread_join() 且未设置线程为分离态,线程终止后会成为“僵尸线程”,占用系统资源。
(3)线程分离(pthread_detach)
对于无需等待退出状态的线程,可设置为分离态(Detached),线程终止后资源会自动回收,无需 pthread_join()。
函数原型:
int pthread_detach(pthread_t thread);
示例(两种设置方式):
// 方式1:创建时通过属性设置分离态
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // 设置分离态
pthread_create(&tid, &attr, thread_func, NULL);
pthread_attr_destroy(&attr);
// 方式2:创建后调用pthread_detach
pthread_create(&tid, NULL, thread_func, NULL);
pthread_detach(tid); // 标记为分离态
3. 强制终止线程(pthread_cancel)
强制终止指定线程,需线程本身可被取消(默认允许取消)。
函数原型:
int pthread_cancel(pthread_t thread);
示例:
void *thread_func(void *arg) {
while (1) {
printf("子线程:运行中...\n");
sleep(1);
}
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
sleep(3); // 让子线程运行3秒
pthread_cancel(tid); // 强制终止子线程
pthread_join(tid, NULL); // 回收资源
return 0;
}
注意:pthread_cancel() 并非立即终止线程,而是向线程发送取消请求,线程会在“取消点”(如系统调用、pthread_testcancel())处响应并终止。
三、线程的共享与私有资源
线程与进程的核心区别在于资源共享,明确哪些资源共享、哪些私有是线程编程的基础。
1. 共享资源(线程间可见)
- 进程地址空间:代码段、数据段、堆(全局变量、静态变量、动态分配的内存
malloc)。 - 文件描述符表:打开的文件、管道、套接字等。
- 信号处理函数:进程级的信号处理配置。
- 当前工作目录、用户ID/组ID 等进程属性。
- 系统资源限制、信号掩码。
2. 私有资源(线程独有)
- 线程ID(TID):用于标识线程(
pthread_self()获取)。 - 栈空间:每个线程有独立的栈,存储局部变量和函数调用上下文。
- 寄存器状态:程序计数器(PC)、栈指针(SP)等,确保线程独立执行。
- 线程特有数据(TSD):通过
pthread_key_create等函数创建的线程私有数据。 - errno变量:现代Linux中每个线程有独立的
errno副本(避免多线程干扰)。
示例:共享全局变量
#include <pthread.h>
#include <stdio.h>
int global_count = 0; // 全局变量,线程共享
void *thread_inc(void *arg) {
for (int i = 0; i < 10000; i++) {
global_count++; // 线程1修改全局变量
}
return NULL;
}
void *thread_dec(void *arg) {
for (int i = 0; i < 10000; i++) {
global_count--; // 线程2修改全局变量
}
return NULL;
}
int main() {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, thread_inc, NULL);
pthread_create(&tid2, NULL, thread_dec, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("最终global_count:%d\n", global_count); // 预期0,但实际可能非0?
return 0;
}
问题:上述代码运行后 global_count 可能不为0,因为 global_count++ 不是原子操作(本质是“读取-修改-写入”三步,可能被其他线程打断),导致竞态条件(Race Condition)。这就是线程同步问题的根源。
四、线程同步:解决竞态条件
当多个线程并发访问共享资源时,若操作非原子,会导致数据不一致(如上述计数器问题)。线程同步机制通过控制资源访问顺序,确保操作的原子性和正确性。
1. 互斥锁(Mutex):最常用的同步工具
互斥锁(Mutual Exclusion)通过“加锁-操作-解锁”流程,确保同一时间只有一个线程访问共享资源。
核心操作函数
| 函数 | 功能 |
|---|---|
pthread_mutex_init() | 初始化互斥锁 |
pthread_mutex_lock() | 加锁(若已锁则阻塞等待) |
pthread_mutex_unlock() | 解锁(唤醒等待的线程) |
pthread_mutex_destroy() | 销毁互斥锁 |
示例:用互斥锁解决计数器问题
#include <pthread.h>
#include <stdio.h>
int global_count = 0;
pthread_mutex_t mutex; // 声明互斥锁
void *thread_inc(void *arg) {
for (int i = 0; i < 10000; i++) {
pthread_mutex_lock(&mutex); // 加锁
global_count++; // 临界区操作(原子化)
pthread_mutex_unlock(&mutex); // 解锁
}
return NULL;
}
void *thread_dec(void *arg) {
for (int i = 0; i < 10000; i++) {
pthread_mutex_lock(&mutex); // 加锁
global_count--; // 临界区操作
pthread_mutex_unlock(&mutex); // 解锁
}
return NULL;
}
int main() {
pthread_mutex_init(&mutex, NULL); // 初始化互斥锁
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, thread_inc, NULL);
pthread_create(&tid2, NULL, thread_dec, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("最终global_count:%d\n", global_count); // 正确输出0
pthread_mutex_destroy(&mutex); // 销毁互斥锁
return 0;
}
关键:global_count++ 被包裹在 pthread_mutex_lock 和 pthread_mutex_unlock 之间,成为“临界区”,确保同一时间只有一个线程执行,避免竞态条件。
2. 条件变量(Condition Variable):线程间协作
条件变量是线程间基于特定条件的同步机制,常用于线程间的“等待-通知”场景(如生产者-消费者模型)。它必须与互斥锁配合使用,确保条件判断和操作的原子性。
完整示例:生产者-消费者模型
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define BUFFER_SIZE 5 // 缓冲区大小
int buffer[BUFFER_SIZE]; // 共享缓冲区
int in = 0, out = 0; // 生产者/消费者索引(环形缓冲区)
// 同步工具
pthread_mutex_t mutex;
pthread_cond_t not_full; // 缓冲区未满条件(生产者等待)
pthread_cond_t not_empty; // 缓冲区非空条件(消费者等待)
// 生产者线程:向缓冲区添加数据
void *producer(void *arg) {
for (int i = 0; i < 10; i++) { // 生产10个数据
int data = rand() % 100; // 随机生成数据
sleep(1); // 模拟生产耗时
pthread_mutex_lock(&mutex); // 加锁保护缓冲区
// 若缓冲区满,等待"缓冲区未满"信号(防止虚假唤醒,用while循环)
while ((in + 1) % BUFFER_SIZE == out) {
printf("生产者:缓冲区满,等待消费者取数据...\n");
// 阻塞等待not_full信号,同时自动释放mutex锁
pthread_cond_wait(¬_full, &mutex);
// 被唤醒后重新获取mutex锁,需再次检查条件
}
// 向缓冲区放入数据
buffer[in] = data;
printf("生产者:放入数据 %d(位置:%d)\n", data, in);
in = (in + 1) % BUFFER_SIZE; // 更新生产者索引
pthread_cond_signal(¬_empty); // 通知消费者:缓冲区非空
pthread_mutex_unlock(&mutex); // 解锁
}
return NULL;
}
// 消费者线程:从缓冲区取数据
void *consumer(void *arg) {
for (int i = 0; i < 10; i++) {
sleep(2); // 模拟消费耗时
pthread_mutex_lock(&mutex); // 加锁保护缓冲区
// 若缓冲区空,等待"缓冲区非空"信号
while (in == out) {
printf("消费者:缓冲区空,等待生产者放数据...\n");
pthread_cond_wait(¬_empty, &mutex); // 释放锁并阻塞
}
// 从缓冲区取数据
int data = buffer[out];
printf("消费者:取出数据 %d(位置:%d)\n", data, out);
out = (out + 1) % BUFFER_SIZE; // 更新消费者索引
pthread_cond_signal(¬_full); // 通知生产者:缓冲区未满
pthread_mutex_unlock(&mutex); // 解锁
}
return NULL;
}
int main() {
// 初始化同步工具
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(¬_full, NULL);
pthread_cond_init(¬_empty, NULL);
// 创建生产者和消费者线程
pthread_t prod_tid, cons_tid;
pthread_create(&prod_tid, NULL, producer, NULL);
pthread_create(&cons_tid, NULL, consumer, NULL);
// 等待线程结束
pthread_join(prod_tid, NULL);
pthread_join(cons_tid, NULL);
// 销毁同步工具
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(¬_full);
pthread_cond_destroy(¬_empty);
return 0;
}
条件变量核心要点
- 配合互斥锁使用:条件变量必须与互斥锁结合,确保条件判断和操作的原子性。
- while循环检查条件:
pthread_cond_wait可能被“虚假唤醒”(无信号却唤醒),需用while而非if循环检查条件,确保被唤醒后条件确实满足。 - 自动释放锁:
pthread_cond_wait阻塞时会自动释放互斥锁,被唤醒后重新获取锁,避免死锁。 - 唤醒机制:
pthread_cond_signal唤醒一个等待线程,pthread_cond_broadcast唤醒所有等待线程(适合多个线程等待同一条件)。
3. 信号量(Semaphore):多资源同步
信号量是比互斥锁更灵活的同步工具,本质是一个计数器,通过 P(等待) 和 V(释放) 操作控制资源访问,支持多个线程同时访问有限资源(如限制并发数)。
核心操作函数(POSIX信号量)
| 函数 | 功能 |
|---|---|
sem_init(sem_t *sem, int pshared, unsigned int value) | 初始化信号量。pshared=0 表示线程间共享,value 为初始计数。 |
sem_wait(sem_t *sem) | P操作:计数器减1,若计数≤0则阻塞等待。 |
sem_post(sem_t *sem) | V操作:计数器加1,若有线程阻塞则唤醒。 |
sem_destroy(sem_t *sem) | 销毁信号量。 |
示例:限制并发线程数
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <unistd.h>
#define MAX_CONCURRENT 2 // 最大并发线程数
sem_t sem;
void *worker(void *arg) {
int id = *(int *)arg;
free(arg);
sem_wait(&sem); // P操作:获取资源(计数器-1)
printf("线程 %d:开始执行(当前并发数:%d)\n", id, MAX_CONCURRENT - sem_trywait(&sem));
sleep(3); // 模拟任务耗时
printf("线程 %d:执行完毕\n", id);
sem_post(&sem); // V操作:释放资源(计数器+1)
return NULL;
}
int main() {
sem_init(&sem, 0, MAX_CONCURRENT); // 初始化信号量,初始计数=2
pthread_t tids[5];
for (int i = 0; i < 5; i++) {
int *id = malloc(sizeof(int));
*id = i;
pthread_create(&tids[i], NULL, worker, id);
}
for (int i = 0; i < 5; i++) {
pthread_join(tids[i], NULL);
}
sem_destroy(&sem);
return 0;
}
输出说明:同一时间最多有2个线程执行“任务”,其他线程需等待前序线程释放信号量后才能执行,实现了并发数限制。
4. 读写锁(Read-Write Lock):读多写少场景优化
读写锁(pthread_rwlock)专为“读多写少”场景设计,允许多个读者同时读取共享资源,但写者必须独占资源(读写互斥、写写互斥),比互斥锁更高效。
核心操作函数
| 函数 | 功能 |
|---|---|
pthread_rwlock_init() | 初始化读写锁。 |
pthread_rwlock_rdlock() | 获取读锁(多个读者可同时持有)。 |
pthread_rwlock_wrlock() | 获取写锁(独占,需等待所有读锁释放)。 |
pthread_rwlock_unlock() | 释放读锁或写锁。 |
pthread_rwlock_destroy() | 销毁读写锁。 |
示例:读写锁保护共享数据
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
int shared_data = 0;
pthread_rwlock_t rwlock;
// 读者线程:读取数据
void *reader(void *arg) {
int id = *(int *)arg;
while (1) {
pthread_rwlock_rdlock(&rwlock); // 获取读锁
printf("读者 %d:读取数据 = %d(当前可并发读)\n", id, shared_data);
sleep(1); // 模拟读操作耗时
pthread_rwlock_unlock(&rwlock); // 释放读锁
sleep(1); // 间隔一段时间再次读取
}
return NULL;
}
// 写者线程:更新数据
void *writer(void *arg) {
int id = *(int *)arg;
while (1) {
pthread_rwlock_wrlock(&rwlock); // 获取写锁(需等待所有读锁释放)
shared_data++;
printf("===== 写者 %d:更新数据 = %d(独占访问) =====\n", id, shared_data);
sleep(2); // 模拟写操作耗时
pthread_rwlock_unlock(&rwlock); // 释放写锁
sleep(3); // 间隔一段时间再次更新
}
return NULL;
}
int main() {
pthread_rwlock_init(&rwlock, NULL);
pthread_t readers[3], writers[1];
int rids[3] = {1, 2, 3}, wid = 1;
// 创建3个读者线程
for (int i = 0; i < 3; i++) {
pthread_create(&readers[i], NULL, reader, &rids[i]);
}
// 创建1个写者线程
pthread_create(&writers[0], NULL, writer, &wid);
// 等待线程(实际中用信号退出,此处简化)
for (int i = 0; i < 3; i++) {
pthread_join(readers[i], NULL);
}
pthread_join(writers[0], NULL);
pthread_rwlock_destroy(&rwlock);
return 0;
}
输出特点:多个读者可同时读取数据(打印信息重叠),写者更新时会独占资源(读者需等待写锁释放后才能继续读)。
5. 自旋锁(Spin Lock):短时间锁场景优化
自旋锁与互斥锁功能类似,但等待锁时不会阻塞睡眠,而是循环尝试获取锁(“自旋”),适合锁持有时间极短的场景(如内核态操作),避免线程上下文切换开销。
核心操作函数
| 函数 | 功能 |
|---|---|
pthread_spin_init(pthread_spinlock_t *lock, int pshared) | 初始化自旋锁,pshared=0 表示线程间共享。 |
pthread_spin_lock(pthread_spinlock_t *lock) | 获取自旋锁,若未获取则循环等待。 |
pthread_spin_unlock(pthread_spinlock_t *lock) | 释放自旋锁。 |
pthread_spin_destroy(pthread_spinlock_t *lock) | 销毁自旋锁。 |
注意事项
- 适用场景:锁持有时间短(如几微秒),且CPU核心数充足(避免单核心下自旋浪费资源)。
- 禁用阻塞操作:持有自旋锁时不能调用
sleep、pthread_cond_wait等阻塞函数,否则会导致其他线程自旋等待,浪费CPU。 - 避免递归:自旋锁不支持递归获取(同一线程多次获取会死锁)。
五、线程特有数据(Thread-Specific Data, TSD)
线程特有数据(TSD)用于线程需要私有数据但无法通过函数参数传递的场景(如每个线程有独立的日志句柄、内存分配器)。TSD通过“键-值”映射实现,每个线程可通过全局键访问自己的私有值。
核心操作函数
| 函数 | 功能 |
|---|---|
pthread_key_create(pthread_key_t *key, void (*destructor)(void *)) | 创建TSD键,destructor 为线程退出时的清理函数。 |
pthread_setspecific(pthread_key_t key, const void *value) | 为当前线程设置键对应的私有值。 |
pthread_getspecific(pthread_key_t key) | 获取当前线程键对应的私有值。 |
pthread_key_delete(pthread_key_t key) | 删除TSD键(不调用清理函数,需手动释放资源)。 |
示例:线程私有日志句柄
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
pthread_key_t log_key; // TSD全局键
// 日志句柄清理函数:线程退出时关闭文件
void log_destructor(void *value) {
FILE *fp = (FILE *)value;
if (fp) {
fclose(fp);
printf("线程 %lu:日志文件关闭\n", pthread_self());
}
}
// 初始化TSD键
void init_log_key() {
pthread_key_create(&log_key, log_destructor);
}
// 线程日志函数:每个线程写入自己的日志文件
void thread_log(const char *msg) {
FILE *fp = (FILE *)pthread_getspecific(log_key);
if (!fp) {
// 首次调用:为当前线程创建日志文件
char filename[20];
sprintf(filename, "thread_%lu.log", pthread_self());
fp = fopen(filename, "a");
pthread_setspecific(log_key, fp); // 绑定到TSD键
}
fprintf(fp, "%s\n", msg);
fflush(fp);
}
// 线程函数:写入日志
void *thread_func(void *arg) {
thread_log("线程开始执行");
thread_log("处理任务中...");
thread_log("线程即将退出");
return NULL; // 退出时自动调用log_destructor关闭文件
}
int main() {
init_log_key();
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);
pthread_key_delete(log_key); // 删除TSD键
return 0;
}
效果:两个线程分别写入自己的日志文件(thread_<tid>.log),线程退出时自动关闭文件,实现了数据隔离。
1123

被折叠的 条评论
为什么被折叠?



