锁问题的分析-有什么类型的锁,为什么需要锁,如何选择锁,死锁是怎么产生的,如何检测死锁

26 篇文章 3 订阅
本文详细探讨了线程执行中的同步机制,介绍了互斥量、自旋锁、条件变量、读写锁和原子操作等不同类型的锁,并结合实例说明了死锁检测与避免策略,以及如何根据场景选择合适的锁。还涉及了数据库锁和分布式锁的应用,以及锁在避免数据一致性问题中的关键作用。
摘要由CSDN通过智能技术生成

锁的种类
在这里插入图片描述

1.线程执行语句时的步骤:如Int a; a++的操作
实际上分为三步:
1.从内存中获取a的值暂时存放在CPU的寄存器中
2.执行++操纵
3.再放回内存之中

在此过程中可能同时有两个线程同时取a的值,导致++操作只进行一次,因此需要线程锁,在一个线程处理数据时锁住资源。

线程函数
pthread_create(&pthread_id,NULL,thread_entry,count);
pthred_id为传出参数,标识线程的唯一ID。
thread_entry为现场具体需要执行的函数。
count为thread_entry的传入参数

在内核之中没有所谓的线程,线程是有类似与task_thread类的结构体链表存储,线程资源。
猜测如下:
thread_struct{
thread_id;
retval;
flag;
thread_struct *next;
} task;

我们使用pthread_create创建多个线程,内核实际会在线程链表里面新增一个任务结构空间。
线程创建完成执行thread_entry操作,count作为entry函数的传入参数,此时内核会将对应的thread_struct结构体的flag标志位置为1.
我们调用函数pthread_join(pthread_id,NULL);
将id传入,阻塞等待对应id的线程退出,判断标准是thred_struct的flag标志位被置为0.
同理我们可以使用pthread_cancel与pthread_exit函数强行结束线程,其中第一个函数是外部线程结束该线程,而pthread_exit是自己结束自己。

以线程锁为例:
1.1互斥量
特点:等待时,当前线程会释放CPU,CPU上下文切换。

1.2自旋锁
特点:等待时,当前线程不会释放CPU,CPU不断获取锁-空转。

1.3条件变量
特点:常常与互斥量搭配使用,条件变量可以减少竞争。如果仅仅是mutex,那么,不管共享资源里有没数据,生产者及所有消费都全一窝蜂的去抢锁,会造成资源的浪费。即允许线程以无竞争的方式等待特定的条件发生。

1.4读写锁
特点:比互斥锁允许更高的并行性,互斥锁要么是锁住的状态,要么是无锁的状态,但读写锁有三种状态,读模式加锁,写模式加锁,不加锁。读模式加锁要求没有写锁存在即可,而写模式加锁,则必须要求所有读锁释放。

读锁可以理解为共享锁,写锁可以理解为排他锁(修改和删除)。

一个线程持有读锁,其他线程获取读锁能够成功,获取写锁需要进行等待。而写锁是需要所有的读锁都释放之后,写锁才能获取成功。

同理,当写锁在操作的时候,所有的读锁都需要等待。

读写锁很危险,因为很可能在业务逻辑处理过程中,场景较深,在使用读锁的时候又进行了修改数据的写的操作,导致死锁。
因此我们考虑在读多写少的场景下,使用
read-cpy-update的技术。

对于被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它,但写者在访问它时首先拷贝一个副本,然后对副本进行修改,最后使用 一个回调(callback)机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据。这个时机就是所有引用该数据的CPU都退出对共享数据的操作。

RCU实际上是一种改进的rwlock,读者几乎没有什么同步开销,它不需要锁,不使 用原子指令,而且在除alpha的所有架构上也不需要内存栅(Memory Barrier),因此不会导致锁竞争,内存延迟以及流水线停滞。不需要锁也使得使用更容易,因为死锁问题就不需要考虑了。写者的同步开销比较大,它需要 延迟数据结构的释放,复制被修改的数据结构,它也必须使用某种锁机制同步并行的其它写者的修改操作。读者必须提供一个信号给写者以便写者能够确定数据可以 被安全地释放或修改的时机。有一个专门的垃圾收集器来探测读者的信号,一旦所有的读者都已经发送信号告知它们都不在使用被RCU保护的数据结构,垃圾收集 器就调用回调函数完成最后的数据释放或修改操作。 RCU与rwlock的不同之处是:它既允许多个读者同时访问被保护的数据,又允许多个读者和多个写者同时访问被保护的数据(注意:是否可以有多个写者并 行访问取决于写者之间使用的同步机制),读者没有任何同步开销,而写者的同步开销则取决于使用的写者间同步机制。但RCU不能替代rwlock,因为如果 写比较多时,对读者的性能提高不能弥补写者导致的损失。

读者在访问被RCU保护的 共享数据期间不能被阻塞,也就说当读者在引用被RCU保护的共享数据期间,读者所在的CPU不能发生上下文切换,spinlock和rwlock都需要这 样的前提。写者在访问被RCU保护的共享数据时不需要和读者竞争任何锁,只有在有多于一个写者的情况下需要获得某种锁以与其他写者同步。写者修改数据前首 先拷贝一个被修改元素的副本,然后在副本上进行修改,修改完毕后它向垃圾回收器注册一个回调函数以便在所有读执行单元已经完成对临界区的访问进行修改操 作。

1.5原子操作
减少操作系统阻塞线程的负担,直接使用硬件级别的锁必然要比在其之上的系统调用(比如OS实现的mutex互斥锁)更节省资源。
进程锁:
如:nginx中实现的accept锁可以由自旋锁(原子操作)、互斥锁(信号量)、文件锁(以加载文件,释放文件的操作)实现,在nginx当中我们将此类锁放入共享内存之中,对共享内存之中的部分资源进行上锁。accept锁主要实现客户端与nginix之间建立连接的问题。

分布式锁:
分布式锁主要解决的问题是:有多个进程,多个服务(微服务),同时竞争一个资源时,我们需要给这个资源上的一种锁,而这个锁就涉及到公平与非公平。

有一个数据中心,多个服务器来访问这个数据中心具体资源的时候,我们也可以通过分布式锁来限制资源的访问,同时让他们之间互相不能感知到对方的存在。

多线程锁和线程锁主要还是在一个操作系统之中。
但当多个进程不在同一个系统之中时,我们就会使用到分布式锁。

数据库锁:
排他锁和共享锁都是行级锁,意向锁是表级别的锁,意向锁又包括意向排他锁,意向共享锁。
注意:排他锁和共享锁都是对某一行加锁,而意向锁则是对某几行加锁。

同时以上说的这些锁又有多种实现方式:
如:record锁
gab锁 间隔锁
next-key锁 临键锁
auto-inc锁 自增长锁

在数据库中,我们为了实现事务,从一个一致的状态,把他转变为另外一个一致的状态,为例实现这种事务状态之间的转换,我们首先需要了解事务的特性,即A(原子性)C(一致性)I(隔离性)D(持续性)特性。
而锁主要解决的是事务的隔离性,事务的隔离性是指,我们俩同时有多个事务在进行时,事务之间不能感知到对方事务的存在,可以独立的运行,因此就需要处理事务之间可能会出现的一些问题。
如:1.幻读 2.脏读 3.不可重复读
为了解决以上的问题,我们需要不同的锁来解决。
比如为了解决幻读,我们就使用gap锁(?)。
record锁,条件(聚集索引),聚集索引:表的数据是有一个索引的,通过这个索引我们可以确认表中数据的具体位置,我们称之为聚集索引。而非聚集索引则是指,我们查询某个数据时,需要全文档进行遍历。在mysql中,聚集索引使用的是B+树的特性,我们可以通过B+树的特性,快速索引出数据的位置,如:select *from t where id = 1;此时就是使用record锁,因为我们会从一个具体的位置搜索某个值。

gap锁,间隔锁,如我们的id是一个间隔包含1,3,5,7此时我们要insert i=2那么我们就需要间隔锁,将开区间(1,3)之间这个开区间给锁住。gap锁可以锁很多个区间,如(1,3)(-,1)(3,5)

next-key锁其实就是record锁+gap锁来形成的一个数据库锁,其实就是左开右闭的操作(1,3] (3,5】

2.为什么需要锁(以多线程为例)
原因:多个线程操作同一个资源时,CPU读写操作产生的时序问题。

3.使用什么样的锁
前提:把握锁的粒度粗细,锁的粒度太粗,就会出现很多的线程阻塞,等待时间边长,锁的粒度太细,那么过多的锁开销会使系统性能受到影响,代码复杂。

如提高CPU性能:减少上下文切换,使用自旋锁,但是自旋锁会出现占用CPU过久的问题,我们可以采取:自旋锁+衰减因子 -> 切换成互斥锁。(适用于操作简单的场景,如链表头插法)

互斥量(红黑树)
使用条件变量+互斥量减少资源的抢占,允许线程以无竞争的方式等待特定的条件发生。

使用原子操作(CPU指令集的支持)。

__asm__ __volatile__("InSTructiON List" : Output : Input : Clobber/Modify);
// x86指令体系
movl i, %eax                            //内存访问, 读取i变量的值到cpu的eax寄存器
addl $1, %eax                         //增加寄存器中的值
movl %eax, i                            //写入寄存器中的值到内存

4.死锁的产生
死锁的产生,比如是由于线程获取锁的顺序形成了一个环,我们可以将其理解成有向图的环。我们使用环即有向图的数据结构+dlsym(劫持库函数)的方式可以对锁进行管理。
如,使用dlsym函数进行malloc内存泄漏的检测:

#include <stdio.h>
#include <stdlib.h>
#define __USE_GNU
#include <dlfcn.h>
 
#define TEST_MEM_LEAK 1    //值为1表示加入内存泄露的检测,为0表示不加入
 
#if TEST_MEM_LEAK
 
typedef void *(*malloc_t)(size_t size);
malloc_t malloc_f = NULL;
 
typedef void (*free_t)(void *p);
free_t free_f = NULL;
 
int malloc_flag = 1;    // 用于防止重复递归无法退出,因为printf函数会调用malloc进行内存分配
int free_flag = 1;
 
void *malloc(size_t size)
{
    if(malloc_flag) {
        malloc_flag = 0;  // 用于防止printf造成递归调用malloc而出错
        printf("malloc\n");
       void *p = malloc_f(size);
        malloc_flag = 1;  // 用于保证后续再次调用本文件中malloc时flag标志的初始值一致
       return p;
    } else {
        return malloc_f(size);  // 这里调用dlsym获取的系统库中malloc函数
    }   
}
 
void free(void *p)
{
    if(free_flag) {
        free_flag = 0;
        printf("free\n");
        free_f(p);
       free_flag = 1;
    } else {
        free_f(p);
    }
}
#endif
int main(int argc, char **argv)
{
#if TEST_MEM_LEAK    // 这里if到endif之间的部分可分装成函数调用
    malloc_f = dlsym(RTLD_NEXT, "malloc");
    if(!malloc_f) {
        printf("load malloc failed: %s\n", dlerror());
        return 1;
    }
    free_f = dlsym(RTLD_NEXT, "free");
    if(!free_f) {
        printf("load free failed: %s\n", dlerror());
        return 1;
    }
#endif
    void *p1 = malloc(10);  //这里会先调用本文中的malloc函数
    void *p2 = malloc(20);
    //这里的p2未释放存在内存泄漏,通过利用查看打印的malloc与free次数是否一样来判断
    free(p1);
    return 0;
}

5.检测死锁
1.死锁将会形成一个有向图的环
2.dlsym函数dlsym(RTLD_NEXT,”pthread_lock_t”);

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

举世无双勇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值