Порядок операций между вызовами 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 шт):

Автор решения: VladD

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

Мы видим, что мьютекс защищает не переменные, а разделяемое состояние.


*Ну и процессор имеет право на подобные трюки тоже, это отдельная тема.

**И заставляют его в свою очередь вставлять в код команды, ограничивающие свободу процессора.

→ Ссылка