跨平台可上行和可降级读/写锁定
Cross-platform up- and downgradable read/write lock
我尝试将大型代码库的一些中心数据结构变成多线程。访问接口已更改为表示读/写锁定,可能会升级和降级:
以前:
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_mutex
和std::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_up
和transition_down
技巧"为我"解决上述一些转换。 我认为我可以做得更好,它确实显着增加了代码体积。
让它"自动写入"解锁到唯一,以及唯一到(解锁|共享)是我从中得到的全部。 所以可能不值得。
创建使用上述内容的智能 RAII 对象有点棘手,因为它们必须支持默认unique_lock
和shared_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
添加"可以重新锁定"标志,以便在容器操作/工作期间引发异常时是否执行重新锁定。
- 当只有一个线程主要使用该对象而其他线程很少使用它时,如何最小化该对象的互斥锁锁定?
- C++将互斥锁锁定为来自另一个线程
- 如何使用互斥锁锁定对布尔值的访问?
- 成功的互斥锁锁定阻塞
- 简单的读写锁
- 优化读/写锁的实现
- boost中是否有允许写偏锁定的功能
- 提升::Asio 写锁
- 如何从更基本的同步原语中创建多读/单写锁
- 此用例是否需要读写锁
- 互斥锁锁定内存的哪一部分?(线程)
- 通过私有互斥锁锁定对象的最佳方法是什么
- 在线程内部函数上使用哪种保护方法(互斥,读写锁.)
- 我们可以在socket Map上使用读写锁吗?
- tmultireadexexclusivewritesynchronizer将写锁降级为读锁时的行为
- 如何解锁锁定的位图
- 在另一个函数中增强读/写锁
- 单个进程中有数千个读/写锁
- 如何在c++ 14中实现读/写锁
- C++互斥锁锁定错误