跨平台可上行和可降级读/写锁定

Cross-platform up- and downgradable read/write lock

本文关键字:写锁 锁定 跨平台 降级      更新时间:2023-10-16

我尝试将大型代码库的一些中心数据结构变成多线程。访问接口已更改为表示读/写锁定,可能会升级和降级:

以前:

Container& container = state.getContainer();
auto value = container.find( "foo" )->bar;
container.clear();

现在:

ReadContainerLock container = state.getContainer();
auto value = container.find( "foo" )->bar;
{
  // Upgrade read lock to write lock
  WriteContainerLock write = state.upgrade( container );
  write.clear();
} // Downgrades write lock to read lock

使用实际的锁定std::mutex(而不是 r/w 实现)工作正常,但不会带来任何性能优势(实际上会降低运行时)。

实际更改的数据相对较少,因此采用读/写概念似乎非常可取。现在最大的问题是,我似乎找不到任何实现读/写概念并支持升级和降级的库,并且可以在Windows,OSX和Linux上运行。

Boost已经BOOST_THREAD_PROVIDES_SHARED_MUTEX_UPWARDS_CONVERSIONS但似乎不支持从shared降级(阻止)原子升级到unique

是否有任何库支持所需的功能集?

编辑:

很抱歉不清楚。当然,我的意思是多读器/单写器锁语义。

自从我回答以来,这个问题发生了变化。 由于前面的答案仍然有用,因此我将保留它。

新问题似乎是"我想要一个(通用)阅读器编写器锁,任何读取器都可以原子地升级到写入器"。

如果没有死锁或回滚操作(事务读取)的能力,就无法做到这一点,这远非通用用途。

假设你有爱丽丝和鲍勃。 两人都想读一会儿书,然后他们都想写。

爱丽丝和鲍勃都有读锁。 然后升级到写锁定。 两者都无法进行,因为在获取读锁定时无法获取写锁定。 您无法解锁读锁定,因为这样 Alice 在读取锁定时读取的状态可能与获取写锁定后的状态不一致。

这只能通过>读写升级可能失败的可能性来解决,或者能够在读取中回滚所有操作(因此 Alice 可以"取消读取",Bob 可以前进,然后 Alice 可以重新读取并尝试获取写锁定)。

编写类型安全的事务代码在C++中并不真正受支持。 您可以手动执行此操作,但除了简单的情况之外,它很容易出错。 还可以使用其他形式的事务回滚。 它们都不是通用的读写器锁。

你可以自己动手。 如果状态为 R、U、W 和 {}(读取、可升级、写入和无锁定),则可以轻松支持这些转换:

{} -> R|U|W
R|U|W -> {}
U->W
W->U
U->R

并暗示上述内容:

W->R

我认为这符合您的要求。

"缺失"的过渡是R->U,这就是让我们安全地拥有多个阅读器的原因。 最多一个读取器(升级读取器)有权在不释放其读锁定的情况下升级写入。 当它们处于升级状态时,它们不会阻止其他线程读取(但它们会阻止其他线程写入)。


这是一张草图。 有一个shared_mutex A;和一个mutex B;.

B代表升级写入的权利在您持有时阅读的权利。 所有作家也都持有B,所以你不能同时拥有升级写作的权利,而其他人有权写作。

过渡如下所示:

 {}->R = read(A)
 {}->W = lock(B) then write(A)
 {}->U = lock(B)
 U->W = write(A)
 W->U = unwrite(A)
 U->R = read(A) then unlock(B)
 W->R = W->U->R
 R->{} = unread(A)
 W->{} = unwrite(A) then unlock(B)
 U->{} = unlock(B)

这只需要std::shared_mutexstd::mutex,以及一些样板来编写锁和过渡。

如果您希望能够在升级锁"

保留在范围内"时生成写锁定,则需要做额外的工作来"将升级锁传递回读锁定"。

以下是一些额外的尝试过渡,灵感来自以下@HowardHinnat:

R->try U = return try_lock(B) && unread(A)
R->try W = return R->try U->W

这是一个没有尝试操作的upgradable_mutex:

struct upgradeable_mutex {
  std::mutex u;
  std::shared_timed_mutex s;
  enum class state {
    unlocked,
    shared,
    aspiring,
    unique
  };
  // one step at a time:
  template<state start, state finish>
  void transition_up() {
    transition_up<start, (state)((int)finish-1)>();
    transition_up<(state)((int)finish-1), finish>();
  }
  // one step at a time:
  template<state start, state finish>
  void transition_down() {
    transition_down<start, (state)((int)start-1)>();
    transition_down<(state)((int)start-1), finish>();
  }
  void lock();
  void unlock();
  void lock_shared();
  void unlock_shared();
  void lock_aspiring();
  void unlock_aspiring();
  void aspiring_to_unique();
  void unique_to_aspiring();
  void aspiring_to_shared();
  void unique_to_shared();
};
template<>
void upgradeable_mutex::transition_up<
  upgradeable_mutex::state::unlocked, upgradeable_mutex::state::shared
>
() {
  s.lock_shared();
}
template<>
void upgradeable_mutex::transition_down<
  upgradeable_mutex::state::shared, upgradeable_mutex::state::unlocked
>
() {
  s.unlock_shared();
}
template<>
void upgradeable_mutex::transition_up<
  upgradeable_mutex::state::unlocked, upgradeable_mutex::state::aspiring
>
() {
  u.lock();
}
template<>
void upgradeable_mutex::transition_down<
  upgradeable_mutex::state::aspiring, upgradeable_mutex::state::unlocked
>
() {
  u.unlock();
}
template<>
void upgradeable_mutex::transition_up<
  upgradeable_mutex::state::aspiring, upgradeable_mutex::state::unique
>
() {
  s.lock();
}
template<>
void upgradeable_mutex::transition_down<
  upgradeable_mutex::state::unique, upgradeable_mutex::state::aspiring
>
() {
  s.unlock();
}
template<>
void upgradeable_mutex::transition_down<
  upgradeable_mutex::state::aspiring, upgradeable_mutex::state::shared
>
() {
  s.lock();
  u.unlock();
}
  void upgradeable_mutex::lock() {
    transition_up<state::unlocked, state::unique>();
  }
  void upgradeable_mutex::unlock() {
    transition_down<state::unique, state::unlocked>();
  }
  void upgradeable_mutex::lock_shared() {
    transition_up<state::unlocked, state::shared>();
  }
  void upgradeable_mutex::unlock_shared() {
    transition_down<state::shared, state::unlocked>();
  }
  void upgradeable_mutex::lock_aspiring() {
    transition_up<state::unlocked, state::aspiring>();
  }
  void upgradeable_mutex::unlock_aspiring() {
    transition_down<state::aspiring, state::unlocked>();
  }
  void upgradeable_mutex::aspiring_to_unique() {
    transition_up<state::aspiring, state::unique>();
  }
  void upgradeable_mutex::unique_to_aspiring() {
    transition_down<state::unique, state::aspiring>();
  }
  void upgradeable_mutex::aspiring_to_shared() {
    transition_down<state::aspiring, state::shared>();
  }
  void upgradeable_mutex::unique_to_shared() {
    transition_down<state::unique, state::shared>();
  }

试图让编译器使用transition_uptransition_down技巧"为我"解决上述一些转换。 我认为我可以做得更好,它确实显着增加了代码体积。

让它"自动写入"解锁到唯一,以及唯一到(解锁|共享)是我从中得到的全部。 所以可能不值得。

创建使用上述内容的智能 RAII 对象有点棘手,因为它们必须支持默认unique_lockshared_lock不支持的一些转换。

你可以只写aspiring_lock然后在那里进行转换(作为operator unique_lock,或作为返回所说的方法等),但是从unique_lock&&向下转换为shared_lock的能力是upgradeable_mutex独有的,并且使用隐式转换有点棘手......

活生生的例子。

这是我通常的建议: Seqlock

您可以同时拥有单个编写器和多个读取器。作家使用旋转锁进行竞争。单个作家不需要竞争,因此更便宜。

读者真的只是在阅读。他们不编写任何状态变量、计数器等。这意味着你真的不知道有多少读者。而且,没有缓存线乒乓球,因此您可以在延迟和吞吐量方面获得最佳性能。

有什么问题?数据几乎必须是POD。它实际上不必 POD,但它不能失效(不删除 std::map 节点),因为读者可能会在写入时阅读它。

只有在事实发生后,读者才会发现数据可能不好,他们必须重新阅读。

是的,作家不会等待读者,所以没有升级/降级的概念。您可以解锁一个并锁定另一个。您支付的费用比任何类型的互斥锁都少,但数据可能在此过程中发生了变化。

如果您愿意,我可以更详细地介绍。

std::shared_mutex(如果您的平台上不可用,则在 boost 中实现)为该问题提供了一些替代方案。

对于原子升级锁语义,提升升级锁可能是最佳的跨平台替代方案。

它没有您正在寻找的升级和降级锁定机制,但要获得独占锁,可以先放弃共享访问权限,然后再寻求独占访问权限。

// assumes shared_lock with shared access has been obtained
ReadContainerLock container = state.getContainer();
auto value = container.find( "foo" )->bar;
{
  container.shared_mutex().unlock();
  // Upgrade read lock to write lock
  std::unique_lock<std::shared_mutex> write(container.shared_mutex());
  // container work...
  write.unlock();
  container.shared_mutex().lock_shared();
} // Downgrades write lock to read lock

实用程序类可用于在作用域末尾重新锁定shared_mutex;

struct re_locker {
  re_locker(std::shared_mutex& m) : m_(m) { m_.unlock(); }
  ~re_locker() { m_.shared_lock(); }
  // delete the copy and move constructors and assignment operator (redacted for simplicity)
};
// ...
auto value = container.find( "foo" )->bar;
{
  re_locker re_lock(container.shared_mutex());
  // Upgrade read lock to write lock
  std::unique_lock<std::shared_mutex> write(container.shared_mutex());
  // container work...
} // Downgrades write lock to read lock

根据您想要或要求的异常保证,您可能需要向re_locker添加"可以重新锁定"标志,以便在容器操作/工作期间引发异常时是否执行重新锁定。