线程控制 - 互斥与同步
一、 互斥(Mutex)
1.概念
在多线程中对临界资源的排他性访问。
-
临界资源:在多线程中会被多个线程进行读写操作的资源(全局变量、文件、设备等)
-
排他访问:同一时刻只能有一个线程进行读写操作
2.用途
在多线程中,一个资源同一时刻只能有一个线程访问。
示例问题
int A = 0; // 临界资源
// 线程1
void* th1(void* arg) {
A++; // 这不是原子操作!
}
// 线程2
void* th2(void* arg) {
A++;
}
A++在汇编中至少需要3步:
-
读取A到寄存器
-
寄存器值加1
-
将结果写回A
如果th1执行了1、2步后切换到th2,就会发生数据一致性问题。
解决方案:使用互斥锁
使用步骤
-
定义互斥锁
-
初始化锁
-
加锁
-
解锁
-
销毁
相关函数
1. 定义互斥锁
pthread_mutex_t mutex;
2. 初始化锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
-
功能:初始化互斥锁
-
参数:
-
mutex:要初始化的互斥锁 -
attr:初始化属性,一般为NULL(默认锁)
-
-
返回值:成功返回0,失败返回非0
3. 加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
-
功能:给代码加锁
-
特点:
-
加锁到解锁之间的代码属于原子操作
-
在加锁期间其他线程不能执行该部分代码
-
如果锁已被占用,线程会阻塞等待
-
-
返回值:成功返回0,失败返回非0
4. 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
-
功能:解锁互斥锁
-
注意:加锁和解锁一般成对出现
-
返回值:成功返回0,失败返回非0
5. 销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
-
功能:销毁互斥锁
-
返回值:成功返回0,失败返回非0
示例代码
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int A = 0 ;
pthread_mutex_t mutex;
void* th(void* arg)
{
// pthread_mutex_lock(&mutex);
int i = 5000;
while(i--)
{
pthread_mutex_lock(&mutex);
int tmp = A;
printf("A is %d\n",tmp+1);
A = tmp+1;
pthread_mutex_unlock(&mutex);
}
//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;
}
2. 同步(Synchronization)
概念
按照一定先后顺序对资源的排他性访问。
与互斥的关系
-
互斥包含同步,同步是互斥的一个特例
-
互斥:只关心资源是否被占用
-
同步:不仅关心资源,还关心访问顺序
3. 信号量(Semaphore)
与互斥锁的区别
-
加锁/解锁主体:
-
互斥锁:加锁和解锁必须是同一个线程
-
信号量:可以由不同线程交叉释放(th1释放th2,th2释放th1)
-
-
使用场景:
-
互斥锁:临界区代码要短小精悍,不要有休眠或耗时操作
-
信号量:可以有适当的休眠和小耗时操作
-
计数信号量
信号量初值可以大于1,用于多个资源的情况。
使用步骤
-
定义信号量
-
初始化信号量
-
PV操作
-
销毁信号量
相关函数
1. 定义信号量
sem_t sem;
2. 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
-
功能:初始化信号量
-
参数:
-
sem:要初始化的信号量 -
pshared:-
0:线程间使用 -
非0:进程间使用
-
-
value:信号量初始值-
二值信号量:0或1
-
0:红灯,线程阻塞 -
1:绿灯,线程可通过
-
-
-
返回值:成功返回0,失败返回-1
3. PV操作
-
P操作 :申请资源 →
sem_wait() -
V操作 :释放资源 →
sem_post()
sem_wait()
int sem_wait(sem_t *sem);
-
功能:申请信号量资源
-
行为:
-
如果有资源(>0),申请资源,继续执行
-
如果没资源(=0),线程阻塞等待
-
-
注意 :自动执行
sem = sem - 1 -
返回值:成功返回0,失败返回-1
sem_post()
int sem_post(sem_t *sem);
-
功能:释放信号量资源
-
注意 :自动执行
sem = sem + 1 -
返回值:成功返回0,失败返回-1
4. 销毁信号量
int sem_destroy(sem_t *sem);
示例代码:
cpp
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <semaphore.h>
sem_t sem_H,sem_W;
void* th1(void* arg)
{
int i = 10;
while(i--)
{
sem_wait(&sem_H);// sem_H -1
printf("hello ");
fflush(stdout);
sem_post(&sem_W);// sem_W +1
}
return NULL;
}
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_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;
}
4. 死锁(Deadlock)
概念
由于锁资源安排不合理,导致进程/线程无法继续执行的现象。
产生死锁的四个必要条件
-
互斥条件:一个资源每次只能被一个进程使用
-
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
-
不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
-
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
死锁示例
pthread_mutex_t mutexA = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutexB = PTHREAD_MUTEX_INITIALIZER;
// 线程1
void* thread1(void* arg) {
pthread_mutex_lock(&mutexA); // 获取锁A
sleep(1);
pthread_mutex_lock(&mutexB); // 尝试获取锁B → 死锁!
// ...
pthread_mutex_unlock(&mutexB);
pthread_mutex_unlock(&mutexA);
return NULL;
}
// 线程2
void* thread2(void* arg) {
pthread_mutex_lock(&mutexB); // 获取锁B
sleep(1);
pthread_mutex_lock(&mutexA); // 尝试获取锁A → 死锁!
// ...
pthread_mutex_unlock(&mutexA);
pthread_mutex_unlock(&mutexB);
return NULL;
}
5. 总结对比
| 特性 | 互斥锁(Mutex) | 信号量(Semaphore) |
|---|---|---|
| 用途 | 保护临界区,确保互斥访问 | 控制资源访问数量 |
| 资源数 | 通常保护单个资源 | 可以保护多个相同资源 |
| 加锁/解锁 | 必须由同一线程完成 | 可由不同线程完成 |
| 阻塞 | 锁被占用时线程阻塞 | 资源数为0时线程阻塞 |
| 计数 | 无计数功能 | 有计数功能 |
| 性能 | 轻量级,适用于短临界区 | 稍重,适用于较复杂同步 |
使用原则
-
能用互斥锁就用互斥锁,因为它更简单高效
-
临界区要短小,不要包含耗时操作
-
避免死锁:按固定顺序申请锁,或使用超时机制
-
锁的粒度要合适:不要过大(影响并发)或过小(增加开销)