目录
一丶线程互斥
1丶互斥概念
- 临界资源:多线程执行流共享的资源就叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
- 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
1丶 临界资源与临界区
两个进程共享的资源就是临界资源,定义的全局变量,缓冲区,等等。临界资源的代码就是临界区。线程是共享进程所有资源的~
2丶线程互斥的必要性
int num=0; void*run(void*args) { while(1) { num++; cout<<"l am new thread"<<" "<<num<<endl; sleep(1); } } int main() { pthread_t tid; pthread_create(&tid,nullptr,run,nullptr); while(1) { cout<<"i am main thread"<<" "<<num<<endl; sleep(1); } pthread_join(tid,nullptr); return 0; }
从这个例子我们就可以发现两个线程共享同一份资源。但是这样的两个线程肆无忌惮的去访问同一个资源会不会有问题?
互斥与原子性的概念
互斥:
你在打野怪突然对面来人了,也想打这个野怪这时候怎么办?你们两个一起打然后打完的钱一人一半么?显然是不可能的。这里你和敌人就被看作是两个线程,野怪也就是临界资源。而解决这一问题的方法就是要么你把他干死或者他把你干死,你们两个争斗的行为就是互斥,当只剩一个人的时候就可以安全的打这只野怪了。
互斥的作用:保证在任何时候有且只有一个执行流进入临界区对临界资源进行访问。
原子性:
能保证这段命令是一次执行的(成功或者失败都可以)称之为有原子性,一个人想给你转一些钱,你收到这笔钱你俩操作同时成功,要么就是同时失败。
一个程序要被保证有原子性,就要被完整的执行,要么就是不执行,执行时候被打断那么这个程序就是非原子性的~
原子性:原子性指的是不可被分割的操作,该操作不会被任何调度机制打断,该操作只有两态,要么完成,要么未完成。
非互斥的example;
我们实现一个抢票机制,多人一起抢票最后票被强光 ~
using namespace std; int tickets = 1000;//定义一个全局变量,这就是临界资源,1000张票 void* ThreadRotinue(void* args) { int id = *(int*)args; delete (int*)args; while(true) { if(tickets > 0) { usleep(10000); //usleep函数能把线程挂起一段时间, 单位是微秒(千分之一毫秒)。 printf("我是[%d] 我抢的票是:%d\n", id, tickets); tickets--; //抢票,票数递减 } else { break; } } } int main() { pthread_t tid[5]; for(int i = 0; i < 5; i++)//主线程创建出5个线程去抢票 { int* id = new int(i); pthread_create(tid + i, nullptr, ThreadRotinue, id); } for(int i = 0; i < 5; i++) { pthread_join(tid[i], nullptr); //等待线程 } return 0; }
很明显这个抢票系统出了问题!为什么会出现这样的问题?
- if 语句判断条件为真以后,代码可以并发的切换到其他线程
- usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
- --tickets 操作本身就不是一个原子操作
ticket--其实并不是原子性的操作~
我们在vs上面进行调试打开反汇编
可以发现ticket--执行了三次反汇编~
- mov :将共享变量tickets从内存加载到寄存器中
- sub: 更新寄存器里面的值,执行-1操作
- mov:将新值,从寄存器写回共享变量ticket的内存地址
首先T1把ticket值读进寄存器,--后就被切走了,此时寄存器中保存着T1的上下文信息此时T1保存的值就是999,然后被挂起等待。
然后T2就被调度了,T2把值放进去看到是999也执行--操作,然后T1又被切过来,但是这时T1中的上下文数据是999,他又要和寄存器的值做交换,--。这样已经出问题了~
这里可以解释ticket--也并不是原子性操作~
2丶互斥量
- 大部分情况,线程使用的