线程的互斥
- 临界区资源
- 多个线程的运行
- 多个线程对同一资源的竞争
- 原子性
- 保持线程之间地互斥
- 互斥量(锁的原理)为什么是原子的
- 正确使用锁
临界区资源
进程创建线程,是共享内存的,可以对共享的资源有很方便的操作,当一些共享资源可以被多个线程进行访问操作,该共享的资源被称之为临界资源。比如多个线程如果都有对全局的静态变量进行访问或操作,则该变量就是共享资源。每个执行流的代码访问临界资源就叫做临界区。
多个线程的运行
多个线程在执行的时候,调度哪一个执行流是程序员不可知道的,是由操作系统OS来进行调度的,多个线程可能会有交叉的现象,这种现象是不允许的,比如订票的系统,如果两个线程出现了交叉的现象,可能就会造成最后一张票被两个线程进行获取,两个线程在竞争。有些线程的执行是需要其他的线程执行完之后才能被调度,线程A的资源可能要等到线程B运行完才会有,所以线程会有先后顺序。
多个线程对同一资源的竞争
1step:当票数为1的时候,线程A进行抢票,线程A先对票数进行检查,如果大于1,则进行先一步购票,对票数自减。线程A已经判断出票数剩余1,进行抢票,但系统发现线程A分配的运行时间已经到了,但线程A还未进行购票进行票数自减,然后暂时退出自己的执行流,但线程A会把已经处理的数据(CPU寄存器的数据,判断票数大于1)进行保存(保存在自己进程的上下文),在下次再次被调度时继续使用。
2step:此时,线程B开始被调度进行抢票,假设线程B的时间片足够长,线程B开始执行,也对票数进行判断,票数为1,因为线程A还没对票数进行自减时间片就到了,所以在系统的内存当中,票数依旧为1,然后线程B购票,对票数进行了自减,最后票数为0了,然后再把0的票数拷贝到系统内存,最后票数为0了。此时线程B结束。
3step:过了极短的时间,线程A又有了时间进行调度,因为线程A在之前调度时已经逻辑运算判断票数为1,并把数据保存在自己的进程上下文当中,再次调度时,线程A就会执行下一步购票的任务,首先会在系统的内存当中拷贝剩余的票数到CPU,然后进行自减,因为票数已经被线程B已经自减为0了,所以线程A会对票数0进行自减,最后得到-1的值,然后再把-1拷贝会系统内存当中。会发现剩余票数为-1,导致了数据不正确。对与上述的票资源,本质上是临界资源。是多线程程序中的共享变量。引发数据不准确的原因,又是因为线程时间片结束时,但执行流还未执行完,导致有些数据保存到进程独立的上下文最后因为这些行为不是原子的所导致。这种行为就是线程不是安全的。
原子性
票数的操作在C语言的情况下是不原子的,经过编译后会有三条汇编语句,虽然每条汇编语句是原子的。两条C语句经过汇编会生成5条汇编语句,每执行到一条汇编时,线程都有可能会被操作系统切换,一旦还没执行完全部任务,就会把这些寄存器的数据保存到自己的独立上下文。即使每条汇编是原子的,但多条汇编不一定是原子的,所以C语句不是原子的,每个线程调用时,不能保证在执行完整的过程中,保证该资源一直时一个线程独享的。
保持线程之间地互斥
为了保证共享资源得安全性,临界资源只能由一个执行流访问并进行操作,需要使用一些手段让临界资源只有一个执行流,这种就是互斥。可以通过添加互斥量(锁)这个资源来对多个线程对临界资源进行单独访问操作(加锁)。当执行流已经完成了临界资源得访问和临界区得代码执行完毕,就可以释这个互斥量(解锁)让其他得线程可以访问临界资源和申请锁这个资源继续单独得访问临界资源(锁是原子的)。
int cnt = 100;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 申请锁资源并初始化,这个锁是全局的
///
while(true)
{
pthread_mutex_lock(&mutex);//加锁
if (cnt > 0)
cnt--;
pthread_mutex_unlock(&mutex);//解锁
}
如果一个线程成功申请了锁这个变量,在解锁和加锁这段临界区的临界资源,其他线程是无法访问到的,即使申请成功锁得线程随时被OS切换不调度,必须要等待该线程把锁资源释放掉了(等待过程就是阻塞得过程),其他线程才有机会申请到锁资源并访问临界资源(锁本身也得原子的)。
互斥量(锁的原理)为什么是原子的
互斥量为了保证自身是原子性的,为了实现这一目的,大多数体系结构提供了swap或exchange指令,这些指令用于交换两个操作数(通常是寄存器%al和内存单元mutex,寄存器的数据为线程独有的)的值。由于这些指令的执行是原子的,因此它们可以在多线程环境中安全地用于加锁和解锁操作。当%al为1和内存mutex为0时表明成功获得了锁。当线程完成对共享资源的访问并准备释放锁时,它只需将内存中的锁变量值重置为0即可。这通常可以通过简单的写入指令(而非swap或exchange指令)完成,因为此时没有其他线程会尝试获取锁。最后,多个线程达到了互斥的目的,此时,线程才是安全的,之前不加锁的时候是线程不安全的。
正确使用锁
对于临界资源,多个线程访问,进行申请锁,则每个线程需要申请的时同一个锁才有意义。如果操作不当,会产生死锁问题。
产生死锁的必要条件
- 互斥条件:一个资源每次只能被一个执行流使用,因为要使用了锁。
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放,一直独占该锁资源。
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。
破坏死锁的四个必要条件 - 避免死锁。
- 加锁顺序一致。
- 避免锁未释放的场景。
- 资源一次性分配。