文章目录
定义
(1)临界资源:多执行流下被共享的资源 ,凡是被线程共享访问的资源都是临界资源(多线程、多进程打印数据到显示器)
(2)临界区:代码中访问临界资源的代码
(3)互斥、同步:实现线程访问控制的策略,实现对临界区或对临界资源的保护的功能
(4)互斥:在任意时刻,只允许一个执行流访问某段代码(某部分资源)
(5)同步:一般而言,让访问临界资源的过程在安全的前提下(一般是互斥、具有原子性的),让访问资源具有一定的顺序性–>让访问资源具有合理性
抢票:
// 抢票 1000票,5线程抢
int tickets = 1000;
void* ThreadRoutine(void *args){
int id = *(int*)args;
delete (int*)args;
while(true){
if(tickets>0){
// 抢票
usleep(1000);
std::cout<<"【"<<id<<"】:"<<tickets<<std::endl;
tickets--;
}else{
break;
}
//sleep(1);
}
}
int main(){
pthread_t tid[5];
for(int i = 0;i<5;i++){
int *id = new int(i);
pthread_create(tid+i,nullptr,ThreadRoutine,id);
}
for(int i = 0;i<5;i++){
pthread_join(tid[i],nullptr);
}
return 0;
}
运行发现出现了tickets<0的情况!
原因:tickets是临界资源,不是原子的;
tickets–: ①数据由内存加载到CPU②CPU进行–计算③数据重新写到内存中,可能出现一个线程这三步没有全部完成就切换到了下一个线程,从而导致语句中断(线程切换:1.时间片到了2.信号检测:从内核态返回用户态时)
为了避免这种问题,需要对临界区进行加锁
互斥锁:
相关函数:
销毁函数:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:pthread_mutex_t *mutex:锁的地址
初始化函数:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
参数:pthread_mutex_t *restrict mutex:锁的地址
const pthread_mutexattr_t *restrict attr:锁的属性,设为空
宏初始化:(全局变量或者static)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
加锁:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
解锁:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
改进抢票:对临界区抢票代码进行加解锁处理
#include<metux> // C++内的
// 抢票 1000票,5线程抢
class Ticket{
private:
int tickets;
pthread_mutex_t mtx;//系统级别,原生线程库
std::metux mymtx;// c++语言级别
public:
Ticket():tickets(1000){
pthread_mutex_init(&mtx,nullptr);
}
bool GetTicket(){
pthread_mutex_lock(&mtx);
// 保护临界区
if(tickets>0){
// 抢票
usleep(1000);
std::cout<<"【"<<pthread_self()<<"】:"<<tickets<<std::endl;
tickets--;
pthread_mutex_unlock(&mtx);
return true;
}else{
printf("售罄\n");
pthread_mutex_unlock(&mtx);
return false;
}
}
~Ticket(){
pthread_mutex_destroy(&mtx);
}
};
void* ThreadRoutine(void *args){
Ticket* t= (Ticket*)args;
while(true){
if(t->GetTicket()){
continue;
}else{
break;
}
}
}
int main(){
Ticket *t = new Ticket();
pthread_t tid[5];
for(int i = 0;i<5;i++){
int *id = new int(i);
pthread_create(tid+i,nullptr,ThreadRoutine,(void*)t);
}
for(int i = 0;i<5;i++){
pthread_join(tid[i],nullptr);
}
return 0;
}
可以看到,设置了互斥锁不会再出现票数小于0的情况
注意:
在访问临界资源tickets之前,需要先访问mtx,因此需要所有线程必须先看到mtx,由此可得出,锁本身也是一种临界资源,因此,为了保证锁本身是安全的,需要lock、unlock是原子性的
线程安全和可重入:
线程安全描述的是线程之间互相影响的一种可能性
重入描述的是函数可不可以被重复进入
死锁
定义:
一组执行流因为某些场景而导致它占有了锁资源不释放,同时还在申请对方的资源而处于的永久等待状态
四个必要条件:
互斥条件:
一个资源每次只能被一个执行流使用
请求与保持条件:
一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:
一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:
若干执行流之间形成一种头尾相接的循环等待资源的关系
避免死锁
(1)破坏死锁的四个必要条件
(2)加锁顺序一致
(3)避免锁未释放的场景
(4)资源一次性分配