В отличие от того, что было сказано там, я не думаю, что есть какая-то причина для сбоя функции try_lock
из-за причин, связанных с ОС: такая операция не блокируется, поэтому сигналы не могут ее прерывать. Скорее всего, все дело в том, как эта функция реализована на уровне процессора. В конце концов, бесспорный случай обычно самый интересный для мьютекса.
Блокировка мьютекса обычно требует некоторой формы операции обмена атомарным сравнением. C++11 и C11 вводят atomic_compare_exchange_strong
и atomic_compare_exchange_weak
. Последний допускает ложный отказ.
Допуская ложный сбой try_lock
, реализациям разрешается использовать atomic_compare_exchange_weak
для максимизации производительности и минимизации размера кода.
Например, в ARM64 атомарные операции обычно реализуются с использованием инструкций монопольной загрузки (LDXR
) и монопольного сохранения (STRX
). LDXR
запускает аппаратное обеспечение «монитора», которое начинает отслеживать все обращения к области памяти. STRX
выполняет сохранение только в том случае, если между LDXR
и 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
. Ветки с обратным условием прогнозируются ЦП как «будущие выполнены» в отсутствие информации о прогнозировании ветвлений, поскольку предполагается, что они являются частью цикла - такое предположение в этом случае неверно, так как большая часть временной блокировки будет получена ( низкая конкуренция считается наиболее распространенным случаем).
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
, например, try_lock_shared
из shared_mutex
может потребоваться увеличить счетчик считывателя и может произойти сбой, если другой считыватель ввел блокировку. «Сильный» вариант такой функции должен генерировать цикл и, следовательно, может страдать от аналогичного неправильного определения ветвления.
Еще одна небольшая деталь: если мьютекс написан на C, некоторые компиляторы (например, Clang) могут выравнивать цикл по 16-байтовой границе, чтобы улучшить его производительность, раздувая тело функции отступами. В этом нет необходимости, если цикл почти всегда выполняется один раз.
Другой причиной ложного отказа является невозможность получить внутреннюю блокировку мьютекса (если мьютекс реализован с использованием спин-блокировки и какого-либо примитива ядра). Теоретически тот же принцип может быть реализован в реализации ядра try_lock
, хотя это не кажется разумным.
11.02.2018
try_lock
различал разные случаи сбоя... 17.01.2018try-lock
кажется неправильным. Что ты посоветуешь? 17.01.2018