线程控制:互斥与同步
在多线程编程中,多个线程共享进程资源是提高效率的关键,但同时也带来了临界资源访问冲突的问题。比如多个线程同时操作一个变量、一块内存或一个文件时,可能会导致数据错乱、结果不可预期。解决这类问题的核心就是互斥与同步机制 —— 前者保证资源的排他性访问,后者保证线程间操作的有序性。
一、互斥:临界资源的 "排他通行证"
1. 什么是互斥?
互斥指的是在多线程环境中,对临界资源(如共享变量、文件、内存块等)的访问必须是排他性的:同一时刻只能有一个线程操作该资源,其他线程需等待当前线程释放资源后才能访问。
临界资源的典型特征是 "多线程共享且修改",例如多个线程同时对一个计数器进行自增操作,若不控制互斥,最终结果可能远小于预期值(因指令交错执行)。
2. 互斥的实现:互斥锁(Mutex)
互斥锁是实现互斥的核心机制,它像一把 "锁",线程访问临界资源前需 "上锁",访问完毕后 "解锁"。未获得锁的线程会阻塞等待,直到锁被释放。
(1)互斥锁的核心要素
- 类型:
pthread_mutex_t
(POSIX 标准定义的互斥锁类型) - 本质:内核对象,用于标记资源的占用状态
- 操作原则:加锁(
lock
)和解锁(unlock
)之间的代码为原子操作(不可分割,要么全执行,要么全不执行)
(2)互斥锁的使用框架
互斥锁的使用需遵循固定流程:定义→初始化→加锁→解锁→销毁,每个步骤对应特定函数:
步骤 | 函数 | 功能描述 |
---|---|---|
定义锁 | pthread_mutex_t mutex; | 声明一个互斥锁变量(内核对象) |
初始化锁 | int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); | 初始化锁,attr 为锁属性(通常传NULL 表示默认属性) |
加锁(阻塞) | int pthread_mutex_lock(pthread_mutex_t *mutex); | 尝试获取锁,若锁已被占用则阻塞等待,直到锁释放 |
解锁 | int pthread_mutex_unlock(pthread_mutex_t *mutex); | 释放锁,唤醒等待该锁的线程 |
销毁锁 | int pthread_mutex_destroy(pthread_mutex_t *mutex); | 释放锁占用的资源(必须在所有线程解锁后执行) |
(3)关键函数详解
初始化锁(
pthread_mutex_init
):- 参数
mutex
:指向要初始化的锁变量的指针; - 参数
attr
:锁的属性(如共享范围、类型等),NULL
表示默认锁(普通互斥锁); - 返回值:成功返回 0,失败返回非 0 错误码。
- 参数
加锁(
pthread_mutex_lock
):- 若锁未被占用,当前线程获取锁,继续执行;
- 若锁已被占用,当前线程进入阻塞状态(放弃 CPU,等待锁释放);
- 加锁后到解锁前的代码为 "临界区",确保唯一线程访问。
非阻塞加锁(
pthread_mutex_trylock
):- 功能与
pthread_mutex_lock
类似,但不阻塞:若锁已被占用,直接返回非 0 错误码(如EBUSY
),线程可执行其他逻辑。
- 功能与
(4)示例:用互斥锁解决共享变量冲突
下面的代码模拟 10 个线程争夺 3 个资源的场景,通过互斥锁保证资源计数的正确性:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>int count = 3; // 共享资源:3个可用资源
pthread_mutex_t mutex; // 定义互斥锁void *thread_func(void *arg) {while (1) {// 加锁:进入临界区pthread_mutex_lock(&mutex);if (count > 0) {printf("线程%d获取资源,剩余%d个\n", *((int*)arg), --count);pthread_mutex_unlock(&mutex); // 解锁:退出临界区break; // 获取资源后退出循环} else {printf("线程%d等待资源...\n", *((int*)arg));pthread_mutex_unlock(&mutex); // 解锁:避免死锁// 短暂休眠,让其他线程有机会获取锁usleep(100000);}}return NULL;
}int main() {pthread_t tid[10];int ids[10];// 初始化互斥锁pthread_mutex_init(&mutex, NULL);// 创建10个线程for (int i = 0; i < 10; i++) {ids[i] = i;pthread_create(&tid[i], NULL, thread_func, &ids[i]);}// 等待所有线程结束for (int i = 0; i < 10; i++) {pthread_join(tid[i], NULL);}// 销毁互斥锁pthread_mutex_destroy(&mutex);return 0;
}
3. 互斥锁练习
练习 1:多线程安全写入字符数组
设计多线程程序,让 5 个线程向同一块字符数组写入不同字符串,要求同一时刻只有一个线程操作数组(用堆区互斥锁,即动态分配的锁)。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>char shared_buf[1024] = {0}; // 共享字符数组
pthread_mutex_t *mutex; // 堆区互斥锁void *write_func(void *arg) {char *str = (char*)arg;// 加锁:独占访问数组pthread_mutex_lock(mutex);strcat(shared_buf, str);printf("写入后数组内容:%s\n", shared_buf);// 解锁:释放访问权pthread_mutex_unlock(mutex);return NULL;
}int main() {pthread_t tid[5];char *strs[5] = {"Hello ", "World ", "from ", "thread ", "!"};// 堆区分配并初始化互斥锁mutex = malloc(sizeof(pthread_mutex_t));pthread_mutex_init(mutex, NULL);// 创建线程写入数据for (int i = 0; i < 5; i++) {pthread_create(&tid[i], NULL, write_func, strs[i]);}// 等待所有线程结束for (int i = 0; i < 5; i++) {pthread_join(tid[i], NULL);}// 销毁锁并释放堆内存pthread_mutex_destroy(mutex);free(mutex);return 0;
}
二、同步:线程间的 "有序协作"
1. 什么是同步?
同步指的是多线程在访问资源时,需按照预定的先后顺序执行(如 "先生产后消费")。互斥锁仅保证排他性,无法控制执行顺序,而同步机制则解决这一问题。
例如:线程 A 负责向缓冲区写数据,线程 B 负责读数据,必须保证 "线程 A 写完后,线程 B 才能读",这就是同步的典型场景。
2. 同步的实现:信号量(Semaphore)
信号量是实现同步的核心机制,它通过一个计数器控制线程的执行顺序:线程需先 "申请" 信号量(计数器减 1),若计数器为 0 则阻塞;执行完毕后 "释放" 信号量(计数器加 1),唤醒阻塞的线程。
(1)信号量的分类
- 无名信号量:用于线程间同步(共享于同一进程的线程),需初始化在共享内存(如堆区);
- 有名信号量:用于进程间同步(通过文件系统路径标识),本文重点讲解无名信号量。
(2)信号量的使用框架
与互斥锁类似,信号量的使用流程为:定义→初始化→P 操作→V 操作→销毁:
步骤 | 函数 | 功能描述 |
---|---|---|
定义信号量 | sem_t sem; | 声明信号量变量 |
初始化 | int sem_init(sem_t *sem, int pshared, unsigned int value); | 初始化信号量,pshared=0 表示线程间使用,value 为初始计数器值 |
P 操作 | int sem_wait(sem_t *sem); | 申请资源:计数器减 1,若结果 < 0 则阻塞等待 |
V 操作 | int sem_post(sem_t *sem); | 释放资源:计数器加 1,唤醒阻塞的线程 |
销毁 | int sem_destroy(sem_t *sem); | 释放信号量占用的资源 |
(3)关键函数详解
初始化(
sem_init
):- 参数
sem
:指向信号量变量的指针; - 参数
pshared
:0 表示线程间使用,非 0 表示进程间使用; - 参数
value
:初始计数器值(例如:value=1
表示 "互斥信号量",value=0
表示 "同步信号量"); - 返回值:成功返回 0,失败返回 - 1。
- 参数
P 操作(
sem_wait
):- 功能:申请资源。计数器先减 1,若结果≥0,线程继续执行;若结果 < 0,线程阻塞。
- 例如:初始
value=0
时,第一个执行sem_wait
的线程会阻塞,直到其他线程执行sem_post
(计数器变为 1)。
V 操作(
sem_post
):- 功能:释放资源。计数器加 1,若有线程因该信号量阻塞,则唤醒其中一个。
(4)示例:用信号量实现 "输入 - 统计" 同步
需求:线程 A 获取用户输入,线程 B 统计输入长度,要求 B 必须在 A 输入后执行,输入 "quit" 时程序结束。
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <string.h>char input_buf[1024] = {0};
sem_t sem; // 同步信号量:控制B在A之后执行// 线程A:获取用户输入
void *input_thread(void *arg) {while (1) {printf("请输入内容(输入quit退出):");fgets(input_buf, sizeof(input_buf), stdin);// 移除换行符input_buf[strcspn(input_buf, "\n")] = '\0';// V操作:通知B可以统计(计数器+1)sem_post(&sem);// 若输入quit,退出循环if (strcmp(input_buf, "quit") == 0) break;}return NULL;
}// 线程B:统计输入长度
void *count_thread(void *arg) {while (1) {// P操作:等待A输入(计数器-1,若为0则阻塞)sem_wait(&sem);if (strcmp(input_buf, "quit") == 0) break;printf("输入长度:%ld\n", strlen(input_buf));}return NULL;
}int main() {pthread_t tidA, tidB;// 初始化信号量:线程间使用,初始值0(B先阻塞,等待A的V操作)sem_init(&sem, 0, 0);// 创建线程pthread_create(&tidA, NULL, input_thread, NULL);pthread_create(&tidB, NULL, count_thread, NULL);// 等待线程结束pthread_join(tidA, NULL);pthread_join(tidB, NULL);// 销毁信号量sem_destroy(&sem);return 0;
}
3. 同步练习:火车票售票系统
需求:2 个售票窗口(线程)共卖 100 张票,要求不重复售票,输出格式如 "窗口 1 卖出车票 1"。
思路:用互斥锁保证票数的原子操作,用信号量控制初始可售票数(value=100
)。
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>int ticket_count = 100; // 总票数
pthread_mutex_t mutex; // 互斥锁:保护ticket_count
sem_t sem; // 信号量:控制可售票数void *sell_ticket(void *arg) {int window = *((int*)arg);while (1) {// P操作:申请售票资格(若票已售完则阻塞)sem_wait(&sem);// 加锁:修改票数pthread_mutex_lock(&mutex);if (ticket_count <= 0) {pthread_mutex_unlock(&mutex);break;}printf("窗口%d 卖出车票%d\n", window, ticket_count--);pthread_mutex_unlock(&mutex);}return NULL;
}int main() {pthread_t tid1, tid2;int win1 = 1, win2 = 2;// 初始化互斥锁和信号量pthread_mutex_init(&mutex, NULL);sem_init(&sem, 0, 100); // 初始100张票// 创建售票窗口线程pthread_create(&tid1, NULL, sell_ticket, &win1);pthread_create(&tid2, NULL, sell_ticket, &win2);// 等待线程结束pthread_join(tid1, NULL);pthread_join(tid2, NULL);// 清理资源pthread_mutex_destroy(&mutex);sem_destroy(&sem);return 0;
}
三、死锁:多线程编程的 "陷阱"
1. 什么是死锁?
死锁是指多个线程因争夺资源而陷入无限等待的状态。例如:线程 A 持有锁 1 并等待锁 2,线程 B 持有锁 2 并等待锁 1,两者永远无法继续执行。
2. 死锁的四个必要条件
死锁的产生必须同时满足以下四个条件,破坏任意一个即可避免死锁:
- 互斥条件:资源只能被一个线程占用;
- 请求与保持条件:线程持有部分资源,同时请求其他资源;
- 不剥夺条件:线程已获得的资源不能被强行剥夺;
- 循环等待条件:线程间形成资源请求的循环链(如 A 等 B,B 等 A)。
3. 避免死锁的建议
- 按固定顺序申请资源(如所有线程先申请锁 1,再申请锁 2);
- 限时申请资源(用
pthread_mutex_trylock
,超时则释放已占资源); - 减少资源持有时间(临界区代码尽量简短)。
总结
- 互斥通过互斥锁实现临界资源的排他访问,核心是
pthread_mutex_lock/unlock
; - 同步通过信号量控制线程执行顺序,核心是
sem_wait
(P 操作)和sem_post
(V 操作); - 死锁是多线程编程的常见问题,需通过合理设计避免四个必要条件的同时满足。
掌握互斥与同步机制,是编写安全、高效多线程程序的基础。实际开发中需结合具体场景选择合适的工具,平衡性能与安全性。