std :: mutex :: try_lock虚假失败?

时间:2018-01-17 15:46:49

标签: c++ locking mutex

关于std::mutex::try_lock可能I'm misunderstanding

允许此函数虚假失败并返回false,即使互斥锁当前未被任何其他线程锁定。

这意味着如果没有一个线程锁定mutex,当我尝试try_lock时,可以返回false?出于什么目的?

try_lock如果没有人锁定,false锁定 OR true,则SELECT date, userId, SUM(totals.visits) totalvisits, GROUP_CONCAT(device.deviceCategory) sequentialdevice FROM ( SELECT date, visitStartTime, customDimensions.value userId, totals.visits, device.deviceCategory FROM TABLE_DATE_RANGE([164345793.ga_sessions_], TIMESTAMP('20171127'), CURRENT_TIMESTAMP()) WHERE customDimensions.index = 1 AND customDimensions.value CONTAINS "hip|" GROUP BY date, visitStartTime, userId, totals.visits, device.deviceCategory HAVING userId="hip|7e4fbce9-bbfb-4677-aab0-dcd02851fdb4" ORDER BY date ASC, visitStartTime ASC) GROUP BY date, userId 的功能是否会返回SELECT date, userId, totalsvisits, STRING_AGG(DISTINCT devicecategory ORDER BY date ASC, vstime ASC) deviceAgg FROM ( SELECT date, visitStartTime vstime, cd.value userId, totals.visits totalsvisits, device.deviceCategory devicecategory FROM `12314124123123.ga_sessions_*`, UNNEST(customDimensions) AS cd WHERE cd.index=1 AND cd.value IS NOT NULL GROUP BY date, visitStartTime, userId, totals.visits, device.deviceCategory HAVING userId="hip|7e4fbce9-bbfb-4677-aab0-dcd02851fdb4" ORDER BY date ASC, visitStartTime ASC) GROUP BY date, userId, totalsvisits ?不确定我的非母语英语是否在欺骗我......

5 个答案:

答案 0 :(得分:5)

  

这意味着如果没有一个线程锁定该互斥锁,当我尝试try_lock时,它可能返回false?

是的,这正是它所说的。

  

如果没有锁定,try_lock的函数是否会返回false,如果没有锁定,则返回true?

不,try_lock的功能是尝试来锁定互斥锁。

然而,失败的方法不止一种:

  1. 互斥锁已经锁定在其他位置(这是您正在考虑的那个)
  2. 某些特定于平台的功能会中断或阻止锁定尝试,并且控制权将返回给可以决定是否重试的调用方。
  3. POSIX-ish平台上的常见情况是从POSIX线程继承的,它是一个信号传递给当前线程(并由信号处理程序处理),从而中断锁定尝试。

    在其他平台上可能存在其他特定于平台的原因,但行为是相同的。

答案 1 :(得分:1)

根据你的评论,我会写(引用你的话):

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,在实践中,虚假失败/唤醒是非常罕见的(正如无用的指出,在Linux上,它们可能在信号传递时发生)。

有关虚假问题的更多信息,请参阅:https://en.wikipedia.org/wiki/Spurious_wakeupWhy does pthread_cond_wait have spurious wakeups?

<强>更新

如果你真的想消除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机制清除标志会更好。

答案 2 :(得分:0)

如果对try_lock()的调用返回true,则调用成功锁定锁。如果没有,则返回false。这就是全部。是的,当没有其他人拥有锁时,该函数可以返回false。 False意味着锁定尝试未成功;它没有告诉你它失败的原因。

答案 3 :(得分:0)

与那里所说的不同,我不认为由于操作系统相关原因导致try_lock功能失败有任何原因:这种操作是无阻塞的,因此信号无法真正中断它。很可能它与在CPU级别上如何实现此功能有关。毕竟,无争议的案例通常是互斥体最有趣的案例。

互斥锁定通常需要某种形式的原子比较交换 操作。 C ++ 11和C11引入了atomic_compare_exchange_strongatomic_compare_exchange_weak。允许后者虚假失败。

通过允许try_lock虚假失败,允许实现使用atomic_compare_exchange_weak来最大限度地提高性能并最小化代码大小。

例如,在ARM64上,原子操作通常使用独占负载(LDXR)和独占存储(STRX)指令来实现。 LDXR启动&#34;监控&#34;硬件开始跟踪对存储区域的所有访问。 STRX仅在LDXRSTRX指令之间未对该区域进行访问时才执行商店。因此,如果另一个线程访问该内存区域或者两者之间存在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

唯一的区别是&#34;弱&#34; version消除了条件向后分支cbnz w4, .L8并将其替换为cmp w4, 0。 CPU预测后向条件分支将被视为&#34;将被采取&#34;因为它们被假定为循环的一部分而忽略了分支预测信息 - 在这种情况下这种假设是错误的,因为大多数时间锁定将被获取(低争用被认为是最常见的情况)。

Imo这是这些功能之间唯一的性能差异。 &#34;斯特朗&#34;在某些工作负载下,单个指令基本上可能存在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函数也可能失败,例如try_lock_shared的{​​{1}}可能需要增加阅读器计数器,如果其他阅读器已输入,则可能会失败锁。 &#34;斯特朗&#34;这种函数的变体需要生成一个循环,因此可能遭受类似的分支错误预测。

另一个小细节:如果mutex是用C语言编写的,那么一些编译器(如Clang)可能会在16字节边界处对齐循环以改善其性能,使用填充来膨胀函数体。如果循环几乎总是运行一次,这是不必要的。

虚假失败的另一个原因是无法获取内部互斥锁(如果使用自旋锁和某些内核原语实现互斥锁)。从理论上讲,同样的原则可以在shared_mutex的内核实现中获得,尽管这似乎不合理。

答案 4 :(得分:0)

在论文Foundations of the C++ Concurrency Memory Model的第3节中,已经清楚地解释了该标准为何允许try_lock的虚假故障。简而言之,它是为了使try_lock的语义与C ++内存模型中race的定义一致。