[英]std::mutex::try_lock spuriously fail?
這意味着如果沒有一個線程鎖定該互斥鎖,當我嘗試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,在實踐中,虛假失敗/喚醒是非常罕見的(正如無用的指出,在Linux上,它們可能在信號傳遞時發生)。
有關虛假問題的更多信息,請參閱: https : //en.wikipedia.org/wiki/Spurious_wakeup或為什么pthread_cond_wait有虛假的喚醒? 。
UPDATE
如果你真的想要消除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()
的調用返回true,則調用成功鎖定鎖。 如果沒有,則返回false。 就這樣。 是的,當沒有其他人擁有鎖時,該函數可以返回false。 False 僅表示鎖定嘗試沒有成功; 它沒有告訴你它失敗的原因。
與那里說的不同,我認為沒有任何理由因為操作系統相關的原因導致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
cbnz w4, .L8
替換它。 由於假設它們是循環的一部分,CPU將預測向后條件分支作為“將被采用”,因為它們被認為是循環的一部分 - 在這種情況下這種假設是錯誤的,因為大多數時間鎖定將被獲取(假設低爭用是最常見的情況)。
Imo這是這些功能之間唯一的性能差異。 在某些工作負載下,“強”版本在單指令上基本上會受到100%分支誤預測率的影響。
順便說一句,ARMv8.1引入了原子指令,因此兩者之間沒有區別,就像在x86_64上一樣。 用-march=armv8.1-a
生成的代碼-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
即使使用atomic_compare_exchange_strong
,某些try_lock
函數也會失敗,例如, shared_mutex
try_lock_shared
可能需要增加讀取器計數器,如果另一個讀取器已進入鎖定,則可能會失敗。 這種函數的“強”變體需要生成循環,因此可能遭受類似的分支錯誤預測。
另一個小細節:如果互斥體是用C語言編寫的,那么一些編譯器(如Clang)可能會在16字節邊界處對齊循環以改善其性能,使用填充來增加函數體。 如果循環幾乎總是運行一次,這是不必要的。
虛假失敗的另一個原因是無法獲取內部互斥鎖(如果使用自旋鎖和某些內核原語實現互斥鎖)。 理論上,在try_lock
的內核實現中可以獲得相同的原則,盡管這似乎不合理。
在C ++並發內存模型第3部分的紙基礎中 ,已經清楚地解釋了為什么該標准允許try_lock
虛假失敗。 簡而言之,它被指定使try_lock
的語義與C ++內存模型中的種族定義一致。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.