Порядок операций между вызовами lock unlock
К примеру есть такой вод код:
void obj::foo() {
... // some operations
std::lock_guard lock{mut_};
... // some operations and initialization ariables
lock.unlock();
... // other operations
}
Как видно критическая секция находится в середине функции и изолирована с помощью мьютекса. Однако, вроде где-то читал, что компилятор может по своему усмотрению тасовать порядок операций если они не зависят друг от друга, так что я хотел уточнить: гарантируется ли что все, что находтся между инициализацией lock и вызовом lock.unlock() будет защищено мьютексом? Или, быть может, компилятор может решить что какая-то операция между этими вызовами может исполнятся в другом месте, за пределами блокировки?
Ответы (1 шт):
TL;DR: Может переставить, если у него получится доказать, что этого никто не заметит.
Компилятор вправе менять по своему произволу операции, их порядок, выбрасывать любые операции и вводить новые, только бы конечный результат был бы одинаковым с точки зрения однопоточного кода. Это разрешение известо как as-if rule, и позволяет компилятору генерировать более оптимальный код*.
Различные синхронизирующие конструкции — например, мьютексы — ограничивают свободу компилятора**. Заметные снаружи эффекты (например, состояние разделяемых между потоками переменных) на границах работы мьютекса (то есть в точке, где находится lock или unlock) должны быть такими же, как если бы никаких оптимизаций не было.
Прочие изменения всё ещё возможны. Например, если переменная локальна и компилятор видит, что она не видна законными методами из других потоков, он всё ещё может делать с ней что угодно, лишь бы результат был одинаковым.
Пример: в таком коде
int i = 1;
{
std::lock_guard<std::mutex> lock{mutex};
i = 2;
}
std::cout << i;
компилятор clang переносит работу со значением 2 в область после отпускания мьютекса, а присвоение значения 1 вовсе выбросил вместе с переменной i.
...
mov edi, offset mutex
call pthread_mutex_lock
...
mov edi, offset mutex
call pthread_mutex_unlock
.LBB0_4:
mov edi, offset std::cout
mov esi, 2
pop rax
jmp std::basic_ostream<char, std::char_traits<char>>::operator<<(int) # TAILCALL
Мы видим, что мьютекс защищает не переменные, а разделяемое состояние.
*Ну и процессор имеет право на подобные трюки тоже, это отдельная тема.
**И заставляют его в свою очередь вставлять в код команды, ограничивающие свободу процессора.