1. 互斥
1.1 为什么需要互斥
多线程抢票模型演示了并发访问共享数据的问题。
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string>
#include<vector>
using namespace std;
#define NUM 4
int ticket = 100; // 用多线程,模拟一轮抢票
class ThreadData {
public:
ThreadData(int number){ _thread_name = "thread-" + to_string(number); }
public:
string _thread_name;
};
void* GetTicket(void* args) {
ThreadData* td = static_cast<ThreadData*>(args);
const char* name = td->_thread_name.c_str();
while(true) {
if(ticket > 0){
usleep(5000);
printf("i am %s, get a ticket:%d\n", name, ticket);
ticket--;
} else break;
}
printf("%s ... quit\n", name);
return nullptr;
}
int main() {
vector<pthread_t> tids;
vector<ThreadData*> thread_datas;
for(int i=0; i<NUM; i++) {
pthread_t tid;
ThreadData* td = new ThreadData(i);
thread_datas.push_back(td);
pthread_create(&tid, nullptr, GetTicket, thread_datas[i]);
tids.push_back(tid);
}
for(auto &e : tids) {
pthread_join(e, nullptr);
}
for(auto &e : thread_datas) {
delete e;
}
return 0;
}
在上述代码中,抢票到最后可能出现负数。这是因为 ticket-- 不是原子操作,涉及读取、修改、写回三个步骤。若在此过程中发生线程切换,会导致数据不一致。例如,线程 A 读取票数为 1000,线程 B 执行 100 次减操作后写回 900,线程 A 恢复上下文后再次写回 999,导致票数计算错误。
1.2 互斥锁
解决上述问题的方法是引入互斥锁,保证任何时候只有一个执行流访问共享数据。
锁资源的定义、初始化和释放:
pthread_mutex_t是库提供的数据类型。pthread_mutex_init用于初始化锁。pthread_mutex_destroy用于销毁锁(全局变量使用宏初始化时可不销毁)。
锁的申请和释放:
pthread_mutex_lock加锁。pthread_mutex_unlock解锁。pthread_mutex_trylock非阻塞加锁版本。
临界资源是指被多个线程访问的资源,访问临界资源的代码区称为临界区。加锁的本质是用时间换安全,表现为线程对临界区代码的串行执行。原则是尽量保证临界区代码越少越好。若锁分配不合理,可能导致其他线程饥饿。
互斥锁版的多线程抢票模型代码演示:
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string>
#include<vector>
using namespace std;
#define NUM 5
int ticket = 100;
class ThreadData {
public:
ThreadData(int number, pthread_mutex_t* lock) {
_thread_name = "thread-" + to_string(number);
_lock = lock;
}
public:
string _thread_name;
pthread_mutex_t *_lock;
};
void* GetTicket(void* args) {
ThreadData* td = static_cast<ThreadData*>(args);
const char* name = td->_thread_name.c_str();
while(true) {
pthread_mutex_lock(td->_lock);
if(ticket > 0){
printf("i am %s, get a ticket:%d\n", name, ticket);
ticket--;
} else {
pthread_mutex_unlock(td->_lock);
break;
}
pthread_mutex_unlock(td->_lock);
();
}
(, name);
;
}
{
lock;
(&lock, );
vector<> tids;
vector<ThreadData*> thread_datas;
( i=; i<NUM; i++) {
tid;
ThreadData* td = (i, &lock);
thread_datas.(td);
(&tid, , GetTicket, thread_datas[i]);
tids.(tid);
}
( &e : tids) {
(e, );
}
( &e : thread_datas) {
e;
}
(&lock);
;
}
1.3 初谈互斥与同步
互斥解决了数据竞争问题,但可能导致线程饥饿。同步则规定了获取资源的顺序。例如 VIP 自习室规则要求排队进入,按顺序获取资源即为同步。
1.4 锁的原理
锁本身也是临界资源,申请和释放锁被设计为原子性操作。在汇编层面,xchqb 指令交换内存与寄存器数据,实现原子加锁;movb 指令写回数据实现解锁。对于其他线程而言,当前线程持有锁的过程是原子的。
1.5 可重入 VS 线程安全
- 线程安全:多个线程并发同一段代码时,结果一致。
- 可重入:同一函数被不同执行流调用,前一个流程未结束时另一个再次进入,结果仍正确。
- 关系:可重入函数一定是线程安全的,但线程安全不一定是可重入的。若函数中有全局变量且无锁保护,则既不安全也不可重入。若加锁保护但锁未释放即重入,可能引发死锁。
1.6 死锁
死锁指进程因互相申请对方占用的资源而永久等待。
产生必要条件:
- 互斥条件
- 请求与保持条件
- 不剥夺条件
- 循环等待条件
避免方法:
- 破坏任一必要条件
- 加锁顺序一致
- 避免锁未释放场景
- 资源一次性分配
1.7 避免死锁的算法(扩展)
银行家算法可用于避免死锁,通过模拟资源分配确保系统处于安全状态。


