一、概念
**进程:**一个动态的概念,本质是一个程序正在执行的序列,是程序运行的载体。
线程:隶属于进程,是进程内部的一条执行路径,也是进程内部的一个执行序列。一个进程可以包含多个线程,多个线程共享进程的资源,协同完成进程的任务。
Linux中线程的实现
与其他操作系统不同,在内核的角度来讲,Linux 系统中并没有专门的"线程"概念。
Linux 内核将所有的线程都当作进程来实现和管理,既没有为线程设计专门的调度算法,也没有定义专门表征线程的数据结构。
在 Linux 内核中,线程仅仅被视为一个与其他进程共享某些资源的特殊进程。每个进程(包括被当作线程的进程)都拥有唯一隶属于自己的 task_struct 结构体(进程控制块),因此从内核视角来看,线程和普通进程没有本质区别,唯一的不同是:线程会与其他一些进程共享地址空间、打开的文件等资源,而普通进程拥有独立的资源空间。
进程和线程的区别
- 本质区别:进程是程序正在执行的过程,是动态概念;线程是进程内部的一条执行路径或执行序列,是进程的组成部分。
- 资源分配:进程是资源分配的最小单位,拥有独立的地址空间和系统资源;线程是 CPU 调度的最小单位,不拥有独立的资源,共享所属进程的地址空间和资源。
- 开销差异:进程的创建、切换开销较大(需分配独立资源、切换进程控制块);线程的创建、切换开销相对较小(无需分配新资源,仅切换线程执行上下文)。
二、线程的接口相关函数
线程的操作依赖于 pthread 库提供的接口,以下是最常用的三个核心接口,用于线程的创建、等待和退出。
pthread_create
创建一个新的线程,并指定线程的入口函数,新线程创建成功后会立即执行入口函数。
cpp
int pthread_create(
pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg
);
- thread:输出型参数,用于接收创建成功后的线程 ID,后续操作(如等待线程退出)需通过该 ID 定位线程。
- attr:用于设置线程的属性(如线程优先级、栈大小等),一般无需特殊设置,直接传 NULL 即可(使用默认属性)。
- start_routine:线程入口函数的指针,该函数的返回值和参数均为 void* 类型,是新线程启动后执行的核心逻辑。
- arg:传递给线程入口函数的参数,若无需传递参数,可传 NULL;若传递多个参数,需封装为结构体指针传入。
- 返回值:函数执行成功返回 0;失败返回对应的错误码(注意:不是 errno,需通过错误码判断失败原因)。
pthread_join
阻塞等待指定的线程退出,直到该线程执行完毕后,当前线程(通常是主线程)才会继续执行,同时可以接收线程退出时的返回信息。
cpp
int pthread_join(pthread_t thread, void **retval);
- thread: 要等待的线程的 ID,即 pthread_create 函数返回的线程 ID
- retval:输出型参数,用于接收线程退出时通过 pthread_exit 传递的退出信息(本质是一个 void* 类型的指针)。
pthread_exit
用于在子线程内部主动退出线程,同时可以指定退出信息,该信息可被 pthread_join 函数接收。
cpp
int pthread_exit(void *retval);
- retval:用于指定线程的退出信息,传递给 pthread_join 函数的 retval 参数。
- 注意:不能返回临时变量的地址。因为临时变量存储在栈上,线程退出后,栈空间会被释放,临时变量的地址会变成无效地址,若通过 retval 接收该地址,会导致程序异常。
三、使用示例
核心注意点
- 如果主进程不使用 pthread_join 等待子线程退出,可能会出现"线程函数还未执行完毕,主进程就已经退出"的情况。主进程退出后,其所属的所有子线程会被强制终止,导致线程逻辑无法完整执行。
- pthread_exit 的作用:用于线程自身主动退出,并传递退出信息,通常在子线程中使用。
- pthread_join 的额外作用:除了等待线程退出、接收退出信息外,还会释放子线程占用的系统资源,避免资源泄漏。
- 主线程并非必须调用 pthread_join:只要主线程不提前退出(如通过循环、阻塞等方式保持运行),子线程可以正常执行完毕,此时无需调用 pthread_join。
示例代码
cpp
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
// 线程入口函数
void * thread_fun(void *arg)
{
for(int i=0;i<10;i++)
{
printf("fun run\n");
sleep(1);
}
pthread_exit("fun over\n");// 子线程退出,并传递退出信息
}
int main()
{
pthread_t id;// 存储线程ID
// 创建子线程:无属性、无参数
pthread_create(&id,NULL,thread_fun,NULL);
for(int i=0;i<5;i++)
{
printf("main run\n");
sleep(1);
}
char *s=NULL;
pthread_join(id,(void **)&s);// 等到子线程退出,并接收退出信息
printf("join:s=%s\n",s);// 打印退出信息
exit(0);
}

如果不使用pthread_join的运行结果

若删除 main 函数中的 pthread_join 语句,主线程会在循环 5 次(5 秒)后直接退出。此时子线程还在执行(需执行 10 次,共 10 秒),但由于主进程退出,子线程会被强制终止,无法完成后续的打印操作,最终只能看到 5 次"main run"和 5 次左右的"fun run"(具体次数取决于线程调度),无法看到"fun over"的输出。
四、线程同步
相关概念
a. 同步概念
同一个进程中的所有线程,共享该进程的地址空间以及进程拥有的其他资源 (如打开的文件、全局变量等)。那么,一个线程对资源的修改会影响其他线程的运行环境,因此需要通过**线程同步,**对多个线程的执行顺序进行控制,让线程按照预定的规则执行,从而保证线程运行的安全性和效率。
同步:让线程按规则执行,保证安全与效率。
**线程同步:**一个线程在操作临界资源时,其他线程必须等待,直到操作完成。
b. 关键名词
临界资源:同一时刻,只允许被一个进程或一个线程访问的资源。例如打印机、全局变量等
临界区:程序中访问临界资源的那段代码段。线程同步的核心就是保护临界区,确保同一时刻只有一个线程进入临界区执行操作。
c. 线程同步方法
为了实现线程同步,Linux 提供了多种机制,最常用的四种方法如下:
- 互斥锁:最基础、最常用的同步方式,保证同一时刻只有一个线程进入临界区。
- 信号量:用于控制资源的可用数量,可实现多线程对有限资源的有序访问。
- 条件变量:用于实现线程等待某个条件成立,避免线程频繁检查条件而浪费 CPU 资源。
- 读写锁:区分读操作和写操作,适用于"读多写少"的场景,提高并发效率。
1. 互斥锁
a. 核心接口
互斥锁的使用遵循"初始化→加锁→解锁→销毁"的流程,核心接口如下:
pthread_mutex_init
初始化锁
cpp
int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr);
- mutex:执行互斥锁变量的指针
- attr:用于设置互斥锁的属性,不需要特殊设置,传空即可
pthread_mutex_lock
加锁:尝试获取互斥锁,若锁未被占用,则当前线程获取锁并进入临界区;若锁已被其他线程占用,则当前线程阻塞,直到锁被释放。
cpp
int pthread_mutex_lock(pthread_mutex_t *mutex);
pthread_mutex_unlock
解锁:当前线程释放互斥锁,若有其他线程因获取该锁而阻塞,会唤醒其中一个线程获取锁。
cpp
int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_destroy
销毁锁:释放互斥锁占用的系统资源,需在所有使用该互斥锁的线程全部结束后调用。
cpp
int pthread_mutex_destroy(pthread_mutex_t *mutex);
注意:互斥锁的所有接口中,mutex 参数都需要传递地址,因为接口内部会修改互斥锁的状态(如锁的占用情况),传递地址才能实现状态的修改。
b. 使用示例
i. 解决多线程并发计数问题
场景:5 个线程同时对全局变量 wg 进行 1000 次自增操作,若不加锁,由于 wg++ 不是原子操作(本质是"读取-修改-写入"三步操作),多线程会相互干扰,导致最终结果小于 5000;加锁后,可保证每次自增操作的原子性,最终结果为 5000(正确值)。
不加锁代码
cpp
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
int wg=0;
void* func(void* arg){
for(int i=0;i<1000;i++){
wg++;
}
}
int main(){
pthread_t id[5] = {0};
for(int i = 0; i < 5; i++){
int res = pthread_create(&id[i], NULL, func, NULL);
if(res != 0){
printf("pthread create failed\n");
return -1;
}
}
for(int i=0;i<5;i++){
pthread_join(id[i], NULL);
}
printf("wg = %d\n", wg);
return 0;
}
运行结果:多次运行,会发现有些结果并不是5000。这是因为多个线程同时执行 wg++ 时,会出现"多个线程读取到同一个 wg 值,同时自增,最终只实现一次有效自增"的情况,导致数据混乱。

加锁方式 1:锁整个循环
效率较低,一次只允许一个线程执行全部 1000 次自增
cpp
pthread_mutex_lock(&mutex);
for(int i=0;i<1000;i++){
wg++;
}
pthread_mutex_unlock(&mutex);
加锁方式 2:锁单次操作
效率较高,多个线程可交替执行自增,仅保证单次自增原子性
cpp
for(int i=0;i<1000;i++){
pthread_mutex_lock(&mutex);
wg++;
pthread_mutex_unlock(&mutex);
}
加锁原理:加锁后,无论哪种方式,都能将 wg++ 操作变成原子操作,即同一时刻只有一个线程能执行 wg++,避免了线程之间的干扰,最终保证 wg 的值为 5000。
ii. 模拟共享打印机(互斥访问)
场景:主线程和子线程模拟访问同一台打印机,主线程输出两个字符'A'(分别表示开始使用和结束使用打印机),子线程输出两个字符'B'(逻辑与主线程一致)。由于打印机是临界资源,同一时刻只能被一个线程使用,因此输出结果不能出现 ABAB 交替的情况(即不能出现"一个线程未结束使用,另一个线程就开始使用")。
通过互斥锁,可保证同一时刻只有一个线程访问打印机(临界资源),实现互斥访问。
cpp
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
// 打印机模拟
pthread_mutex_t mutex;
void* func(void* arg){
for(int i = 0; i < 5; i++){
pthread_mutex_lock(&mutex);// 加锁
printf("B\n");
sleep(2);// 模拟打印机
printf("B\n");
pthread_mutex_unlock(&mutex);// 解锁
sleep(1);
}
}
int main(){
// 需要在创建线程之前进行初始化
pthread_mutex_init(&mutex, NULL);// 初始化锁
pthread_t id;
int res = pthread_create(&id, NULL, func, NULL);
if(res != 0){
printf("pthread create failed\n");
return -1;
}
for(int i = 0; i < 5; i++){
pthread_mutex_lock(&mutex);// 加锁
printf("A\n");
sleep(2);// 模拟打印机
printf("A\n");
pthread_mutex_unlock(&mutex);// 解锁
sleep(1);
}
pthread_join(id, NULL);// 等待线程结束
pthread_mutex_destroy(&mutex);// 没有线程在使用锁,才能删除
return 0;
}
2.信号量
信号量通过控制资源的可用数量,实现多个线程对临界资源的有序访问。其核心类型为 sem_t,通常定义为全局变量(方便多线程共享访问),使用前需包含指定头文件。
a.核心接口
头文件
cpp
#include<semaphore.h>
sem_init
用于初始化信号量的初始值和共享属性,是使用信号量的第一步。
cpp
int sem_init(sem_t *sem, int pshared, unsigned int value);
- sem:指向信号量变量的指针,需传入地址(因要修改信号量状态)。
- pshared:设置信号量是否在进程间共享,Linux 系统不支持进程间共享信号量,因此该参数固定传
0(非 0 表示进程间共享,无效) - value:信号量的初始值,代表可用资源的数量(核心参数)。例如 value=1 时,等价于互斥锁,实现线程互斥;value > 1 时,可实现多线程并发访问有限资源。
sem_wait
P操作,等待获取资源
- 若信号量值大于 0,将其减 1(表示占用一个资源),函数立即返回;
- 若信号量值为 0,函数阻塞,直到有其他线程释放资源(执行 V 操作)。
cpp
int sem_wait(sem_t *sem);
sem_post
V操作,释放一个资源,将信号量值加 1,若有线程因调用 sem_wait 阻塞,会唤醒其中一个线程获取资源。
cpp
int sem_post(sem_t *sem);
sem_destroy
销毁信号量,用于释放信号量占用的系统资源,必须在所有使用该信号量的子线程全部结束后调用,否则会导致资源泄漏或程序异常。
cpp
int sem_destroy(sem_t *sem);
b.使用示例
i. 解决多线程并发计数问题
场景:5个线程同时对全局变量 wg 进行 1000 次自增操作,通过信号量保证自增操作的原子性,避免数据混乱(与互斥锁功能类似,此处信号量初始值为 1,等价于互斥锁)。
cpp
#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>
sem_t sem;// 全局变量
int wg=0;
void* func(void* arg){
for(int i=0;i<1000;i++){
sem_wait(&sem);// P操作
wg++;
sem_post(&sem);// V操作
}
}
int main(){
sem_init(&sem, 0, 1);// 初始化
pthread_t id[5] = {0};
for(int i = 0; i < 5; i++){
int res = pthread_create(&id[i], NULL, func, NULL);
if(res != 0){
printf("pthread create failed\n");
return -1;
}
}
for(int i=0;i<5;i++){
pthread_join(id[i], NULL);
}
printf("wg = %d\n", wg);
sem_destroy(&sem);// 销毁信号量
return 0;
}
ii. 实现读写线程同步
场景:主线程从键盘获取用户输入,子线程将输入内容写入文件,通过两个信号量控制读写顺序,保证"先读(输入)、后写(写入文件)",避免读写混乱。
核心逻辑:sem1 控制主线程输入(初始值1,允许先输入),sem2 控制子线程写入(初始值0,等待输入完成后再写入),形成"输入→写入"的同步流程。
cpp
#include<stdio.h>
#include<string.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<pthread.h>
#include<semaphore.h>
// 2.主线程获取用户输入,函数线程将用户输入的数据存储到文件中
// 需要两个信号量:保证读写的同步
sem_t sem1;
sem_t sem2;
char buff[128] = {0};
void* func(void* arg){
int fd = open("a.txt", O_RDWR | O_CREAT, 0644);// 打开文件
if(fd == -1){
printf("open failed\n");
return NULL;
}
while(1){
sem_wait(&sem2);
if(strncmp(buff, "end", 3) == 0){
break;
}
write(fd,buff,strlen(buff));// 将buff内容写入文件
memset(buff, 0 ,128);// 重置buff内容
sem_post(&sem1);
}
close(fd);// 关闭文件
}
int main(){
sem_init(&sem1, 0, 1);
sem_init(&sem2, 0, 0);
pthread_t id;
pthread_create(&id, NULL, func, NULL);
while(1){
sem_wait(&sem1);
printf("please input:");
fflush(stdout);// 刷新缓冲区
fgets(buff, 128, stdin);// 从标准输入获取
buff[strlen(buff)-1]='\0';// 主线程写入buff
sem_post(&sem2);
if(strncmp(buff, "end", 3) == 0){
break;
}
}
pthread_join(id, NULL);
sem_destroy(&sem1);
sem_destroy(&sem2);
return 0;
}
iii. 实现线程顺序打印ABC
场景:创建3个线程,分别打印A、B、C,要求严格按照ABC的顺序循环打印5次。结合表驱动思想(用函数数组替代条件判断),简化线程创建逻辑,通过3个信号量控制线程执行顺序。
表驱动
核心特点:替代大量 if/switch 判断,代码更简洁;数据与逻辑分离,新增功能只需修改函数数组,无需改动核心逻辑;通过索引快速定位函数,效率更高。
cpp
typedef void* (*Func)(void*);
Func funcArr[]={
func1,
func2,
func3
};
pthread_t id[3];
for(int i = 0; i < 3; i++){
int res = pthread_create(&id[i], NULL, funcArr[i], NULL);
assert(res != -1);
}
cpp
#include<stdio.h>
#include<string.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<assert.h>
#include<pthread.h>
#include<semaphore.h>
// 3.循环打印ABC(顺序必须是ABC)
sem_t sem1;
sem_t sem2;
sem_t sem3;
void* func1(void* arg){
int count = 5;
while(count--){
sem_wait(&sem1);
printf("A\n");
sleep(1);
sem_post(&sem2);
}
}
void* func2(void* arg){
int count = 5;
while(count--){
sem_wait(&sem2);
printf("B\n");
sleep(1);
sem_post(&sem3);
}
}
void* func3(void* arg){
int count = 5;
while(count--){
sem_wait(&sem3);
printf("C\n");
sleep(1);
sem_post(&sem1);
}
}
typedef void* (*Func)(void*);
Func funcArr[]={
func1,
func2,
func3
};
int main(){
sem_init(&sem1, 0, 1);
sem_init(&sem2, 0, 0);
sem_init(&sem3, 0, 0);
pthread_t id[3];
for(int i = 0; i < 3; i++){
int res = pthread_create(&id[i], NULL, funcArr[i], NULL);
assert(res != -1);
}
for(int i = 0; i < 3; i++){
pthread_join(id[i], NULL);
}
sem_destroy(&sem1);
sem_destroy(&sem2);
sem_destroy(&sem3);
return 0;
}
3.条件变量
用于实现"线程等待某个条件成立"的场景,通常与互斥锁配合使用,避免线程因频繁检查条件而浪费CPU资源(阻塞等待,条件成立时被唤醒)。其核心类型为 pthread_cond_t,需定义为全局变量,使用前包含指定头文件。
a.核心接口
cpp
#include<pthread.h>
pthread_cond_init
初始化条件变量
cpp
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr);
- cond:指向条件变量的指针,需传入地址
- attr:条件变量的属性,一般无需设置,传 NULL 即可(使用默认属性)。
pthread_cond_wait
阻塞等待条件成立
- 将当前线程加入条件变量的等待队列,同时自动释放互斥锁(避免死锁);
- 当条件成立被唤醒时,线程会重新获取互斥锁,然后继续执行。
cpp
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
- cond:指向条件变量的指针。
- mutex:互斥锁指针,必须与条件变量配合使用(核心:避免多线程同时检查条件的竞态问题)。
pthread_cond_signal
唤醒单个线程:从条件变量的等待队列中,唤醒一个阻塞的线程(随机唤醒一个),适用于"只要有一个线程处理即可"的场景。
cpp
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_broadcast
唤醒所有线程:唤醒条件变量等待队列中的所有阻塞线程,适用于"所有线程都需要处理条件"的场景(如程序退出时,唤醒所有等待线程)。
cpp
int pthread_cond_broadcast(pthread_cond_t *cond);
pthread_cond_destroy
销毁条件变量:用于释放条件变量占用的系统资源,需在所有使用该条件变量的线程全部结束后调用,且调用前需确保没有线程处于等待状态。
cpp
int pthread_cond_destroy(pthread_cond_t *cond);
b.使用示例
场景:主线程从键盘获取用户输入,将输入内容存入缓冲区 buff(条件成立);创建两个工作线程,阻塞等待条件成立,被唤醒后打印缓冲区内容;输入"end"时,唤醒所有工作线程,程序退出。
cpp
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<assert.h>
#include<pthread.h>
pthread_cond_t cond;
pthread_mutex_t mutex;
char buff[128] = {0};
void* work_thread(void* arg){
while(1){
char* thread_name = (char*)arg;// 线程名
pthread_mutex_lock(&mutex);// 加锁
pthread_cond_wait(&cond, &mutex);// 等待目标条件变量
pthread_mutex_unlock(&mutex);// 解锁
if(strncmp(buff, "end", 3) == 0){
break;
}
printf("%s: %s", thread_name, buff);
}
}
int main(){
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);
pthread_t id1,id2;
int res1 = pthread_create(&id1, NULL, work_thread, "thread1");
assert(res1 == 0);
int res2 = pthread_create(&id2, NULL, work_thread, "thread2");
assert(res2 == 0);
while(1){
fgets(buff, 127, stdin);
if(strncmp(buff, "end", 3)==0){
// 唤醒所有线程
pthread_cond_broadcast(&cond);
break;
}else{
// 唤醒一个线程
pthread_cond_signal(&cond);
}
}
pthread_join(id1, NULL);
pthread_join(id2, NULL);
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
return 0;
}

4.读写锁
读写锁是一种更灵活的同步机制,区分"读操作"和"写操作",适用于"读多写少"的场景(如数据查询多、修改少),能提高并发效率。其核心类型为 pthread_rwlock_t,使用前需包含指定头文件。
a.核心接口
头文件:#include <pthread.h>
pthread_rwlock_init
初始化读写锁:第一个参数是锁的地址,第二个参数是锁的属性,一般写NULL
cpp
int pthread_rwlock_init(pthread_rwlock_t *rwlock, pthread_rwlockattr_t *attr);
- rwlock:指向读写锁的指针,需传入地址。
- attr :读写锁的属性,一般无需设置,传
NULL即可(使用默认属性)。
pthread_rwlock_rdlock
加读锁:为读操作加锁,多个线程可同时加读锁(读操作并发执行);若当前有线程加了写锁,则读锁会阻塞,直到写锁释放。
cpp
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
pthread_rwlock_wrlock
加写锁:为写操作加锁,同一时刻只能有一个线程加写锁(写操作互斥);若当前有线程加了读锁或写锁,则写锁会阻塞,直到所有锁释放。
cpp
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
pthread_rwlock_unlock
解锁:无论加的是读锁还是写锁,都通过该函数解锁
cpp
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
pthread_relock_destroy
销毁:用于释放读写锁占用的系统资源,需在所有使用该读写锁的线程全部结束后调用,且调用前需确保读写锁处于未加锁状态(避免资源泄漏)。
cpp
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)
b.使用示例(读多写少场景)
场景:创建3个线程,2个线程执行读操作(并发),1个线程执行写操作(互斥),模拟"读多写少"场景,验证读写锁的特性:多个读线程可同时执行,读写线程互斥,写线程执行时读线程阻塞。
cpp
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<pthread.h>
pthread_rwlock_t rwlock;// 读写锁变量
void* w_thread(void* arg){
// 写进程
for(int i=0;i<5;i++){
char* s = (char*)arg;
pthread_rwlock_wrlock(&rwlock);// 加写锁
printf("%s: start\n",s);
int n=rand()%3;
sleep(n);
printf("%s: end\n",s);
pthread_rwlock_unlock(&rwlock);// 解锁
n=rand()%3;
sleep(n);
}
}
void* r_thread(void* arg){
for(int i=0;i<5;i++){
char* s = (char*)arg;
pthread_rwlock_rdlock(&rwlock);// 加读锁
printf("%s: start\n",s);
int n=rand()%3;
sleep(n);
printf("%s: end\n",s);
pthread_rwlock_unlock(&rwlock);// 解锁
n=rand()%3;
sleep(n);
}
}
int main(){
// 读写锁的初始化
pthread_rwlock_init(&rwlock, NULL);
pthread_t id[3];
int res1 = pthread_create(&id[0], NULL, w_thread, "w_thread");
assert(res1 == 0);
int res2 = pthread_create(&id[1], NULL, r_thread, "r_thread1");
assert(res2 == 0);
int res3 = pthread_create(&id[2], NULL, r_thread, "r_thread2");
assert(res3 == 0);
pthread_join(id[0], NULL);
pthread_join(id[1], NULL);
// 删除读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
运行情况说明:两个读线程可以同时打印"start"和"end"(并发执行);而写线程执行时,两个读线程会阻塞,直到写线程解锁;读线程执行时,写线程也会阻塞,直到所有读线程解锁,完全符合读写锁的特性。

5.自旋锁
自旋锁是 Linux 中轻量级同步机制,与互斥锁功能类似但实现逻辑不同。
核心定义 :自旋锁让等待锁的线程持续忙等(自旋)而非休眠阻塞,直到获取锁,适用于锁持有时间极短的场景。
核心特性
- 忙等不阻塞:获取锁失败时,线程不放弃 CPU,循环检查锁状态,CPU 会持续占用。
- 适用短临界区:仅当临界区执行时间<线程上下文切换开销时使用,否则 CPU 浪费严重。
- 不可休眠场景:内核中断处理程序等不能休眠的场景,只能用自旋锁(互斥锁会导致死锁)。
与互斥锁的关键区别
| 特性 | 自旋锁 | 互斥锁 |
|---|---|---|
| 等待方式 | 忙等(自旋) | 休眠阻塞 |
| CPU 消耗 | 高(自旋时占用 CPU) | 低(休眠释放 CPU) |
| 适用场景 | 锁持有时间极短、不可休眠 | 锁持有时间较长 |
核心接口
头文件<pthread.h>,核心函数如下:
cpp
// 初始化:pshared指定进程共享属性(PRIVATE/SHARED)
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
// 销毁
int pthread_spin_destroy(pthread_spinlock_t *lock);
// 加锁(忙等)
int pthread_spin_lock(pthread_spinlock_t *lock);
// 尝试加锁(失败立即返回EBUSY)
int pthread_spin_trylock(pthread_spinlock_t *lock);
// 解锁
int pthread_spin_unlock(pthread_spinlock_t *lock);
使用注意事项
- 禁止长时间持有:否则会持续占用 CPU,导致其他线程自旋等待过久。
- 不可调用休眠函数:持有自旋锁时,不能调用
sleep/read等可能让线程休眠的函数。 - 不支持重入:同一线程对已持有的自旋锁再次加锁,会导致未定义行为(如死锁)。
- 返回值检查:加锁失败会返回错误码(如
EDEADLK死锁、EBUSY锁被占用),需处理。
五、线程安全性
1. 定义
线程安全是指:在多线程运行环境中,不论线程的调度顺序如何,多个线程同时调用同一个函数或访问同一个资源时,最终的结果始终是正确的、一致的,不会出现数据混乱、逻辑错误或程序崩溃。
2. 不安全的原因
核心原因是"竞态条件":多个线程同时访问和修改共享资源(如全局变量、静态变量),且访问/修改操作不是原子操作,导致线程之间相互干扰,出现数据错误。
只要使用了全局变量或静态变量的函数,大多是线程不安全的(不可重入函数)。
不可重入函数:当函数被多个线程反复调用时,会产生错误的结果(因共享了非局部变量)。
3. 保证安全的方法
-
实现线程同步:通过互斥锁、信号量、条件变量、读写锁等机制,保证同一时刻只有一个线程访问临界资源(临界区操作原子化)。
-
使用线程安全的函数(可重入函数):这类函数不使用全局变量、静态变量,仅使用局部变量或通过参数传递数据,多个线程同时调用不会产生竞态条件。
-
避免滥用全局变量:尽量使用局部变量,若必须使用共享变量,需通过同步机制保护。
4. 死锁(重点)
概念:两个或多个线程(进程)在执行过程中,因争夺临界资源或程序推进顺序不当,导致相互等待、永久阻塞的现象。一旦发生死锁,线程会一直阻塞,无法继续执行,程序也无法正常退出。
产生的原因主要有:
- 系统资源不足:多个线程争夺有限的资源(如信号量初始值太小),导致部分线程无法获取资源而阻塞,进而引发相互等待。
- 程序推进顺序不当:线程获取资源和释放资源的顺序不合理(如线程A先获取锁1,线程B先获取锁2,然后两者都尝试获取对方已持有的锁)。
- 资源分配不当:资源分配策略不合理,导致资源无法被及时释放,多个线程长期占用资源并等待其他资源。
死锁产生的必要条件:
- 互斥条件:临界资源只能被一个线程(进程)占用,其他线程(进程)只能等待,无法同时访问。
- 请求和保持条件:线程(进程)获取部分资源后,又尝试获取其他资源,同时不释放已持有的资源。
- 不剥夺条件:线程(进程)已获取的资源,不能被其他线程(进程)强制剥夺,只能由自身主动释放。
- 环路等待条件:多个线程(进程)之间形成循环等待链,每个线程(进程)都在等待下一个线程(进程)释放资源。
死锁的处理方法:
- 预防死锁:破坏死锁产生的任意一个必要条件(最常用,如按固定顺序获取资源、一次性获取所有资源)。
- 避免死锁:在资源分配前,通过算法判断是否会产生死锁(如银行家算法),若可能产生死锁,则拒绝分配资源。
- 检测死锁:通过系统工具或自定义算法,检测系统中是否存在死锁(如资源分配图)。
- 解除死锁:当检测到死锁后,采取措施释放资源、撤销线程(进程),打破死锁循环(如剥夺死锁线程的资源、撤销优先级最低的线程)。
六、总结
线程同步是多线程编程的核心,用于解决多线程共享资源时的数据竞争问题,保证程序运行结果正确、稳定。
1. 核心目的
- 让多线程按规则、有序地访问临界资源
- 避免数据混乱、逻辑错误、程序崩溃
- 保证线程安全
2. 四大同步机制对比
| 同步机制 | 核心作用 | 典型场景 | 特点 |
|---|---|---|---|
| 互斥锁 | 同一时刻只允许一个线程访问资源 | 打印机、变量自增、独占资源 | 简单通用、完全互斥 |
| 信号量 | 控制可用资源的数量 | 顺序执行、有限资源并发 | 可控制并发数、可实现同步 |
| 条件变量 | 线程等待某个条件成立再执行 | 生产者消费者、数据就绪通知 | 不浪费 CPU、必须配合互斥锁 |
| 读写锁 | 读共享、写互斥 | 读多写少、配置文件、缓存 | 读并发高、写独占 |
