pthreads: 读写锁,将读锁升级为写锁

18

我在Linux上使用读/写锁,发现尝试将一个已经被读锁定的对象升级为写锁定会导致死锁。

即:

// acquire the read lock in thread 1.
pthread_rwlock_rdlock( &lock );

// make a decision to upgrade the lock in threads 1.
pthread_rwlock_wrlock( &lock ); // this deadlocks as already hold read lock.

我已阅读了man页,它非常具体。

如果调用线程在调用时持有读写锁(无论是读锁还是写锁),则可能会发生死锁。

在这种情况下,最佳的方式是如何将读锁升级为写锁。 我不想在保护变量时引入竞态条件。

我可以创建另一个互斥锁来包含释放读锁和获取写锁的过程,但是这样做似乎没有使用读写锁的意义。 我可能还不如只使用一个普通的互斥锁。

谢谢。


Boost.Thread具有UpgradeLockable概念,但如果您的代码已经深入到pthread中,那么我认为它对您没有太大用处。 - Steve Jessop
@Steve。你知道它是如何实现的吗?它使用单独的互斥锁吗?这正是我考虑要做的。 我想我可以下载它并检查一下:o) - ScaryAardvark
不好意思,我不知道Boost.Thread是如何做到的。 - Steve Jessop
1
@ScaryAardvark(以及其他想要了解Boost实现摘要的人):我刚刚查看了pthread实现版本1.47.0的代码,它使用了互斥锁和3个条件变量,而不是pthread_rwlock_t。请查看boost/thread/pthread/shared_mutex.hpp以获取详细信息。 - BD at Rivenhill
4个回答

22

在以下场景中,你还需要什么比死锁更糟糕的情况?

  • 线程1获取读锁
  • 线程2获取读锁
  • 线程1请求将锁升级为写锁
  • 线程2请求将锁升级为写锁

因此,我会释放读锁,获取写锁,并再次检查是否需要进行更新。


如果另一个线程持有读锁,为什么会有一个线程获得写锁?如果两个线程最终都可以获得写锁,第二个线程将遇到问题,因为所有已经进行的检查都必须重新执行,因为受保护的资源可能已经发生了重大变化。释放和重新获取提供相同的行为。 - AProgrammer
我本希望避免对已锁定对象进行第二次检查。 我知道IBM的读/写锁实现允许调用线程升级其锁定,如果它是唯一持有写锁的线程。http://publib.boulder.ibm.com/infocenter/iseries/v5r4/index.jsp?topic=/apis/users_93.htm - ScaryAardvark
@nos。是的,这会造成死锁。当T1请求升级时,由于T2持有读锁,因此T1将被阻塞无法升级。当T2尝试升级为写锁时,由于T1持有读锁,因此T2也会被阻塞。 - ScaryAardvark
6
回答你的问题“你期望什么”,答案是,如果读写锁可以升级,那么可能会出现以下情况:线程1尝试升级阻塞,然后线程2的尝试会失败。线程2会通过释放读取锁并尝试重新获取它来响应此问题,并重新开始整个过程。一旦线程2释放,线程1就可以获得写入锁定,完成其事务并释放锁定。在Thread1等待写入锁定的同时,任何想要获取读取锁定的人都会被阻止,直到Thread1释放两者。这有点像乐观锁定。 - Steve Jessop
就我的经验而言,我已经从头开始编写了一个可升级的读写锁类。 "Upgrade" 要么是一个非阻塞的“尝试”函数,可能会失败,要么需要能够“解锁”,然后重新加锁(这可能会被阻止,也可以被视为失败案例)。 - Adisak
将读锁升级为写锁的问题在于,如果有多个线程尝试进行此操作,并且如果您无法获得写锁,则重新进入并尝试获取另一个读锁,这可能会导致活锁或长时间的锁队列。您该如何解决这个问题? - CMCDragonkai

3

pthread库不直接支持此操作。

作为解决方法,您可以定义一个互斥锁来保护锁定:

  • 要获取锁,请先获取互斥锁,然后获取锁(根据需要读取或写入),然后释放互斥锁。(永远不要在没有持有互斥锁的情况下获取锁。)
  • 要释放锁,只需释放它(此处不需要互斥锁)。
  • 要升级锁,请获取互斥锁,释放读锁,获取写锁,然后释放互斥锁。
  • 要降级锁,请获取互斥锁,释放写锁,获取读锁,然后释放互斥锁。

这样,当您尝试升级锁时,其他线程无法夺取写锁。但是,如果其他线程在您尝试升级时持有读锁,则您的线程将被阻塞。

另外,如上所述,如果两个线程同时尝试升级同一锁,则会遇到死锁:

  • T1和T2都持有读锁。
  • T1希望升级,获取互斥锁,释放读锁并尝试获取写锁。这被T2阻塞。
  • T2希望升级,尝试获取互斥锁,并被T1阻塞。

我的CS讲座中得出的结论:无法可靠地避免死锁。针对提出的每种策略,至少有一种使用情况是不切实际的。您唯一能做的就是检测死锁条件(即如果调用失败,则会出现“EDEADLK”),并确保您的代码准备处理该情况。(如何恢复严重依赖于您的代码。)

以此方式降级不容易发生死锁¹,但降级和并发升级可能会死锁。如果只有一个线程以此方式升级该锁(并且其他线程立即需要写入锁定),那么也没有死锁的风险¹。

正如其他人所说,当您可能需要它时立即获取写锁将是另一种不容易发生死锁¹的替代方法,但可能会不必要地阻止其他读操作同时进行。

结论:取决于您的代码。

如果只读阶段很短(即足够短,以至于您可以承受在此期间阻止其他读操作),则我会选择过度的写锁方法。

如果只读阶段可能很长,并且在此期间阻止其他读取是不可接受的,请选择受互斥锁保护的锁升级,但要么将其限制为每个锁一个线程(“只有T1可以升级锁L42,但不能升级其他线程”),要么提供一种检测和恢复死锁的方法。


¹除非涉及除此锁及其互斥锁之外的资源


1
最简单和最安全的方法是从您想要更改数据的那一刻开始获取写锁,而不是从您确定要更改数据的那一刻开始获取。我知道这会使访问您的数据变得更加序列化。
当我阅读这个问题时,我有点惊讶,因为我甚至没有考虑过先获取读锁,然后升级到写锁。好吧,不同的情况可能需要不同的方法。

2
当你可能不需要它时,获取写锁似乎有点过头了。这会像你指出的那样串行化。而获取读锁不会阻塞。在你知道需要更改某些内容的时候,你可以升级到写锁。只是在posix下实际执行此操作会引入竞争,这很遗憾。 - ScaryAardvark
很遗憾,但在我所知道的大多数情况下,这更多是理论上的差异。 - stefaanv

0

我认为,与其使用pthread读/写锁,不如使用Posix fcntl()。在这里,您可以轻松地从读取升级到写入,而无需任何麻烦。我们正在将其用于B树插入。一旦我们知道插入发生的节点,我们将将其升级为写锁。此外,当我们需要拆分节点时,我们将将节点、其父节点和子节点的锁从读取升级到写入。由于B树是基于文件的数据结构,因此它有助于对文件区域进行锁定。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接