std::mutex::try_lock 虚假失败
std::mutex::try_lock spuriously fail?
也许我对std::mutex::try_lock
有误解:
允许此函数虚假失败并返回 false,即使互斥锁当前未被任何其他线程锁定。
这意味着如果没有一个线程在该mutex
上锁定,当我尝试try_lock
时,它可能会返回false
?出于什么目的?
如果它被锁定,try_lock
返回的功能不是false
的,或者如果没有人锁定它,true
吗?不太确定我的非母语英语是否在愚弄我......
这意味着如果没有一个线程具有该互斥锁,当我尝试try_lock时,它可能会返回 false?
是的,这正是它所说的。
如果它被锁定,try_lock返回 false 的功能不是假的,如果没有人锁定它,则返回 true 的函数不是真的吗?
不,try_lock
的功能是尝试锁定互斥锁。
但是,它可能会失败的方式不止一种:
- 互斥锁已经在其他地方锁定(这是您正在考虑的那个)
- 某些特定于平台的功能会中断或阻止锁定尝试,并将控制权返回给可以决定是否重试的调用方。
POSIX-ish平台上的常见情况,并从POSIX线程继承,是信号被传递到当前线程(并由信号处理程序处理),中断锁定尝试。
在其他平台上可能还有其他特定于平台的原因,但行为是相同的。
根据您的评论,我会写(引用您的话):
std::unique_lock<std::mutex> lock(m, std::defer_lock); // m being a mutex
...
if (lock.try_lock()) {
... // "DO something if nobody has a lock"
} else {
... // "GO AHEAD"
}
请注意,lock.try_lock()
有效地调用m.try_lock()
,因此它也容易出现虚假失败。但我不太关心这个问题。IMO,在实践中,虚假故障/唤醒非常罕见(正如Useless指出的那样,在Linux上,它们可能在信号传递时发生)。
有关虚假问题的更多信息,请参阅例如:https://en.wikipedia.org/wiki/Spurious_wakeup 或为什么pthread_cond_wait有虚假唤醒?。
更新
如果你真的想消除try_lock
的杂散失败,你可以使用一些原子标志,例如:
// shared by threads:
std::mutex m;
std::atomic<bool> flag{false};
// within threads:
std::unique_lock<std::mutex> lock(m, std::defer_lock); // m being a mutex
...
while (true) {
lock.try_lock();
if (lock.owns_lock()) {
flag = true;
... // "DO something if nobody has a lock"
flag = false;
break;
} else if (flag == true) {
... // "GO AHEAD"
break;
}
}
它可能会被重写成更好的形式,我没有检查。另外,请注意,flag
不会通过 RAII 自动取消设置,一些范围保护在这里可能很有用。
更新 2
如果您不需要mutex
的阻塞功能,请使用std::atomic_flag
:
std::atomic_flag lock = ATOMIC_FLAG_INIT;
// within threads:
if (lock.test_and_set()) {
... // "DO something if nobody has a lock"
lock.clear();
} else {
... // "GO AHEAD"
}
只是,再一次,通过一些 RAII 机制清除标志会更好。
与那里所说的不同,我认为try_lock
函数没有任何理由由于与操作系统相关的原因而失败:这样的操作是非阻塞的,所以信号不能真正中断它。它很可能与如何在 CPU 级别实现此功能有关。毕竟,对于互斥锁来说,无争议的情况通常是最有趣的情况。
互斥锁通常需要某种形式的原子比较交换 操作。C++11 和 C11 介绍了atomic_compare_exchange_strong
和atomic_compare_exchange_weak
。后者被允许虚假地失败。
通过允许try_lock
虚假失败,允许实现使用atomic_compare_exchange_weak
来最大化性能和最小化代码大小。
例如,在ARM64上,原子操作通常使用独占加载(LDXR
)和独占存储(STRX
)指令来实现。LDXR
启动"监视器"硬件,该硬件开始跟踪对内存区域的所有访问。 仅当在LDXR
和STRX
指令之间未访问该区域时,STRX
才会执行存储。因此,如果另一个线程访问该内存区域,或者两者之间存在 IRQ,则整个序列可能会虚假失败。
实际上,为使用弱保证实现try_lock
生成的代码与使用强保证实现的代码没有太大区别。
bool mutex_trylock_weak(atomic_int *mtx)
{
int old = 0;
return atomic_compare_exchange_weak(mtx, &old, 1);
}
bool mutex_trylock_strong(atomic_int *mtx)
{
int old = 0;
return atomic_compare_exchange_strong(mtx, &old, 1);
}
查看为 ARM64 生成的程序集:
mutex_trylock_weak:
sub sp, sp, #16
mov w1, 0
str wzr, [sp, 12]
ldaxr w3, [x0] ; exclusive load (acquire)
cmp w3, w1
bne .L3
mov w2, 1
stlxr w4, w2, [x0] ; exclusive store (release)
cmp w4, 0 ; the only difference is here
.L3:
cset w0, eq
add sp, sp, 16
ret
mutex_trylock_strong:
sub sp, sp, #16
mov w1, 0
mov w2, 1
str wzr, [sp, 12]
.L8:
ldaxr w3, [x0] ; exclusive load (acquire)
cmp w3, w1
bne .L9
stlxr w4, w2, [x0] ; exclusive store (release)
cbnz w4, .L8 ; the only difference is here
.L9:
cset w0, eq
add sp, sp, 16
ret
唯一的区别是"弱"版本消除了条件向后分支cbnz w4, .L8
并将其替换为cmp w4, 0
。在分支预测信息缺失的情况下,CPU 将后向条件分支预测为"将被采用",因为它们被假定为循环的一部分 - 在这种情况下,这种假设是错误的,因为大多数时间都会被获取锁(假设低争用是最常见的情况)。
Imo 这是这些功能之间的唯一性能差异。"强"版本在某些工作负载下,基本上可以遭受单条指令100%的分支误判率。
顺便说一下,ARMv8.1 引入了原子指令,所以两者之间没有区别,就像在x86_64上一样。使用-march=armv8.1-a
标志生成的代码:
sub sp, sp, #16
mov w1, 0
mov w2, 1
mov w3, w1
str wzr, [sp, 12]
casal w3, w2, [x0]
cmp w3, w1
cset w0, eq
add sp, sp, 16
ret
某些try_lock
函数即使在使用atomic_compare_exchange_strong
时也可能失败,例如,shared_mutex
try_lock_shared
可能需要增加读取器计数器,如果另一个读取器进入锁定,则可能会失败。这种函数的"强"变体需要生成一个循环,因此可能会遭受类似的分支错误预测。
另一个小细节:如果互斥体是用 C 语言编写的,一些编译器(如 Clang)可能会在 16 字节边界对齐循环以提高其性能,使函数体膨胀。如果循环几乎总是运行一次,则不需要这样做。
虚假故障的另一个原因是无法获取内部互斥锁(如果使用自旋锁和某些内核原语实现互斥锁)。理论上,在try_lock
的内核实现中也可以获得相同的原理,尽管这似乎并不合理。
在论文《C++并发内存模型基础》第3节中,已经明确解释了为什么该标准允许try_lock
的虚假故障。简而言之,它被指定为使try_lock
的语义与C++记忆模型中的种族定义保持一致。
如果对try_lock()
的调用返回 true,则调用成功锁定锁。如果它返回 false,如果没有。仅此而已。是的,当没有其他人拥有锁时,该函数可以返回 false。False仅表示尝试锁定未成功;它不会告诉你为什么它失败了。
- 如果没有malloc,链表实现将失败
- 模板参数替换失败,并且未完成隐式转换
- 具有默认模板参数的多态类的模板推导失败
- 视图中的参数推导失败:take_while
- 链接到自行创建的dll失败
- 带有特殊路径部分的"std::filesystem::weakly_canonical"失败
- GetShortPathName在网络驱动器上使用中文文件夹时失败
- gcc和c++17的过载解析失败
- 为什么使用 P/Invoke 调用 dll 时,某些计算机中的 LoadLibrary 失败?
- 在WSL:configure_file上对config_file的每次调用都失败:配置文件时出现问题
- 使用 GCC 卸载的 OpenMP 卸载失败,并出现"Ptx assembly aborted due to errors"
- 使用cmake从源代码构建MySQL连接器/C++失败(与以前的声明冲突)
- 链接阶段在Ubuntu上失败,但在MacOS上失败
- 从父数组测试用例构造二叉树失败
- LibGit2 SSH身份验证失败
- 如何让LLDB在成功时退出,在失败时等待
- VS2017,C++包含目录与附加包含目录,子文件夹包含失败-但为什么
- 生成MRPT库时cmake配置失败
- 为什么除非添加括号,否则构造函数上的模板替换会失败?
- std::mutex::lock在Windows上失败,错误码3