一、基础概念区分(进程、线程、程序)
1. 核心定义
| 概念 |
本质说明 |
关键特性 |
| 程序 |
存储在磁盘上的代码指令集合(静态文件,如.c源文件、.exe可执行文件) |
无执行状态,不占用系统资源(CPU、内存等),仅为静态存储载体 |
| 进程 |
正在运行的程序实例(动态实体),操作系统进行资源分配和调度的基本单位 |
1. 拥有独立的内存空间、文件描述符等系统资源;2. 进程间相互独立,通信需依赖 IPC 机制;3. 开销大,创建 / 销毁 / 切换成本高 |
| 线程 |
进程内部的一条执行路径(轻量级进程),CPU 调度和执行的基本单位 |
1. 共享所属进程的资源(内存、文件描述符等);2. 线程间切换开销小,创建效率高;3. 一个进程至少包含一个主线程(main 函数执行路径) |
| 主线程 |
程序启动后默认创建的线程,对应main()函数的执行流程 |
1. 是进程的入口线程,负责初始化进程资源;2. 主线程退出会导致整个进程终止(除非设置分离线程);3. 可通过pthread库创建其他子线程(副线程) |
2. 形象类比
- 进程 = 工厂(提供生产环境、厂房、设备等资源)
- 线程 = 工厂内的工人(真正执行生产任务,共享工厂资源)
- 主线程 = 工厂厂长(负责统筹启动,默认最先运行)
- 程序 = 生产手册(静态文档,指导工人如何操作)
二、进程间通信(IPC)方式汇总
| 通信方式 |
核心特性 |
适用场景 |
| 管道 |
1. 无名管道:仅支持父子 / 兄弟进程,内核级无文件实体;2. 有名管道:支持任意进程,以文件形式存在于文件系统;3. 半双工通信,数据先进先出 |
简单的字节流传输,如父子进程间批量数据传递 |
| 信号量 |
1. 特殊非负整数,仅支持 P 操作(减 1,获取资源)和 V 操作(加 1,释放资源);2. 原子操作,用于解决临界资源竞争;3. 不存储数据,仅做同步互斥控制 |
多进程 / 线程间的同步互斥,如共享资源访问控制 |
| 共享内存 |
1. 多个进程映射到同一块物理内存,直接读写无需数据拷贝,速度最快;2. 本身无同步机制,需配合信号量 / 互斥锁使用;3. 临界资源,存在数据一致性问题 |
大量数据高速传输,如大数据量共享场景 |
| 消息队列 |
1. 内核中的消息链表,以结构体形式存储带类型的消息;2. 支持按类型读取消息,无需随进程持续运行;3. 数据独立于进程,进程退出消息仍保留 |
需分类传输的数据,如多进程间指令 / 数据分发 |
| 套接字(Socket) |
1. 支持跨主机、跨进程通信,兼顾本地和网络 IPC;2. 全双工通信,支持 TCP/UDP 两种协议;3. 通用性强,底层封装了复杂的网络协议 |
网络通信(如客户端 - 服务器架构)、本地跨进程高可靠通信 |
三、线程核心知识点(基于 Linux pthread 库)
1. 线程基础特性
(1)线程创建与执行特性
- 线程创建后不一定立即执行,由系统线程调度器决定执行时机,与创建顺序无关;
- 线程调度依赖优先级、时间切片算法,可能出现 "先创建后执行" 的情况;
- 主线程与副线程(子线程)同步并发执行,互不阻塞(除非使用
pthread_join()等同步函数);
- 线程退出:
pthread_exit()终止当前线程(释放自身资源,不影响其他线程);主线程退出会导致整个进程终止。
(2)线程分类(按管理级别)
| 线程类型 |
创建 / 管理者 |
开销 |
核心特性 |
| 用户级线程 |
用户态库(如 pthread) |
小 |
1. 内核无感知,调度由用户库完成;2. 切换无需进入内核,效率高;3. 无法利用多处理器并行,一个线程阻塞会导致整个进程阻塞 |
| 内核级线程 |
操作系统内核 |
大 |
1. 内核直接管理,每个线程对应内核调度实体;2. 支持多处理器并行执行;3. 线程阻塞不会影响其他线程,Linux 系统默认使用该类型 |
| 混合级线程 |
用户库 + 内核 |
中等 |
1. 多个用户级线程映射到一个内核级线程;2. 兼顾用户级的高效和内核级的并行能力;3. 复杂场景下的最优选择 |
(3)并发与并行的区别
| 概念 |
本质说明 |
依赖条件 |
| 并发 |
多个任务交替执行(宏观上同时进行,微观上串行切换) |
无需多处理器,单处理器通过时间切片实现(如单 CPU 同时运行多个软件) |
| 并行 |
多个任务同时执行(宏观 + 微观均为同时进行) |
必须依赖多处理器 / 多核 CPU,是 "特殊的并发" |
| 核心关联 |
1. 并行是并发的子集,所有并行都是并发,但并发不一定是并行;2. 单处理器仅支持并发,多处理器可同时支持并发与并行 |
- |
2. 线程核心函数(pthread 库)
(1)头文件与编译要求
- 头文件:
#include <pthread.h>(线程函数、锁、读写锁均包含在此头文件)
- 编译命令:必须添加
-lpthread参数(指定链接 pthread 线程库),示例:
bash
复制代码
gcc thread_demo.c -o thread_demo -lpthread
(2)核心函数说明与示例
1. 线程创建:pthread_create()
- 函数原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
- 参数说明:
thread:输出参数,存储创建后的线程 ID;
attr:线程属性(通常设为 NULL,使用默认属性);
start_routine:线程函数指针(返回值void*,参数void*);
arg:传递给线程函数的参数(无参数时设为 NULL);
- 返回值:成功返回 0,失败返回非 0 错误码。
2. 线程等待:pthread_join()
- 函数原型:
int pthread_join(pthread_t thread, void **retval);
- 功能:阻塞主线程,等待指定副线程执行完毕,可接收副线程的返回值;
- 参数说明:
thread:要等待的线程 ID;
retval:二级指针,存储线程退出时的返回值(pthread_exit()的参数);
- 返回值:成功返回 0,失败返回非 0 错误码。
3. 线程退出:pthread_exit()
- 函数原型:
void pthread_exit(void *retval);
- 功能:终止当前线程,释放线程资源,返回指定状态码,不影响其他线程;
- 参数:
retval:线程退出状态指针,可通过pthread_join()获取。
4. 基础线程示例(创建 + 等待 + 退出)
cpp
复制代码
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
// 线程函数(副线程执行逻辑)
void* thread_fun(void* arg) {
int num = *(int*)arg; // 接收传递的参数
printf("副线程:ID=%lu,接收参数=%d,开始执行\n", (unsigned long)pthread_self(), num);
sleep(2); // 模拟业务耗时
int* ret = malloc(sizeof(int));
*ret = num * 2; // 线程返回结果
pthread_exit((void*)ret); // 终止线程,返回结果
}
int main() {
// 主线程:默认执行路径
printf("主线程:ID=%lu,开始运行\n", (unsigned long)pthread_self());
pthread_t tid1, tid2; // 线程ID
int arg1 = 10, arg2 = 20; // 传递给线程的参数
// 创建两个副线程
if (pthread_create(&tid1, NULL, thread_fun, &arg1) != 0) {
perror("创建线程1失败");
return 1;
}
if (pthread_create(&tid2, NULL, thread_fun, &arg2) != 0) {
perror("创建线程2失败");
return 1;
}
// 等待副线程执行完毕,获取返回值
void* ret1 = NULL;
void* ret2 = NULL;
pthread_join(tid1, &ret1);
pthread_join(tid2, &ret2);
printf("主线程:线程1返回结果=%d,线程2返回结果=%d\n", *(int*)ret1, *(int*)ret2);
// 释放线程返回值的内存
free(ret1);
free(ret2);
printf("主线程:执行完毕,退出\n");
return 0;
}
编译运行:
bash
复制代码
gcc thread_basic.c -o thread_basic -lpthread
./thread_basic
- 运行结果:主线程创建两个副线程,等待其执行完毕后获取返回值,最终退出。
3. 线程安全问题
(1)核心概念
- 线程安全:多线程环境下,函数 / 代码段能保证执行结果的正确性,不会出现数据竞争、值被覆盖等问题;
- 线程不安全原因:
- 共享资源未加保护,多个线程同时读写;
- 非原子操作(如
i++,分为 "读取 - 修改 - 写入" 三步,中间可能被其他线程打断);
- 函数内部使用静态变量 / 全局变量(如
strtok()的光标指针,会被多个线程覆盖)。
(2)线程不安全示例(i++非原子操作)
cpp
复制代码
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
int g_count = 0; // 全局共享变量(临界资源)
// 线程函数:对全局变量执行10000次i++
void* count_fun(void* arg) {
for (int i = 0; i < 10000; i++) {
g_count++; // 非原子操作,存在数据竞争
}
pthread_exit(NULL);
}
int main() {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, count_fun, NULL);
pthread_create(&tid2, NULL, count_fun, NULL);
// 等待两个线程执行完毕
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
// 预期结果:20000,实际结果大概率小于20000(数据竞争导致)
printf("最终计数结果:%d\n", g_count);
return 0;
}
(3)线程安全解决方案
- 使用同步互斥机制(信号量、互斥锁等)保护临界资源;
- 使用线程安全版本的函数(如
strtok_r()替代strtok())。
线程安全函数示例(strtok_r()字符串分割)
cpp
复制代码
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
// 线程函数:使用strtok_r进行字符串分割(线程安全)
void* str_split_fun(void* arg) {
char* str = (char*)arg;
char* saveptr; // 存储分割上下文(每个线程独立拥有,避免覆盖)
char* token = strtok_r(str, " ", &saveptr);
printf("线程%lu:分割结果:\n", (unsigned long)pthread_self());
while (token != NULL) {
printf(" %s\n", token);
token = strtok_r(NULL, " ", &saveptr);
}
pthread_exit(NULL);
}
int main() {
char str1[] = "Hello Thread Safe Function";
char str2[] = "pthread strtok_r is secure";
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, str_split_fun, str1);
pthread_create(&tid2, NULL, str_split_fun, str2);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
四、同步互斥机制(解决临界资源竞争)
1. 信号量(线程 / 进程通用)
(1)核心概念
- 本质 :特殊非负整数,仅支持 P 操作(
sem_wait(),减 1,获取资源,可能阻塞)和 V 操作(sem_post(),加 1,释放资源,唤醒阻塞线程);
- 原子操作:P/V 操作不可被打断,保证多线程 / 进程下操作的唯一性;
- 线程信号量头文件 :
#include <semaphore.h>(进程信号量为sys/sem.h);
- 核心特性:信号量不与特定线程 / 进程绑定,仅通过值判断资源是否可用,值为 1 时允许访问,值为 0 时阻塞(系统自动休眠,无需手动编写阻塞代码)。
(2)线程信号量核心函数
| 函数名 |
功能说明 |
sem_init() |
初始化线程信号量,原型:int sem_init(sem_t *sem, int pshared, unsigned int value);(pshared=0表示线程信号量,value为初始值) |
sem_wait() |
P 操作,信号量减 1,若值 < 0 则阻塞,原型:int sem_wait(sem_t *sem); |
sem_post() |
V 操作,信号量加 1,若值≤0 则唤醒阻塞线程,原型:int sem_post(sem_t *sem); |
sem_destroy() |
销毁信号量,释放资源,原型:int sem_destroy(sem_t *sem); |
(3)线程信号量示例(解决i++线程安全问题)
cpp
复制代码
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <unistd.h>
int g_count = 0;
sem_t sem; // 线程信号量
// 线程函数:加锁后执行i++
void* count_fun(void* arg) {
for (int i = 0; i < 10000; i++) {
sem_wait(&sem); // P操作:获取资源,进入临界区
g_count++; // 临界区操作(保护共享变量)
sem_post(&sem); // V操作:释放资源,退出临界区
}
pthread_exit(NULL);
}
int main() {
// 初始化信号量:初始值1(互斥访问),线程私有
sem_init(&sem, 0, 1);
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, count_fun, NULL);
pthread_create(&tid2, NULL, count_fun, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("最终计数结果:%d\n", g_count); // 预期结果:20000
// 销毁信号量
sem_destroy(&sem);
return 0;
}
编译运行:
(4)形象类比:景点检票
- 线程 / 进程 = 游客;
- 信号量值 = 门票数量(1 张表示仅允许 1 人进入,0 张表示无票);
sem_wait() = 检票员:检查门票(信号量值),有票则撕票(减 1)允许进入,无票则让游客排队阻塞;
sem_post() = 游客离开景区:归还门票(加 1),唤醒排队的游客重新检票。
2. 互斥锁(线程专用,高效互斥)
(1)核心概念
- 本质:实现 "独占式访问",只有持有锁的线程才能访问临界资源,其他线程需等待解锁;
- 特性:轻量级开销小,仅支持 "加锁" 和 "解锁" 操作,适用于短时间临界资源访问;
- 核心函数 :均定义在
pthread.h中。
(2)互斥锁核心函数
| 函数名 |
功能说明 |
pthread_mutex_init() |
初始化互斥锁,原型:int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);(attr=NULL使用默认属性) |
pthread_mutex_lock() |
加锁:若锁未被占用则获取锁,若已被占用则阻塞等待 |
pthread_mutex_unlock() |
解锁:释放锁,唤醒等待该锁的线程 |
pthread_mutex_destroy() |
销毁互斥锁,释放资源 |
(3)互斥锁示例(保护共享变量)
cpp
复制代码
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
int g_count = 0;
pthread_mutex_t mutex; // 互斥锁
void* count_fun(void* arg) {
for (int i = 0; i < 10000; i++) {
pthread_mutex_lock(&mutex); // 加锁:进入临界区
g_count++;
pthread_mutex_unlock(&mutex); // 解锁:退出临界区
}
pthread_exit(NULL);
}
int main() {
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, count_fun, NULL);
pthread_create(&tid2, NULL, count_fun, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("最终计数结果:%d\n", g_count); // 正确结果:20000
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
3. 读写锁(线程专用,区分读写场景)
(1)核心概念
- 特性:允许多个线程同时读操作,写操作需独占(读 - 读并行,读 - 写互斥,写 - 写互斥);
- 适用场景:读操作远多于写操作的场景(如配置文件读取、数据查询),比互斥锁效率更高。
(2)读写锁核心函数
| 函数名 |
功能说明 |
pthread_rwlock_init() |
初始化读写锁,原型:int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); |
pthread_rwlock_rdlock() |
加读锁:无写锁时可获取,允许多个线程同时持有读锁 |
pthread_rwlock_wrlock() |
加写锁:无读锁 / 写锁时可获取,独占资源 |
pthread_rwlock_unlock() |
解锁:释放读锁或写锁,唤醒等待线程 |
pthread_rwlock_destroy() |
销毁读写锁,释放资源 |
cpp
复制代码
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
int g_data = 100; // 共享数据
pthread_rwlock_t rwlock; // 读写锁
// 读线程函数:多次读取共享数据
void* read_fun(void* arg) {
int id = *(int*)arg;
for (int i = 0; i < 5; i++) {
pthread_rwlock_rdlock(&rwlock); // 加读锁
printf("读线程%d:读取g_data=%d\n", id, g_data);
sleep(1); // 模拟读耗时
pthread_rwlock_unlock(&rwlock); // 解读锁
sleep(1);
}
pthread_exit(NULL);
}
// 写线程函数:修改共享数据
void* write_fun(void* arg) {
for (int i = 0; i < 2; i++) {
pthread_rwlock_wrlock(&rwlock); // 加写锁
g_data += 50;
printf("【写线程】:修改g_data=%d\n", g_data);
sleep(2); // 模拟写耗时
pthread_rwlock_unlock(&rwlock); // 解写锁
sleep(1);
}
pthread_exit(NULL);
}
int main() {
pthread_rwlock_init(&rwlock, NULL);
pthread_t tid_read1, tid_read2, tid_write;
int id1 = 1, id2 = 2;
// 创建2个读线程,1个写线程
pthread_create(&tid_read1, NULL, read_fun, &id1);
pthread_create(&tid_read2, NULL, read_fun, &id2);
pthread_create(&tid_write, NULL, write_fun, NULL);
// 等待所有线程执行完毕
pthread_join(tid_read1, NULL);
pthread_join(tid_read2, NULL);
pthread_join(tid_write, NULL);
// 销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
- 运行结果:两个读线程可同时读取数据,写线程执行时会阻塞所有读 / 写线程,体现 "读 - 读并行,读 - 写互斥" 特性。
4. 条件变量(线程专用,实现线程间同步)
(1)核心概念
- 本质:用于线程间的 "条件等待",当条件不满足时,线程阻塞等待;当条件满足时,被其他线程唤醒继续执行;
- 搭配使用:必须与互斥锁配合(防止条件判断与阻塞操作之间被打断)。
(2)条件变量核心函数
| 函数名 |
功能说明 |
pthread_cond_init() |
初始化条件变量 |
pthread_cond_wait() |
阻塞线程,等待条件满足,同时释放互斥锁(原子操作) |
pthread_cond_signal() |
唤醒一个等待该条件变量的线程 |
pthread_cond_broadcast() |
唤醒所有等待该条件变量的线程 |
pthread_cond_destroy() |
销毁条件变量 |
五、进程与线程的特殊场景
1. fork()与线程的关系
fork()创建子进程时,子进程仅复制父进程中调用fork()的线程(通常是主线程),其他副线程不会被复制;
- 子进程会复制父进程的锁(互斥锁、读写锁)及其状态,但父子进程的锁相互独立,各自管理所属进程的资源;
- 若父进程在持有锁时调用
fork(),子进程会继承锁的 "已锁定" 状态,但无法解锁(因为锁的持有者是父进程的线程),可能导致死锁。
2. 线程调度的资源类型
线程调度器分配的核心资源包括:
- CPU 时间片(决定线程执行的先后顺序和时长);
- 内存空间(共享进程内存,无需单独分配);
- 系统设备(如打印机、显卡,通过进程文件描述符访问);
- 网络带宽(通过进程的套接字资源共享);
- 信号量与锁(同步互斥工具,控制临界资源访问);
- 文件描述符(共享进程的文件、管道等资源句柄)。
3. 线程同步的核心原则
- 临界区代码尽可能简短,将非临界区代码(如休眠、无关计算)放在同步锁之外,提高并发效率;
- 避免嵌套锁(防止死锁);
- 优先使用轻量级同步机制(互斥锁 > 信号量);
- 保证共享资源的访问唯一性,所有线程必须通过同步机制访问临界资源。
六、补充说明
1. 编译与运行通用要求
| 功能场景 |
头文件 |
编译参数 |
运行方式 |
| 基础线程操作 |
pthread.h |
-lpthread |
直接运行可执行文件,如./thread_demo |
| 线程信号量 |
semaphore.h、pthread.h |
-lpthread |
同上 |
| 互斥锁 / 读写锁 |
pthread.h |
-lpthread |
同上 |
| 进程信号量 |
sys/sem.h |
无需额外参数 |
同上 |
| 共享内存 |
sys/shm.h |
无需额外参数 |
多进程需分别运行,如./shm_write和./shm_read |
2. 关键注意事项
- 主线程应最后退出,避免提前终止导致副线程被强制销毁;
- 动态分配的线程返回值需手动释放(防止内存泄漏);
- 线程退出优先使用
pthread_exit(),而非exit()(exit()会终止整个进程);
- 同步机制使用完毕后必须销毁(如
sem_destroy()、pthread_mutex_destroy()),释放系统资源。