目录
一、互斥
1.互斥的概念
2.互斥锁接口
3.线程加锁解锁本质
4.死锁
二、同步
1.同步的概念
2.条件变量
3.条件变量接口
一、互斥
1.互斥的概念
互斥指的是任何时刻,互斥保证有且只有一个执行流进入临界区,进行临界资源的访问,通常对于临界资源起到保护作用的一种机制。
在多线程下,访问全局变量的话,一个线程的修改会影响到其他线程,如果说一些操作不是原子的话,会有数据不一致的问题。例如,多线程下抢电影票的情况,本来有count:100张票,但是最后可能卖出去100多张,是因为count--操作不是原子的。
当执行count--操作的时候,转化为汇编操作有3步,第一步是将count的值读取出来放入CPU寄存器中,第二步执行--操作,第三步将count的值覆盖会物理内存的位置,改变count在物理内存中的值。但是如果说在第二步执行完之后,该线程的时间片到了,就会从CPU上剥离下来,本来取出来的是10,在CPU中已经修改为9了,但是没有写回到内存中,所以内存的count值还是10,那么别的线程就会读取到count值是10,之后在CPU中在进行操作,并写回物理内存的值也为9。等到第一个修改为9的进程再次回到CPU中执行时,会执行第三步操作,将9有一次的写回到了物理内存当中。此时count的值是多少都不知道,可能已经减为0了,但是又被写回了9,所以说不是原子的操作在多线程下会有很大的问题。
又或者是多线程下一个变量充当if判断条件的时候,可能刚读取到该变量,时间片就到了,没有来得及进行判断,就被剥离了下来,其他的线程继续操作,最终把变量改为了不满足if判断条件了,本来该线程应该判断后退出的,但是该线程已经取出了该条件变量,是最开始的值,所以还是会判断符合条件执行if内部代码,所以说也会出错的。
数据在内存是被多线程共享的,但是一旦读取到寄存器当中,就变成了线程的上下文数据,就属于私有了。所以互斥就是为了解决上述的数据不一致问题,就是同一时间只让一个线程进行访问该资源。
2.互斥锁接口
上述的问题可以使用互斥锁来解决,锁是用来保护临界区在同一时间只能由一个进程进行访问的。锁相当于一个资源,多线程去竞争该资源,但是只有一个线程可以获取到该资源,其他线程就会阻塞在锁的位置等待锁资源,获取到锁资源的线程才可以去访问临界区的资源。当线程执行完临界区的操作之后,释放锁,其他线程才可以继续竞争锁资源。
全局锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
创建局部锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
第一个参数是要初始化的锁对象,第二个参数是设置锁的属性,一般设置为nullptr即可。
销毁局部锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
对于pthread_mutex_lock加锁是阻塞式的申请锁资源,如果没有申请到的话,则会阻塞等待锁资源,而pthread_mutex_trylock则是尝试申请锁资源,也就是非阻塞式的申请,如果有的话就申请到了,没有的话就不申请了,直接返回。
3.线程加锁解锁本质
多线程去申请锁资源,锁保证了线程之间的互斥,但是锁也是共享资源啊,谁来保证锁的安全性呢?所以要和信号量一样,对于申请锁的操作设置为原子的。该原子的操作是基于硬件指令的支持,比如说比较并交换的CAS等指令就是一步执行的操作操作。把申请锁的操作转换为原子的汇编指令就保证了申请锁的原子性。
pthread_mutex_lock()接口的加锁实现:
lock:
movb %0, %al
xchgb %al, mutex
if(al寄存器 > 0) return 0;
else 挂起等待
go to lock;
首先将al寄存器的内容置为0,之后xchgb交换al寄存器和mutex标记位中的值,当有锁资源的时候mutex的标记位是1,没有的时候是0。xchagb就是一种原子性的操作指令,也就是swap操作。 交换之后判断是否申请到了锁资源,也就是判断al寄存器交换过来的数据是不是1,如果是1的话证明获取了锁资源就可以执行访问临界区的代码了,如果没有获取到,那么就会进入else阻塞的等待,如果有锁资源了,会跳转到lock开头重复上述动作进行申请锁资源。
当有一批线程申请该锁资源的时候,第一个进行swap交换的线程会将mutex的标记位交换位0,而1则是进入了该线程的al寄存器中,判断al寄存器大于0跳出循环,执行临界区代码。此时锁的标记位就是0了,其他线程来了之后,再怎么交换al寄存器中的值都是0,所以都需要进行阻塞等待。
当线程调度切换的时候,也不会有影响,其他线程来的时候还是要阻塞等待,因为标志位1被申请到锁资源的线程通过切换上下文数据而带走了,只有等到该线程重新被CPU调度,执行完临界区代码,将标志位1交换会mutex,别的线程才能交换到标志位为1,才能让al寄存器大于0,从而获得到锁资源,返回执行临界区代码。
也就是说锁资源的标志位,就相当于一把钥匙,也只要一把钥匙,那么一个人拿走该钥匙之后,不管什么情况,当这个人没用完之前,没把钥匙换回来之前,其他人都打不开这个门,都需要在门口进行等待。
pthread_mutex_unlock()接口的解锁实现:
unlock:
movb $1, mutex
唤醒阻塞等待的进程
return 0;
解锁的操作,就是将标志位的1交换会锁资源中,然后唤醒等待的线程去竞争锁资源。
4.死锁
死锁指的是一组线程中,各个线程均占有不会被释放掉的资源,但又因相互申请被其他线程锁占用不会释放的资源而处于一种持久等待的状态,称之为死锁。例如下面的图所示,有两个资源A和B,1号线程占有A资源,2号线程占有B资源,而两者又互相想要对方的资源,所以都在互相等待。
一个线程也可以产生死锁,可以让一个进程对同一个锁资源重复申请两次就变成了拥有了该资源还在申请该资源的情况,就变为了死锁。
那么死锁如何避免呢?首先死锁的产生拥有四个条件:互斥条件、请求与保持条件、不剥夺条件、循环等待条件。请求与保持指的是一个执行流因请求资源而阻塞的时候,对以获得的资源保持不放,并且还在一直申请资源。循环等待指的是若干个执行流之间形成了一种头尾详解的循环等待资源的关系。
解决死锁的方式就是破坏其中一个条件即可。例如:不适用锁;在申请锁资源不成功的时候,把自己拥有的资源释放掉;强行将别人的资源抢占过来;按照同样的顺序申请锁资源,就是对于多个资源的申请做一个先后顺序,先有A资源,才可以申请B资源,而不是所有资源都可以无条件的申请。
二、同步
1.同步的概念
同步指的是在临界资源使用安全的情况下,让多线程的执行具有一定的顺序性,能够较为充分的利用资源。对于竞争锁资源谁竞争到,我们是无法控制的,但是我们可以通过一些方式来规定谁可以去竞争锁资源来实现线程访问临界资源具有一定的顺序性。
2.条件变量
条件变量是一种用于线程同步的机制。它主要用于让线程在某个条件满足之前等待,当条件满足时,线程可以被唤醒并继续执行。在多线程编程环境中,条件变量提供了一种高效的方式来协调线程之间的执行顺序和资源访问。通常与互斥锁配合使用。互斥锁用于保护共享资源的访问,而条件变量用于线程的等待和唤醒操作。
简单可以理解为他内部有一个标记位和一个线程等待队列,当一个线程申请到锁资源的时候,会访问临界区的代码,此时我们在临界区代码中判断条件变量是否就绪,如果说就绪的话,条件变量的标记位为1,就可以让申请到锁的进行继续访问临界区资源,如果说没有准备就绪的话,就把该线程链入到线程等待队列中,同时释放锁资源。当条件准备就绪之后,会唤醒等待队列的一个或多个进程继续竞争锁资源,竞争到锁资源的线程就可以访问临界区资源了。
下面举一个场景,有两个线程和一个缓冲区临界资源,一个线程为写入线程,一个线程为读取线程。那么显而易见,需要先写入后读取,那么这就是线程之间访问临界资源的顺序,也就是线程的同步策略。那么如何实现呢?用一把锁和一个条件变量去实现,该条件变量关联的条件就是缓冲区中有数据。当读取进程申请到锁的时候,会进入条件变量的判断,因为条件变量没有就绪,所以会就读写进程链入到线程等待队列中,释放锁资源。那么写入线程就会竞争到锁资源,进行写入,写入之后,会调用条件变量就绪接口并唤醒等待该条件变量的等待队列中的线程,之后释放锁资源。那么读取线程就可以继续参与锁的竞争了,当获取到锁资源后,就可以读取临界区中的资源数据了。这就是使用条件变量和锁实现的线程同步。
3.条件变量接口
全局条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
条件变量创建
int pthread_cond_init(pthread_cont_t* cond, const pthread_condattr_t* attr);
条件变量销毁
int pthread_cond_destroy(pthread_cond_t* cond);
等待条件变量
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);
唤醒等待队列中的线程
int pthread_cond_signal(pthread_cond_t* cond); //唤醒队列头部的一个线程
int pthread_cond_broadcast(pthread_cond_t* cond); //唤醒所有线程
接口的使用代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>//全局锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//全局条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;void* handler(void* arg)
{while(true){//申请锁资源pthread_mutex_lock(&mutex);//等待条件变量就绪pthread_cond_wait(&cond, &mutex);//临界区std::cout << pthread_self() << "-thread entry critical section" << std::endl;//释放锁资源pthread_mutex_unlock(&mutex);}
}int main()
{//创建线程for(int i = 0; i < 5; i++){pthread_t tid;pthread_create(&tid, nullptr, handler, nullptr);//线程分离--无需等待pthread_detach(tid);}//反复让条件变量就绪,唤醒一个线程while(true){pthread_cond_signal(&cond);sleep(1);}return 0;
}
对于在条件变量等待队列被唤醒的线程,也要重新参与锁的竞争,但是并非是跳到申请锁的那行代码,而是说该pthreat_cont_wait函数内部也是有申请锁的代码的。