目录
一、临界资源与线程安全问题
二、互斥:让临界区 “独占” 执行
1.互斥锁的原理
2.互斥锁的使用步骤(pthread 库)
2.1 定义互斥锁
2.2 初始化互斥锁
2.3 加锁(进入临界区)
2.4 解锁(离开临界区)
2.5 销毁互斥锁
3.互斥锁实战示例
三、同步:让线程 “按顺序” 执行
1. 同步与互斥的关系
2.信号量的原理
3.信号量的使用步骤
3.1 定义信号量
3.2 初始化信号量
3.3 信号量的 PV 操作
3.4 销毁信号量
4.信号量同步实战示例
四、死锁
一、临界资源与线程安全问题
- 临界资源:在线程间会被读写操作的资源(比如全局变量、文件、硬件设备)。
- 线程安全问题:多个线程 “穿插执行” 临界资源的操作时,会破坏数据一致性。
举个例子:A++看似是一行代码,但编译后会分成 3 步(读 A→A+1→写回 A)。如果线程 1 执行到 “读 A” 后被切换到线程 2,线程 2 也执行A++,最终 A 的值会比预期小 —— 这就是数据竞争。
二、互斥:让临界区 “独占” 执行
互斥的核心是排他性访问:同一时刻,只有一个线程能操作临界资源。
1.互斥锁的原理
通过 “锁” 来保护临界区代码(操作临界资源的代码):
- 线程要执行临界区,必须先 “加锁”;
- 锁被占用时,其他线程会阻塞等待;
- 线程执行完临界区,必须 “解锁”,让其他线程可以竞争锁。
th1、th2是并发运行的两个线程。也就是代码在运行时,th1与th2是穿插进行的。
2.互斥锁的使用步骤(pthread 库)
Linux 下用 pthread_mutex_t 实现互斥锁,步骤是:定义→初始化→加锁→解锁→销毁。
2.1 定义互斥锁
#include <pthread.h> // 定义全局/共享的互斥锁 pthread_mutex_t mutex;2.2 初始化互斥锁
// 函数原型 int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); // 示例 pthread_mutex_init(&mutex, NULL);- 功能:将已经定义好的互斥锁初始化。
- 参数:mutex 是要初始化的锁;attr 传NULL表示用默认属性。
- 返回值:成功返回 0,失败返回非 0。
2.3 加锁(进入临界区)
// 函数原型 int pthread_mutex_lock(pthread_mutex_t *mutex);- 功能:用指定的互斥锁开始加锁代码,成功则进入临界区;失败则阻塞等待。
- 注意:加锁后的代码是原子操作(线程调度不会打断这段代码)。
2.4 解锁(离开临界区)
// 函数原型 int pthread_mutex_unlock(pthread_mutex_t *mutex);- 功能:将指定的互斥锁解锁,让其他线程可以竞争。解锁之后代码不再排他访问。
- 注意:加锁和解锁必须成对出现,且要在同一个线程中执行。
2.5 销毁互斥锁
// 函数原型 int pthread_mutex_destroy(pthread_mutex_t *mutex);- 功能:释放互斥锁的资源,锁不再使用时调用。
3.互斥锁实战示例
比如两个线程同时对全局变量 A 做 ++ 操作,用互斥锁保证线程安全:
#include <pthread.h> #include <stdio.h> int A = 0; pthread_mutex_t mutex; // 线程函数 void* th(void* arg) { int i = 5000; while (i--) { // 加锁:进入临界区 pthread_mutex_lock(&mutex); int tmp = A; printf("A is %d\n", tmp + 1); // 循环输出到 A is 10000 A = tmp + 1; // 解锁:离开临界区 pthread_mutex_unlock(&mutex); } return NULL; } int main(int argc, char** argv) { pthread_t tid1, tid2; // 初始化互斥锁 pthread_mutex_init(&mutex, NULL); pthread_create(&tid1, NULL, th, NULL); pthread_create(&tid2, NULL, th, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); // 销毁互斥锁 pthread_mutex_destroy(&mutex); return 0; }三、同步:让线程 “按顺序” 执行
互斥解决了 “资源独占”,但没解决 “执行顺序”。同步是让线程按预先约定的顺序执行(比如 “线程 1 输出 Hello 后,线程 2 再输出 World”)。
1. 同步与互斥的关系
- 同步是互斥的 “特例”:同步不仅要排他访问,还要控制执行顺序。
- 实现同步的工具:信号量(可以理解为 “带计数的锁”)。
- 互斥锁:加锁和解锁是同一个线程,临界区代码短小精悍,避免休眠、大耗时的操作
- 信号量:th1 释放 th2,th2 释放 th1。由线程交叉释放。可以有适当休眠、小的耗时操作
2.信号量的原理
信号量是一个整数 sem,通过 P 操作(申请资源)和 V 操作(释放资源)实现同步:
- P 操作:sem--,若 sem<0 则线程阻塞;
- V 操作:sem++,若 sem<=0 则唤醒一个阻塞的线程。
注:Linux 下用 sem_t 实现信号量
3.信号量的使用步骤
步骤是:定义→初始化→PV 操作→销毁。
3.1 定义信号量
#include <semaphore.h> sem_t sem;3.2 初始化信号量
// 函数原型 int sem_init(sem_t *sem, int pshared, unsigned int value);- 参数:
- sem:要初始化的信号量;
- pshared:0表示线程间共享,非 0 表示进程间共享;
- value:信号量初始值(比如 0 表示 “无资源”,1 表示 “有 1 个资源”)。
- 返回值:成功返回 0,失败返回 -1。
3.3 信号量的 PV 操作
- P 操作(申请资源):对应sem_wait()
int sem_wait(sem_t *sem);功能:判断当前 sem 信号量是否有资源可用。
如果 sem 有资源 (==1),则申请该资源,程序继续运行;
如果 sem 没有资源 (==0),则线程阻塞等待,一旦有资源则自动申请资源并继续运行程序。
- V 操作(释放资源):对应sem_post()
int sem_post(sem_t *sem);功能:函数可以将指定的 sem 信号量资源释放,并默认执行 sem = sem+1。
线程在该函数上不会阻塞。
3.4 销毁信号量
int sem_destroy(sem_t *sem);功能:使用完毕将指定的信号量销毁。
4.信号量同步实战示例
#include <pthread.h> #include <semaphore.h> #include <stdio.h> #include <unistd.h> sem_t sem_H, sem_W; // 线程1:输出Hello void *th1(void *arg) { int i = 10; while (i--) { sem_wait(&sem_H); printf("hello "); fflush(stdout); sem_post(&sem_W); } return NULL; } // 线程2:输出World void *th2(void *arg) { int i = 10; while (i--) { sem_wait(&sem_W); printf("world\n"); sleep(1); sem_post(&sem_H); } return NULL; } int main(int argc, char **argv) { pthread_t tid1, tid2; // 初始化信号量:sem_H=1(线程1可以直接执行),sem_W=0(线程2等待) sem_init(&sem_H, 0, 1); sem_init(&sem_W, 0, 0); pthread_create(&tid1, NULL, th1, NULL); pthread_create(&tid2, NULL, th2, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); sem_destroy(&sem_H); sem_destroy(&sem_W); return 0; }四、死锁
由于锁资源安排的不合理(锁资源的申请和释放逻辑不对),导致进程、线程无法正常继续执行(推进)的现象。
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
破坏任意一个就能避免死锁(比如按固定顺序申请资源)。