多线程中不可避免的会用到锁,进一步也会接触到读写锁的存在;读写锁与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享;读写锁非常适合于对数据结构读的次数远大于写的情况,合理运用可以有效地对锁降级,提高程序效率;
最近在程序中遇到一个死锁问题,耽误了我将近一整天时间,最后发现是由于读写锁的递归使用导致的死锁问题,读写锁的递归导致死锁其实早在说明中就提到了,可惜当初看到时没能理解;
https://docs.microsoft.com/zh-cn/windows/win32/sync/slim-reader-writer--srw--locks
中提到:
Exclusive mode SRW locks cannot be acquired recursively. If a thread tries to acquire a lock that it already holds, that attempt will fail (for TryAcquireSRWLockExclusive) or deadlock (for AcquireSRWLockExclusive)
我将我遇到的问题简化后得到下面这份代码:
SRWLOCK m_rwLock;
unsigned int __stdcall WriteLockThread(PVOID pM)
{
cout<<"进入线程"<<endl;
cout<<"准备加写锁"<<endl;
::AcquireSRWLockExclusive(&m_rwLock);// 加写锁
cout<<"写锁锁住"<<endl;
for (int i = 1;i<5;i++)
{
cout<<i<<endl;
}
cout<<"准备卸写锁"<<endl;
::ReleaseSRWLockExclusive(&m_rwLock);// 卸写锁
cout<<"卸写锁完成"<<endl;
return 0;
}
void Main()
{
/*********************************/
::AcquireSRWLockShared(&m_rwLock);// 第一次加读锁
cout<<"第一次上读锁"<<endl;
HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, WriteLockThread, this, 0, NULL);
Sleep(1000 * 5);
{
cout<<"准备第二次上读锁"<<endl;
::AcquireSRWLockShared(&m_rwLock);// 第二次加读锁
cout<<"第二次读锁锁住"<<endl;
for (int i = 1;i<5;i++)
{
cout<<i<<endl;
}
cout<<"准备卸第二次读锁"<<endl;
::ReleaseSRWLockShared(&m_rwLock);// 卸第二读锁
cout<<"卸第二次读锁完成"<<endl;
}
cout<<"准备卸第一次读锁"<<endl;
::ReleaseSRWLockShared(&m_rwLock);// 卸第一读锁
cout<<"卸第一次读锁完成"<<endl;
}
看上面的代码可以看到,在Main函数中,第一次对m_rwLock加了一个共享锁,即读锁,然后开启一个线程,线程函数中会对m_rwLock再添加一个写锁,但由于此时,Main函数中第一次加的读锁还未释放,此时写锁不会添加成功,而是出于一个等待的状态;随后Main函数中过5秒,Main函数要求对m_rwLock添加第二个读锁,如果是在没有之前开启的线程时,这种情况第二个读锁是可以创建成功的,因为读锁之间是不互斥的,但是此时由于在第二个读锁之前已经有了一个写锁在等待第一个读锁的释放,导致此时第二个读锁需要等到写锁取到锁并执行操作并释放后才能进入,于是第二个读锁也被迫等待;最后主线程中,第一个读锁需要等到第二个读锁执行结束后继续执行才能释放,而此时第二个读锁还在等待,导致形成了一种三角恋型的死锁;
根据这个问题可以进一步延伸,得到递归锁和非递归锁的概念,对于读写锁来说读锁即为递归锁,写锁为非递归锁,读写混用时为非递归锁,因为互斥锁(写锁)优先级高。如果想要对写锁进行递归使用,可以自己实现,大致思路是在一个写锁的基础上添加一个引用计数,这个引用计数可能还会需要加锁等等。