在程序设计的过程中,使用读写锁功能出现了一种死锁场景,现象是对读写锁unlock了两次,导致的死锁,于是带着问题研究了下读写锁的使用,下面为大家介绍下我解决问题过程中总结的笔记。
一、读写锁简介
1、为什么需要读写锁?
假设,大多数人都是查询火车票的余票数量,只有少量的人是购买火车票。比方说同时有 99 个人查访余票,而只有 1 个人买票……如果此时用互斥量,就会显的效率非常低了,比方有任何一个人在查票的时候,其它 98 个想查票的人只能等待,造成时间浪费。为了提高读并发性,也就是说允许多个人同时查票。
当有人查票的时候,其他查票的人可以查票,但买票的人无法购买。
当有人买票的时候,其他买票和查票的人都需排队。
简言之:查共享、买互斥,这其实就是一种读写锁机制。这种机制下,相对于一直互斥的情况,该机制极大提高了效率。
2、读写锁的作用
读数据的频率远大于写数据的频率的应用中。这样可以在任何时刻运行多个读线程并发的执行,给程序带来了更高的并发度。
适合场景:有大量线程频繁的读取数据,只有少量的的线程对数据进行修改
3、读写锁的基本特征
当读写锁是写加锁状态时, 在这个锁被解锁之前,所有试图对这个锁加锁的线程(当前线程除外)都会被阻塞。
当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是如果线程希望以写模式对此锁进行加锁,它必须直到所有的线程释放锁。
4、读写锁函数介绍
头文件:#include<pthread.h>
(1)初始化函数pthread_rwlock_init()
功能:初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *rwlock, \ const pthread_rwlockattr_t *attr);
参数说明:
rwlock:是要进行初始化的读写锁。
attr:是rwlock的属性,决定读优先和写优先的变量,为null时,默认为读优先。
ps:此参数一般不关注,可设为NULL,后面再介绍读写锁源码后给出这个参数非赋值方法。
(2)销毁函数pthread_rwlock_destroy()
功能:销毁初始化的锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
参数说明:
rwlock:是需要进行销毁的读写锁。
(3)加锁和解锁
在进行读操作的时候加的锁:
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);//阻塞性
int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);//非阻塞性
在进行写操作的时候加的锁:
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);
对读/写统一进行解锁:
int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);
读写锁的一些简单使用方法这里不在进行演示,读者可以自行找一下简单的使用方法。
二、读写锁一种死锁案列分析
在开发调试代码的过程中,细节就不说了,具体现象就是,作者使用的读写锁进行了unlock两次的操作,直接导致后续其他线程访问不到读写锁了,一直获取读写锁失败,即造成了死锁现象。
具体,可以通过一个测试程序来说明存在这种问题的。
测试程序说明:1、封装读写锁函数,便于打印加锁成功失败日志。2、开个线程,先加写锁,再连续解两次锁,主线程尝试获取锁,看结果。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void fun_rdlock(pthread_rwlock_t *lock)
{
int ret = 0;
ret = pthread_rwlock_rdlock(lock);
if(ret)
{
printf("pthread_rwlock_rdlock lock error, ret:%d\n",ret);
}
else
{
printf("pthread_rwlock_rdlock lock success \n");
}
}
void fun_wrlock(pthread_rwlock_t *lock)
{
int ret = 0;
ret = pthread_rwlock_wrlock(lock);
if(ret)
{
printf("pthread_rwlock_wrlock lock error ret:%d\n",ret);
}
else
{
printf("pthread_rwlock_wrlock lock success \n");
}
}
void fun_unlock(pthread_rwlock_t *lock)
{
if(pthread_rwlock_unlock(lock))
{
printf("pthread_rwlock_unlock unlock error\n");
}
else
{
printf("pthread_rwlock_unlock unlock success\n");
}
}
void fun_test(pthread_rwlock_t *lock)
{
printf("fun_test start lock\n");
fun_wrlock(lock);
fun_unlock(lock);
}
void* thread_test(void *arg)
{
printf("thread_test start\n");
printf("thread_test start lock\n");
fun_wrlock(&test_lock);
fun_unlock(&test_lock);
fun_unlock(&test_lock);
printf("thread_test end lock\n");
}
pthread_rwlock_t test_lock;
int main()
{
int ret = 0;
pthread_t pid;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
ret = pthread_create(&pid,NULL,thread_test,NULL);
if(ret)
{
printf("pthread_create failed,ret:%d\n",ret);
return 1;
}
sleep(1);
fun_test(&test_lock);
pthread_rwlock_destroy(&test_lock);
return 0;
}
下面是程序运行结果:
[root@localhost wang]# ./test
thread_test start
thread_test start lock
pthread_rwlock_wrlock lock success
pthread_rwlock_unlock unlock success
pthread_rwlock_unlock unlock success
thread_test end lock
fun_test start lock
^C
程序卡住了,即出现了死锁。
读写锁加锁函数有四种分别是:
pthread_rwlock_rdlock、
pthread_rwlock_tryrdlock、
pthread_rwlock_wrlock、
pthread_rwlock_trywrlock。
由于并不知道读写锁的具体实现原理,通过多次执行二次解锁操作后,无论刚开始加读锁还是写锁,二次解锁后效果一样,发现一些规律:
1)、先加一次读/写锁后,解锁2次,再次加读锁会失败,如果再次解锁一次,则能加读锁成功。
2)、先加一次读/写锁后,解锁2次,再次加写锁会失败,再次解锁,加写锁失败。
上述规律实质就是读写锁已经初始化后,当前并无读、写操作持有锁,在这个基础上进行解锁,就会导致死锁。后续再次解锁,能加上读锁,则需要通过分析源码才能得知。
三、读写锁源码分析
读写锁的源码来至于glibc下读写锁源文件,读者可以自行搜索下载。
3.1、读写锁结构体定义:
struct
{
int __lock;
unsigned int __nr_readers; //当前读锁个数
unsigned int __readers_wakeup; //读唤醒
unsigned int __writer_wakeup; //写唤醒
unsigned int __nr_readers_queued; //读请求
unsigned int __nr_writers_queued; //写请求
int __writer; //当前写锁持有者,不为0表示当前线程tid
int __shared;
unsigned long int __pad1;
unsigned long int __pad2;
/* FLAGS must stay at this position in the structure to maintain
binary compatibility. */
unsigned int __flags; //加读锁时,读、写优先标记
} __data;
这里需要注意的是,当前读锁个数变量类型为unsigned。
3.2、读写锁的解锁函数
int
__pthread_rwlock_unlock (pthread_rwlock_t *rwlock)
{
LIBC_PROBE (rwlock_unlock, 1, rwlock);
lll_lock (rwlock->__data.__lock, rwlock->__data.__shared);
if (rwlock->__data.__writer)
rwlock->__data.__writer = 0;
else
--rwlock->__data.__nr_readers;
if (rwlock->__data.__nr_readers == 0)
{
if (rwlock->__data.__nr_writers_queued)
{
++rwlock->__data.__writer_wakeup;
lll_unlock (rwlock->__data.__lock, rwlock->__data.__shared);
lll_futex_wake (&rwlock->__data.__writer_wakeup, 1,
rwlock->__data.__shared);
return 0;
}
else if (rwlock->__data.__nr_readers_queued)
{
++rwlock->__data.__readers_wakeup;
lll_unlock (rwlock->__data.__lock, rwlock->__data.__shared);
lll_futex_wake (&rwlock->__data.__readers_wakeup, INT_MAX,
rwlock->__data.__shared);
return 0;
}
}
lll_unlock (rwlock->__data.__lock, rwlock->__data.__shared);
return 0;
}
读写锁解锁函数功能:
(1)如果当前写持有锁,写持有锁变量清0。写锁只能一个线程持有。
(2)如果读持有锁,读持有锁变量减1。读锁可以多个线程持有。
(3)唤醒其他线程的写、读请求。
其实,根据源码,我们从第二条就可以看出,二次解锁问题的本质是:
在当前没有任何线程持有锁的情况下,进行解锁时,源码会进入0–的逻辑,导致读锁持有者达到了unsigned的最大值,由于上限处理,这样导致后续的读锁加不上,由于读锁占用,写锁自然用于也加不上了。这样便导致了死锁。
3.3、读加锁函数的源码实现:
int
__pthread_rwlock_rdlock (rwlock)
pthread_rwlock_t *rwlock;
{
int result = 0;
LIBC_PROBE (rdlock_entry, 1, rwlock);
/* Make sure we are alone. */
lll_lock (rwlock->__data.__lock, rwlock->__data.__shared);
while (1)
{
/* Get the rwlock if there is no writer... */
if (rwlock->__data.__writer == 0
/* ...and if either no writer is waiting or we prefer readers. */
&& (!rwlock->__data.__nr_writers_queued
|| PTHREAD_RWLOCK_PREFER_READER_P (rwlock)))
{
/* Increment the reader counter. Avoid overflow. */
if (__builtin_expect (++rwlock->__data.__nr_readers == 0, 0))
{
/* Overflow on number of readers. */
--rwlock->__data.__nr_readers;
result = EAGAIN;
}
else
LIBC_PROBE (rdlock_acquire_read, 1, rwlock);
break;
}
/* Make sure we are not holding the rwlock as a writer. This is
a deadlock situation we recognize and report. */
if (__builtin_expect (rwlock->__data.__writer
== THREAD_GETMEM (THREAD_SELF, tid), 0))
{
result = EDEADLK;
break;
}
/* Remember that we are a reader. */
if (__builtin_expect (++rwlock->__data.__nr_readers_queued == 0, 0))
{
/* Overflow on number of queued readers. */
--rwlock->__data.__nr_readers_queued;
result = EAGAIN;
break;
}
int waitval = rwlock->__data.__readers_wakeup;
/* Free the lock. */
lll_unlock (rwlock->__data.__lock, rwlock->__data.__shared);
/* Wait for the writer to finish. */
lll_futex_wait (&rwlock->__data.__readers_wakeup, waitval,
rwlock->__data.__shared);
/* Get the lock. */
lll_lock (rwlock->__data.__lock, rwlock->__data.__shared);
--rwlock->__data.__nr_readers_queued;
}
/* We are done, free the lock. */
lll_unlock (rwlock->__data.__lock, rwlock->__data.__shared);
return result;
}
读锁作用:
(1)如果当前无写锁,且没有设置读优先,如果有写请求,则读请求等待。如果设置了读优先,即使当前有写请求,也会加读锁成功。
(2)读锁持有者数目的上限处理
(3)同一线程不允许加锁
(4)读请求者数目上限处理
(5)当前无法加锁,读请求等待唤醒
3.4、写加锁函数的源码实现:
int
__pthread_rwlock_wrlock (rwlock)
pthread_rwlock_t *rwlock;
{
int result = 0;
LIBC_PROBE (wrlock_entry, 1, rwlock);
/* Make sure we are alone. */
lll_lock (rwlock->__data.__lock, rwlock->__data.__shared);
while (1)
{
/* Get the rwlock if there is no writer and no reader. */
if (rwlock->__data.__writer == 0 && rwlock->__data.__nr_readers == 0)
{
/* Mark self as writer. */
rwlock->__data.__writer = THREAD_GETMEM (THREAD_SELF, tid);
LIBC_PROBE (wrlock_acquire_write, 1, rwlock);
break;
}
/* Make sure we are not holding the rwlock as a writer. This is
a deadlock situation we recognize and report. */
if (__builtin_expect (rwlock->__data.__writer
== THREAD_GETMEM (THREAD_SELF, tid), 0))
{
result = EDEADLK;
break;
}
/* Remember that we are a writer. */
if (++rwlock->__data.__nr_writers_queued == 0)
{
/* Overflow on number of queued writers. */
--rwlock->__data.__nr_writers_queued;
result = EAGAIN;
break;
}
int waitval = rwlock->__data.__writer_wakeup;
/* Free the lock. */
lll_unlock (rwlock->__data.__lock, rwlock->__data.__shared);
/* Wait for the writer or reader(s) to finish. */
lll_futex_wait (&rwlock->__data.__writer_wakeup, waitval,
rwlock->__data.__shared);
/* Get the lock. */
lll_lock (rwlock->__data.__lock, rwlock->__data.__shared);
/* To start over again, remove the thread from the writer list. */
--rwlock->__data.__nr_writers_queued;
}
/* We are done, free the lock. */
lll_unlock (rwlock->__data.__lock, rwlock->__data.__shared);
return result;
}
写锁作用:
(1)如果当前无写锁且无读锁,则将当前线程tid赋给写锁持有者
(2)同一线程不允许加锁
(3)写请求者数目上限处理
(4)当前无法加锁,写请求等待唤醒
其他函数,如pthread_rwlock_init、pthread_rwlock_destroy、pthread_rwlock_tryrdlock、pthread_rwlock_trywrlock函数不做介绍,相对上述函数相对简单,有兴趣的可以通过glibc库来找源码自己看一下。
下面说一下读写锁的特征和注意事项:
四、读写锁特征总结:
1、当读写锁是写加锁状态时, 在这个锁被解锁之前,所有试图对这个锁加锁的线程(当前线程除外)都会被阻塞。
2、当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是如果线程希望以写模式对此锁进行加锁,它必须直到所有的线程释放锁。
3、当前是写锁持有时,持有期间,其他线程既有读请求也有写请求,这些请求均处于阻塞中,解锁后,优先加写锁
4、当前是读锁持有时,持有期间,其他线程既有读请求也有写请求,写请求是否会阻碍后续的读请求?
当前是读锁时,写请求前面的读请求能直接加上读锁,遇到写请求后:
1)如果读写锁初始化时,pthread_rwlock_init(&rwlock,NULL);第二个参数为NULL,则默认为读请求优先,
则写请求阻塞,写请求后面的读请求能正常加锁,这样有可能会导致写请求一致获取不到锁。
2)如果读写锁初始化时,pthread_rwlock_init(&rwlock,*);第二个参数有指定写请求优先,则写请求后面的读请求会被阻塞。
设置写请求优先的方式:
第二个参数赋值方式如下:
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
pthread_rwlockattr_setkind_np(&attr,PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
pthread_rwlock_init(&rwlock,&attr);
其中,pthread_rwlockattr_setkind_np可以设置读、写请求优先级别。具体可选如下枚举类型:
enum
{
PTHREAD_RWLOCK_PREFER_READER_NP,//读优先
PTHREAD_RWLOCK_PREFER_WRITER_NP,//写优先,但无效
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP,//写优先,源码判断写请求优先的比较对象
PTHREAD_RWLOCK_DEFAULT_NP = PTHREAD_RWLOCK_PREFER_READER_NP//默认读优先
};
五、读写锁使用的注意事项:
1、程序开子进程与读写锁的关系
子进程会继承父进程及其线程中的资源,包括读写锁及其状态,后续子进程对读写锁的操作不影响主进程的读写锁使用。
1 )开子进程前,如果程序已加锁,由于不确定是写锁还是读锁,且读锁个数未知,这时候子进程获取的锁是危险的,无法正常使用,建议:开进程前,确保读写锁没有被占用,即解锁状态。
2)开子进程前,如果读写锁没有被持有,子进程对读写锁进行加锁,修改变量不影响主进程的值。
总结: 读写锁只适用当前进程的数据保护,子进程中的读写锁与父进程中的读写锁无法对相同数据进行保护。
2、读写锁的顺序,队列还是抢占。
从源码看不出如何实现唤醒顺序,但测试发现:
1)当前读或写持有锁时,其他线程的写请求处于阻塞中,当前线程解锁时,其他线程写请求有序加锁。
2)当前写锁持有锁时,其他线程的读请求处于阻塞中,解锁时,读请求无序加锁。
关于唤醒机制顺序问题,有兴趣的可以研究一下。
3、读写锁的另一种死锁场景
线程A加了读锁,线程B写请求处于阻塞,如果设置了写请求优先,这时线程A又来了一个读请求,则线程A处于阻塞中,导致死锁。
建议:每个线程最好只持有锁一次,这一点主要体现在读锁上,同一线程,最好不要加读锁超过一次,一定要先解锁了,再加锁。
六、二次解锁场景的预防:
1、提升编程能力,避免这种场景。
2、程序如果需要设计这种异常处理的话
方式1:封装读写锁函数,新函数内引入互斥锁,用一个全局变量来表示当前当前锁的数量,通过这种方式来避免0–的操作,出现0就不能再解锁。
方式2:维护一个表,即每个线程根据自己的tid作为key,用来唯一标识,然后当前线程有无使用读写锁用来作为value,通过这个键值对的维护,当线程加锁的时候,可以查这个表,看当前线程是否已经获得锁,如果已经持有锁,则不能加锁。解锁的时候,看是否已经持有锁,持有可以解锁,没有持有则不能进行解锁。
该方式契合了需注意事项中3的要求,避免了同一线程加读锁超过一次的问题,该方式处理最优。