linux下读写锁unlock导致一种死锁场景的分析与探索

在程序设计的过程中,使用读写锁功能出现了一种死锁场景,现象是对读写锁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的要求,避免了同一线程加读锁超过一次的问题,该方式处理最优。

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值