锁的基本知识总结

1 概述

因为存在多线程编程和多核并发,会出现对同一块代码段(临界区)的访问,并发访问一个资源通常是不安全的,因此就需要避免在临界区中出现并发访问的情况。

避免并发和防止竞争条件就叫同步。

1.1 同步和异步

同步交互:指发送一个请求,需要等待返回,然后才能发送下一个请求,有个等待过程。

异步交互:指发送一个请求,不需要等待返回,随时可以发送下一个请求,即不需要等待。

区别以及分辨方法:一个需要等待,一个不需要等待。

2 常用锁及使用方法

2.1 原子变量及使用

CPU会对汇编指令做出特殊操作,保证对原子变量对操作属于原子操作。

注意:使用结构体进行定义

atomic_t      stTotalSessNum;    //内核态定义

atomic32_t    stTotalSessNum;     //用户态定义

 

内核中使用关键字volatile修饰整型变量实现原子操作:

typedef  struct  { volatile  int  counter;}  atomic_t;

 

volatile:变量如果加了volatile修饰,则会从内存中重新装载内容,而不是直接从寄存器中拷贝内容。

volatile应用比较多的场合:中断服务程序和cpu相关寄存器的定义

 

内核态和用户态都有原子操作,参考:

 

2.2 自旋锁

内核态:

spin_lock_init

spin_lock(&pstTcpCheck->stClock);

···

spin_unlock(&pstTcpCheck->stClock);

 

用户态:

#inlcude <pthread.h>

typedef struct tagAdjSafeDTQ
{

    DTQ_HEAD_S            stList;

    pthread_spinlock_t    stLock;

} ADJ_SAFE_DTQ_S;

初始化:(VOID)pthread_spin_init(&pstSafeDtq->srLock, PTHREAD_PROCESS_PRIVATE);

加锁:(VOID)pthread_spin_lock(&pstSafeDtq->stLock);

解锁:(VOID)pthread_spin_unlock(&pstSafeDtq->stLock);

 

特点:一旦锁被一个线程使用,其它线程获取锁时就会忙循环来等待(不会把CPU切出去,会一直占用)

对于自旋锁来说,它只需消耗很少的资源来建立锁;随后当线程被阻塞时,它就会一直重复检查看锁是否可用了,也就是说当自旋锁处于等待状态时它会一直消耗CPU时间。

如果初始化spin_lock的线程设置第二个参数为PTHREAD_PROCESS_SHARED,那么,该spin_lock不仅被初始化线程所在的进程中所有线程看到,还可以被其它进程中的线程看到,PTHREAD_PROCESS_PRIVATE则只被同一进程中的线程看到。如果不设置该参数,默认为后者。

我们的代码中较多的用来锁单个表项或整条链、session、hash等。单个表项时防止表项的读写竞争;整条链时避免添加、删除的并发。

 

2.3 互斥锁

用户态:

#include    <pthread.h>

初始化:(VOID)pthread_mutex_init(&g_stARPRealTimeQueLock,    NULL);    //初始化之前,需要将其所在内存清零;第二个参数为新建互斥锁的属性,如果为空表示使用默认属性(快速互斥锁)

加锁:(VOID)pthread_mutex_lock(&g_stARPRealTimeQueLock);

解锁:(VOID)pthrad_mutex_unlock(&g_stARPRealTimeQueLock);

删除:(VOID)pthread_mutex_destroy(&g_stARPRealTimeQueLock);

 

特点:当线程尝试获取锁时,如果在被其它线程使用,该线程休眠(可以把cpu切出去),直到之前的线程解除对互斥量的锁定,被休眠的线程被唤醒继续执行。

 

对于互斥锁来说,与自旋锁相比,它需要消耗大量的系统资源来建立锁;随后当线程被阻塞时,线程的调度状态被修改,并且线程被加入等待线程队列;最后当锁可用时,在获取锁之前,线程会被从等待队列中取出并更改期调度状态;但是,在线程被阻塞期间,它不消耗cpu的资源。

使用场景:

自旋锁和互斥锁适用于不同的场景:自旋锁适用于那些只需要阻塞很短时间的场景;而互斥锁适用于那些可能会阻塞很长时间的场景。

 

2.4 读写锁

用户态:

#include   <pthread.h>

初始化:(VOID)pthread_rwlock_init(&stFwGroupNameHash.rwGroupNameHashLock,    NULL);

加写锁:(VOID)pthread_rwlock_wrlock(&(stFwGroupNameHash.rwGroupNameHashLock));    //对读写锁加写锁

加读锁:(VOID)pthread_rwlock_rdlock(&(stFwGroupNameHash.rwGroupNameHashLock));    //对读写锁加读锁

解锁:(VOID)pthread_rwlock_unlock(&(stFwGroupNameHash.rwGroupNameHashLock));    //释放读锁与写锁

销毁:(VOID)pthread_rwlock_destroy(&(stFwGroupNameHash.rwGroupHashLock));

 

特点:同一时刻,只有一个线程可以获得写锁,同一时刻可以有多个线程获得读锁。

写锁状态时,试图加读锁或者写锁都会被阻塞

读锁状态时,试图加写锁时,后续其它线程的读锁会被阻塞(写者如果试图获取锁,只有当前没有读者或写者,才能立即获得锁)

 

注意:1、加锁解锁函数中,是有返回值的调用成功则返回0,失败就返回错误码;2、获取锁的两个函数都是阻塞操作的,也就是获取不到锁的话,那么调用线程不是立即返回,而是阻塞进行。

 

非阻塞的读写锁:

int pthread_rwlock_trywrlock(pthread_rwlock_t    *rwptr);

int pthread_rwlock_tryrdlock(pthread_rwlock_t    *rwptr);

例:

iRet = pthread_rwlock_tryrwlock(&g_stDiagHashLock);
if (0 != iRet)
{
    /* 有人正在持有,等待下一个周期再获取 */
    return;
}   

 

2.5 RCU锁

内核态和用户态都有

RCU中允许多个读者和多个写者同时访问被保护的资源。写者的同步开销取决于使用的写者间同步机制,RCU并不对此进行支持。

读者不需要使用锁,要访问资源尽管访问好了。写者的同步开销比较大,要等到所有的读者都访问完成了才能够对被保护的资源进行更新。

写者修改数据前,首先拷贝一个被修改元素的副本,然后再副本上进行修改,修改完毕后它向垃圾回收器注册一个回调函数,以便在适当的时间真正的执行修改操作。读者必须提供一个信号给写者以便写者能够确定数据可以被安全的释放或修改的时机。(有一个专门的垃圾收集器来探测读者的信号,一旦所有的读者都已经发送信号告知他们都已经不再使用被RCU保护的数据结构,垃圾收集器就调用回调函数完成最后的数据释放或修改操作)

 

2.6 中断

内核态有

中断是指计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续执行。

中断处理程序:响应中断时,内核执行的程序就是中断处理程序

上半部:中断处理程序时上半部。接收到一个中断,立即开始执行,但只做有严格时限的工作。

下半部:能够被允许稍后完成的工作。可以被推迟,在合适的时机去执行。(实现方式:软中断,tasklet,工作队列)

举例:网卡收到网络的数据包:1、需要通知内核接受数据报文,从而优化网络吞吐量和传输时期,避免超时:属于重要紧迫和硬件相关的工作,属于上半部;2、在内核中对收到的数据包的处理:可以不马上执行,属于下半部。

 

2.7 内存屏障

内核态和用户态都有:

smp_mb();

 

内存屏障其实就是因为编译器优化和CPU对寄存器、cache的使用,导致对内存的操作不能够及时的反应出来,比如cpu写入后,读出来的值可能是旧的内容。

 

2.8 信号量

如果一个任务试图获得一个已经被占用的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。这样处理器可以重获自由,去执行其它代码。当持有信号量的进程将信号量释放后,处于等待队列中的任务被唤醒,并获得该信号量。

和用户态的互斥锁很像。

猜你喜欢

转载自blog.csdn.net/BaiFeng303/article/details/81430961