文章目录
1. 如何实现线程的加锁和解锁
经过上一篇文章的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,如果我们能让它变成一条汇编语句那么就是原子的。
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。
lock的伪代码:
这里的%al是CPU里的寄存器,mutex是内存中的一个变量。
现在有一个问题,当有多个线程一起执行第一句move时(意思是将0移到寄存器里),线程2的0会覆盖线程1的0吗?线程3的0会覆盖线程2的0吗?
答案是:不会的。因为在寄存器中的数据,全部都是线程内部的上下文。多个线程看起来同时访问寄存器,但是互不影响。因为只有当某一个线程从CPU切走,另外一个线程才能被调度。
过程如下:
假设内存中的mutex变量里面的值是1,当线程1来加锁时,如果执行完第一句move后,就被切走了,线程2来执行。
那么线程2来执行move后,将0放进寄存器中:
然后xchgb一步将0和1进行交换:
那么此时如果线程2被切走,又来个优先级更高的线程3呢?
还是一样,线程2把寄存器上下文带走,线程3把0移到寄存器中,一步交换寄存器和内存mutex值,不过寄存器里面的值还是0。再后面if判断的时候,不会加锁,而是挂起。其它线程也是一样的道理。
unlock的伪代码:
解锁就简单了,只需要将1写回到内存就可以了。
本质:将数据从内存读入寄存器,也就是将数据从共享变成线程私有。
2. 封装一个锁
我们在这里锁的四步封装成一个类。
这里就是我们传一个锁进行加锁和解锁的类。
我们先全局声明自己的锁,声明的时候就初始化。
这是抢一次票的函数,我们在临界区是定义一个局部对象来完成加锁。当一次票就在析构的时候自动解锁。这就是RAII思想。
这是我们的多次抢票,当抢不到票时就break。
从运行结果可以看出:可以完成加锁和解锁操作。
那么以后我们可以这样去使用:
int cnt = 10000;
int main()
{
//代码块
{
//临界资源
LockGuard LockGuard(&mymutex);
cnt++;
...
...
...
}
}
3. 可重入和线程安全
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
像上面抢票的例子,这个函数就是被重入了,因为线程安全的问题,需要加锁。而函数里的局部变量,每个线程各自一份,则不需要加锁。
常见的线程不安全的情况:
1.不保护共享变量的函数。
2.函数状态随着被调用,状态发生变化的函数。
3.返回指向静态变量指针的函数。
4.调用线程不安全函数的函数。
常见的线程安全的情况:
1.每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。
2.类或者接口对于线程来说都是原子操作。
3.多个线程之间的切换不会导致该接口的执行结果存在二义性。
常见不可重入的情况:
1.调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的。
2.调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
3.可重入函数体内使用了静态的数据结构。
常见可重入的情况:
1.不使用全局变量或静态变量。
2.不使用用malloc或者new开辟出的空间。
3.不调用不可重入函数。
4.不返回静态或全局数据,所有数据都有函数的调用者提供。
5.使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
3.1 可重入与线程安全联系
1.函数是可重入的,那就是线程安全的。
2.函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题。
3.如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
3.2 可重入与线程安全区别
1.可重入函数是线程安全函数的一种。
2.线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数,若锁还未释放则会产生死锁,因此是不可重入的。
4. 常见锁概念
4.1 死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其它进程所占用不会释放的资源而处于的一种永久等待状态。
4.2 代码实现
我们让两个线程执行不同的回调函数,让线程1先申请A锁,让线程B先申请B锁。线程1再申请B锁,线程2再申请A锁都会阻塞,造成死锁。
我们从运行结果可以看到,处于一种阻塞的状态。
那么一把锁可以造成死锁吗?
如果我们加过锁后,又继续加锁,就可能会导致死锁。
while (true)
{
pthread_mutex_lock(&mutex);
cout << name << " count : " << cnt-- << endl;
pthread_mutex_lock(&mutex);
sleep(1);
}
4.3 死锁四个必要条件
1.互斥条件:一个资源每次只能被一个执行流使用。
2.请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
3.不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
4.循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。
如何避免死锁呢?
1.破坏死锁的四个必要条件。
2.加锁顺序一致。
3.避免锁未释放的场景。
4.资源一次性分配。