一、线程基本概念
1. 线程定义
线程是轻量级进程(LWP),是进程内部的多个并发执行任务,共享进程的大部分资源(如地址空间、文件描述符),仅拥有私有执行上下文(寄存器、栈、程序计数器)。
2. 线程与进程的核心区别
核心特性 | 进程(Process) | 线程(Thread) |
---|---|---|
资源分配单位 | 系统最小资源分配单位(独立地址空间、资源) | 系统最小执行单位(共享进程资源) |
资源开销 | 大(约 3GB 地址空间) | 小(约 8MB 线程栈) |
数据共享方式 | 需通过 IPC 机制(管道、共享内存等) | 除栈区外全部共享(全局变量、堆、文件描述符) |
稳定性 | 高(一个进程崩溃不影响其他进程) | 低(一个线程崩溃可能导致整个进程崩溃) |
适用场景 | 独立大任务(如浏览器、编辑器) | 同一任务的子任务(如视频发送 + 控制接收) |
归属关系 | 操作系统独立实体 | 隶属于某个进程,依赖进程存在 |
共同点:均支持并发执行,由操作系统调度器分配 CPU 时间。
3. 线程的优缺点
优点 | 缺点 |
---|---|
比多进程节省系统资源(内存、CPU 切换开销) | 稳定性低(线程崩溃影响进程) |
线程间数据共享便捷(无需 IPC) | 调试复杂(需 GDB 专用命令如 info thread ) |
二、线程核心操作(POSIX 函数)
1. 线程创建:pthread_create
函数原型
c
#include <pthread.h>
int pthread_create(
pthread_t *thread, // 输出:存储新线程 ID 的指针
const pthread_attr_t *attr, // 输入:线程属性(NULL 表示默认属性)
void *(*start_routine)(void *), // 输入:线程入口函数(函数指针)
void *arg // 输入:传递给线程入口函数的参数(void* 类型)
);
功能与返回值
- 功能:向内核请求创建新线程,绑定入口函数并传递参数。
- 返回值:成功返回
0
,失败返回非零错误码(如EAGAIN
资源不足、EINVAL
属性无效)。
关键说明
- 一次调用仅创建一个线程,每个进程至少有一个主线程(
main
函数)。 - 主线程退出(如
return
/exit
)会导致所有子线程强制退出(需用pthread_exit
让主线程退出但保留子线程)。 - 查看线程信息:
ps -eLf
或ps -eLo pid,ppid,lwp,stat,comm
(lwp
为内核线程 ID)。
示例代码
c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 线程1入口函数:发送视频
void *th1(void *arg) {
while (1) {
printf("th1 发送视频(tid:%lu)\n", pthread_self());
sleep(1);
}
return NULL;
}
// 线程2入口函数:接收控制
void *th2(void *arg) {
while (1) {
printf("th2 接收控制(tid:%lu)\n", pthread_self());
sleep(1);
}
return NULL;
}
int main() {
pthread_t tid1, tid2;
// 创建两个子线程
pthread_create(&tid1, NULL, th1, NULL);
pthread_create(&tid2, NULL, th2, NULL);
while (1) sleep(1); // 主线程保持运行,避免子线程被终止
return 0;
}
2. 获取线程 ID:pthread_self
函数原型
c
#include <pthread.h>
pthread_t pthread_self(void);
功能与返回值
- 功能:获取当前线程 的用户态 ID(
pthread_t
类型)。 - 返回值:当前线程的 ID(进程内唯一,跨进程不唯一)。
关键说明
- 不可用
==
直接比较线程 ID(pthread_t
可能是结构体),需用pthread_equal(tid1, tid2)
判断是否相等。 - 与内核线程 ID 区别:
pthread_self
返回用户态 ID,gettid()
系统调用返回内核态 TID(ps -L
中的LWP
)。
示例代码
c
// 线程函数中打印自身 ID
printf("th1 tid: %lu\n", pthread_self());
// 主线程打印自身 ID
printf("main tid: %lu\n", pthread_self());
3. 线程退出:两种方式
方式 1:自行退出(自杀)pthread_exit
函数原型
c
#include <pthread.h>
void pthread_exit(void *retval);
功能与参数
- 功能:当前线程主动终止,可返回状态值(供
pthread_join
获取)。 - 参数
retval
:线程返回值(void*
类型,需指向有效内存,避免局部变量)。
关键说明
- 在线程入口函数中,
return retval
与pthread_exit(retval)
效果一致。 - 若在子函数中调用
pthread_exit
,会直接终止整个线程(而非仅退出子函数)。
示例代码
c
void *th(void *arg) {
int i = 3;
while (i--) {
printf("th 运行中(tid:%lu)\n", pthread_self());
sleep(1);
}
pthread_exit(NULL); // 3 秒后自行退出,返回 NULL
}
方式 2:强制退出(他杀)pthread_cancel
函数原型
c
#include <pthread.h>
int pthread_cancel(pthread_t thread);
功能与参数
- 功能:向指定线程发送 "取消请求",线程在取消点 (如
sleep
/printf
/read
)响应并终止。 - 参数
thread
:要取消的线程 ID(由pthread_create
输出)。
返回值
- 成功返回
0
,失败返回非零错误码(如ESRCH
线程不存在)。
示例代码
c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *th(void *arg) {
while (1) {
printf("th 运行中(tid:%lu)\n", pthread_self());
sleep(1); // 取消点:线程会在此处检查取消请求
}
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, th, NULL);
int i = 0;
while (1) {
printf("main 运行中(tid:%lu)\n", pthread_self());
sleep(1);
if (3 == i) { // 3 秒后强制取消子线程
pthread_cancel(tid);
printf("已发送取消请求\n");
}
i++;
}
return 0;
}
4. 线程回收:pthread_join
函数原型
c
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
功能与参数
- 功能:阻塞主线程,等待指定线程结束,回收其内核资源(避免僵尸线程),并获取返回值。
- 参数:
thread
:要回收的线程 ID。retval
:输出:接收线程返回值的二级指针(无需获取返回值则设为NULL
)。
返回值
- 成功返回
0
,失败返回非零错误码(如EINVAL
线程不可连接)。
关键说明
- 仅能回收 "可连接线程"(默认状态),分离线程(
pthread_detach
)无法用pthread_join
回收。 - 线程返回值
*retval
需指向有效内存(堆 / 全局 / 静态变量),避免局部变量(线程结束后栈释放)。
示例代码
c
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void *th(void *arg) {
// 动态分配内存存储返回值(堆内存,线程结束后不释放)
char *str = malloc(20);
strcpy(str, "我要结束了");
sleep(3); // 模拟任务执行
pthread_exit(str); // 返回堆内存地址
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, th, NULL);
void *ret = NULL;
pthread_join(tid, &ret); // 等待线程结束,获取返回值
printf("线程返回值:%s\n", (char*)ret);
free(ret); // 释放线程分配的堆内存
return 0;
}
5. 线程分离属性:pthread_detach
/ 属性初始化
核心作用
将线程设为 "分离状态",线程结束后自动回收资源,无需 pthread_join
(适用于无需获取返回值的线程,如日志线程)。
两种设置方式
方式 1:直接调用 pthread_detach
c
void *th(void *arg) {
pthread_detach(pthread_self()); // 自身设为分离状态
printf("分离线程运行中\n");
sleep(2);
return NULL; // 结束后自动回收资源
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, th, NULL);
sleep(3); // 等待线程结束(避免主线程先退出)
return 0;
}
方式 2:通过线程属性初始化
c
#include <pthread.h>
int main() {
pthread_t tid;
pthread_attr_t attr;
pthread_attr_init(&attr); // 初始化属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // 设置分离属性
pthread_create(&tid, &attr, th, NULL); // 用分离属性创建线程
pthread_attr_destroy(&attr); // 销毁属性(避免内存泄漏)
sleep(3);
return 0;
}
相关函数
pthread_attr_init(pthread_attr_t *attr)
:初始化线程属性。pthread_attr_destroy(pthread_attr_t *attr)
:销毁线程属性,释放资源。
6. 线程清理函数:pthread_cleanup_push/pop
函数原型
c
#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *), void *arg); // 注册清理函数
void pthread_cleanup_pop(int execute); // 触发/取消清理函数
功能
在线程退出时(无论正常退出、pthread_cancel
或 pthread_exit
),自动执行清理操作(如释放内存、关闭文件)。
参数说明
pthread_cleanup_push
:routine
:清理函数(参数为void*
,无返回值)。arg
:传递给清理函数的参数。
pthread_cleanup_pop
:execute
:非 0 表示执行清理函数,0 表示取消清理函数。
示例代码
c
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
// 清理函数:释放内存+关闭文件
typedef struct {
FILE *fp;
char *buf;
} CleanArgs;
void clean(void *arg) {
CleanArgs *args = (CleanArgs*)arg;
printf("执行清理操作\n");
free(args->buf); // 释放堆内存
fclose(args->fp); // 关闭文件
}
void *th(void *arg) {
FILE *fp = fopen("test.txt", "w");
char *buf = malloc(20);
CleanArgs args = {fp, buf};
pthread_cleanup_push(clean, &args); // 注册清理函数
fputs("hello thread", fp); // 业务逻辑
strcpy(buf, "test");
pthread_cleanup_pop(1); // 执行清理函数(1 表示触发)
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, th, NULL);
pthread_join(tid, NULL);
return 0;
}
三、线程参数与返回值传递
1. 基本参数传递(整数、字符串)
传递整数
c
void *th(void *arg) {
int num = *(int*)arg; // 转换为 int* 并解引用
printf("线程接收的整数:%d\n", num);
return NULL;
}
int main() {
pthread_t tid;
int num = 100;
pthread_create(&tid, NULL, th, &num); // 传递整数地址
pthread_join(tid, NULL);
return 0;
}
传递字符串
字符串类型 | 特点 | 推荐度 |
---|---|---|
栈区字符数组 | 线程结束后内存释放,易悬空 | ❌ 不推荐 |
字符串常量("abc" ) |
全局有效但不可修改 | ⚠️ 谨慎使用 |
堆区字符串(malloc ) |
可修改、生命周期可控 | ✅ 推荐 |
示例(堆区字符串):
c
void *th(void *arg) {
char *str = (char*)arg;
strcat(str, " world"); // 修改堆区字符串
return str;
}
int main() {
pthread_t tid;
char *str = malloc(20);
strcpy(str, "hello");
pthread_create(&tid, NULL, th, str); // 传递堆区字符串地址
void *ret = NULL;
pthread_join(tid, &ret);
printf("线程返回的字符串:%s\n", (char*)ret); // 输出 "hello world"
free(ret); // 释放堆内存
return 0;
}
2. 结构体参数传递(复杂数据)
示例代码
c
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
// 定义结构体:存储个人信息
typedef struct {
char name[50]; // 姓名
int age; // 年龄
char addr[100]; // 地址
} PER;
// 线程函数:给结构体赋值
void *th(void *arg) {
PER *per = (PER*)arg; // 转换为结构体指针
// 读取姓名(去除换行符)
printf("输入姓名:");
fgets(per->name, sizeof(per->name), stdin);
per->name[strlen(per->name)-1] = '\0';
// 读取年龄(字符串转整数)
printf("输入年龄:");
char tmp[5] = {0};
fgets(tmp, sizeof(tmp), stdin);
per->age = atoi(tmp);
// 读取地址
printf("输入地址:");
fgets(per->addr, sizeof(per->addr), stdin);
return per; // 返回结构体指针
}
int main() {
PER per; // 主线程栈上创建结构体
pthread_t tid;
pthread_create(&tid, NULL, th, &per); // 传递结构体地址
void *ret = NULL;
pthread_join(tid, &ret);
// 打印结构体数据
printf("\n姓名:%s\n年龄:%d\n地址:%s\n",
((PER*)ret)->name,
((PER*)ret)->age,
((PER*)ret)->addr);
return 0;
}
关键说明
- 结构体存储在主线程栈上:线程仅修改结构体内容,不 ownership,无需释放。
- 若结构体在堆上(
malloc
):主线程需在回收线程后free
结构体。
四、线程与进程函数对比
进程操作函数 | 对应的线程操作函数 | 功能说明 |
---|---|---|
fork() |
pthread_create() |
创建新执行单元 |
getpid() /getppid() |
pthread_self() |
获取自身 ID |
exit() |
pthread_exit() |
终止自身 |
wait() /waitpid() |
pthread_join() |
等待并回收执行单元资源 |
kill() |
pthread_cancel() |
强制终止指定执行单元 |
atexit() |
pthread_cleanup_push() |
注册退出时的清理函数 |
exec() 系列 |
无(线程共享地址空间,不支持) | 替换进程地址空间(线程不适用) |
五、线程核心要点回顾
- 本质:轻量级进程,共享进程资源,仅私有栈和执行上下文。
- 核心优势:资源开销小、数据共享便捷,适合并发子任务。
- 关键函数 :
- 创建:
pthread_create
- 回收:
pthread_join
- 退出:
pthread_exit
/pthread_cancel
- 分离:
pthread_detach
- 创建:
- 内存安全 :
- 参数 / 返回值需指向有效内存(堆 / 全局 / 静态变量),避免局部变量。
- 共享数据需同步(后续需学习互斥锁、条件变量)。
- 调试技巧 :GDB 中用
info thread
查看线程列表,thread <ID>
切换线程。