【问题标题】:pthreads: reader/writer locks, upgrading read lock to write lockpthreads:读/写锁,将读锁升级为写锁
【发布时间】:2011-01-25 08:09:14
【问题描述】:

我在 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.

我已经阅读了手册页,它非常具体。

如果在 通话时间 读写锁(无论是读还是 写锁)。

在这种情况下,将读锁升级为写锁的最佳方法是什么。我不想在我保护的变量上引入竞争。

大概我可以创建另一个互斥锁来包含读锁的释放和写锁的获取,但是我并没有真正看到读/写锁的使用。我还不如简单地使用一个普通的互斥锁。

谢谢

【问题讨论】:

  • Boost.Thread 具有 UpgradeLockable 概念,但如果您的代码已经深入 pthreads,我怀疑这对您有多大用处。
  • @Steve。你知道它是如何实现的吗?它是否使用我正在考虑做的单独的互斥锁。我想我可以下载它并检查一下:o)
  • 不,我不知道 Boost.Thread 是怎么做到的,抱歉。
  • @ScaryAardvark(以及其他想要总结 Boost 实现的人):我刚刚查看了 pthread 实现的 1.47.0 版本的代码,它使用互斥锁和 3 个条件变量而不是pthread_rwlock_t。查看 boost/thread/pthread/shared_mutex.hpp 了解详情。

标签: c++ locking pthreads


【解决方案1】:

在下面的场景中,除了死锁你还想要什么?

  • 线程 1 获取读锁
  • 线程 2 获取读锁
  • 线程1要求升级锁写
  • 线程2要求升级锁写

所以我只需释放读锁,获取写锁并再次检查是否需要进行更新。

【讨论】:

  • 如果另一个线程持有读锁,为什么其中一个线程会获得写锁?如果两者最终都可以获得写锁,那么第二个将面临所有已进行的检查都必须重做的问题,因为受保护的资源可能已经发生了重大变化。因此,释放和重新获取提供了相同的行为。
  • 我希望避免对被锁定的对象进行第二次检查。我知道 IBM 的读/写锁实现允许调用线程升级它的锁,如果它是唯一持有写锁的线程。 publib.boulder.ibm.com/infocenter/iseries/v5r4/index.jsp?topic=/…
  • @nos。是的,这会陷入僵局。 T1 在请求升级时会被阻止这样做,因为 T2 有一个读锁。 T2 会在升级到写锁时阻塞,因为 T1 持有读锁。
  • 在回答您的问题“您期望什么”时,答案是如果 r/w 锁是可升级的,那么类似于:线程 1 尝试升级块,然后线程 2 的尝试失败。线程 2 通过释放读取器锁并尝试重新获取它并重新开始来对此做出响应。一旦它释放,线程 1 就可以获得写入者锁,完成它的事务,然后释放锁。任何试图在 Thread1 等待写入器锁时获取读取器锁的人都会阻塞,直到 Thread1 释放两者。乐观锁定,有点。
  • FWIW,我从头开始编写了一个可升级的读写器锁类。 “升级”要么必须是一个非阻塞的“尝试”功能,它可能会失败,要么它必须能够“解锁”然后重新锁定(它可以阻塞,也可以被认为是失败的情况)。
【解决方案2】:

pthread 库不直接支持此操作。

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

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

这样,当您尝试升级它时,没有其他线程可以抢夺写锁。但是,当您尝试升级时,如果其他线程持有读锁,您的线程将阻塞。

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

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

我的 CS 讲座要点:无法可靠地避免死锁。对于提出的每一种策略,至少有一个用例表明该策略是不切实际的。您唯一能做的就是检测死锁情况(即,如果调用失败并显示EDEADLK)并确保您的代码已准备好处理这种情况。 (如何恢复很大程度上取决于您的代码。)

以这种方式降级不容易发生死锁¹,尽管降级和并发升级可能会死锁。如果只有一个线程升级以这种方式锁定(其他线程在需要时立即获得写锁定),也没有死锁风险¹。

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

结论:这取决于你的代码。

如果只读阶段很短暂(即足够短暂,以至于您可以在这段时间内阻止其他读取操作),那么我会选择无偿写入锁定方法。

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


¹除非这个锁和它的互斥锁以外的资源发挥作用​​

【讨论】:

    【解决方案3】:

    最简单和最安全的方法是从您想要更改数据的那一刻开始而不是从您确定要更改数据的那一刻开始使用写锁。我知道这将使对您的数据的访问更加序列化。

    阅读这个问题时我有点惊讶,因为我什至从未考虑过先获取读锁然后升级到写锁。嗯,不同的情况可能需要不同的方法。

    【讨论】:

    • 当您可能不需要它时,获取写锁似乎有点过头了。正如您所指出的,这将序列化。而获取读锁不会阻塞。当您知道需要更改某些内容时,您可以升级到写锁。这只是在 posix 下这样做的实际行为,引入了一个种族,这是一种耻辱。
    • 很遗憾,但据我所知,在大多数情况下,这更像是理论上的差异。
    【解决方案4】:

    我认为你可以使用 Posix fcntl() 而不是使用 pthread 读/写锁。在这里,您可以轻松地从读取升级到写入。我们将它用于 B-tree 插入。一旦我们知道插入发生的节点,我们将把它升级为写锁。此外,当我们需要拆分节点时,我们会将节点、其父节点和子节点的锁从读升级为写。由于 B-tree 是一种基于文件的数据结构,它有助于锁定文件的区域。

    【讨论】:

      猜你喜欢
      • 2010-12-23
      • 1970-01-01
      • 1970-01-01
      • 2011-12-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-05-24
      • 1970-01-01
      相关资源
      最近更新 更多