Как привязка С++ std:: mutex к ресурсу?
Является ли компилятор просто проверять, какие переменные изменяются между операторами блокировки и разблокировки и связывать их с мьютексом, поэтому есть эксклюзивный доступ к ним?
Или блокирует ли mutex.lock()
все ресурсы, видимые в текущей области?
Ответы
Ответ 1
Учитывая, что m
является переменной типа std::mutex
:
Представьте эту последовательность:
int a;
m.lock();
b += 1;
a = b;
m.unlock();
do_something_with(a);
Здесь происходит "очевидное":
Назначение a из b и приращение b "защищено" от помех от других потоков, потому что другие потоки будут пытаться заблокировать один и тот же m
и будут заблокированы, пока не назовем m.unlock()
.
И происходит более тонкая вещь.
В однопоточном коде компилятор будет стремиться переупорядочить нагрузки и магазины. Без блокировок компилятор мог бы свободно переписывать свой код, если бы это оказалось более эффективным на вашем чипсете:
int a = b + 1;
// m.lock();
b = a;
// m.unlock();
do_something_with(a);
Или даже:
do_something_with(++b);
Однако std::mutex::lock()
, unlock()
, std::thread()
, std::async()
, std::future::get()
и т.д. являются заборами. Компилятор "знает", что он не может изменять порядок загрузки и хранения (чтение и запись) таким образом, что операция заканчивается на другой стороне забора, где вы указали, когда вы написали код.
1:
2: m.lock(); <--- This is a fence
3: b += 1; <--- So this load/store operation may not move above line 2
4: m.unlock(); <-- Nor may it be moved below this line
Представьте, что произойдет, если это не так:
(Переупорядоченный код)
thread1: int a = b + 1;
<--- Here another thread precedes us and executes the same block of code
thread2: int a = b + 1;
thread2: m.lock();
thread2: b = a;
thread2: m.unlock();
thread1: m.lock();
thread1: b = a;
thread1: m.unlock();
thread1:do_something_with(a);
thread2:do_something_with(a);
Если вы выполните его, вы увидите, что b теперь имеет неправильное значение в нем, потому что компилятор привязывался, чтобы сделать ваш код быстрее.
... и что только оптимизация компилятора. std::mutex
и т.д. также предотвращает переупорядочивание кэшей памяти от переупорядочивания нагрузок и хранилищ более "оптимальным" способом, что было бы хорошо в однопоточной среде, но катастрофично в многоядерной (то есть любой современной ПК или телефонной) системе.
Для этой безопасности существует такая стоимость, поскольку кэш потока должен быть очищен до того, как поток B прочитает одни и те же данные, а сброс кешей в память ужасно медленный по сравнению с доступом к кешированной памяти. Но c'est la vie. Это единственный способ сделать одновременное выполнение безопасным.
Вот почему мы предпочитаем, чтобы, если это возможно, в системе SMP, у каждого потока есть своя копия данных для работы. Мы хотим свести к минимуму не только время, затраченное на блокировку, но и количество раз, когда мы пересекаем ограждение.
Я мог бы поговорить о модификаторах std::memory_order
, но это темная и опасная дыра, которую эксперты часто ошибаются, и у новичков нет надежды на ее правильность.
Ответ 2
"мьютекс" не подходит для "взаимного исключения"; правильная дисциплина для использования мьютекса - заблокировать его перед вводом любого кода, который изменяет переменные, которые разделяются между потоками, и разблокировать его, когда этот раздел кода будет выполнен. Если один поток блокирует мьютекс, любой другой поток, который пытается заблокировать его, будет заблокирован до тех пор, пока поток, который владеет мьютексом, не разблокирует его. Это означает, что только один поток за один раз находится внутри кода, который может изменять общие переменные и устраняет условия гонки.
Остальная часть мьютекса полагается на магию компилятора на некотором уровне: она также запрещает компилятору перемещать нагрузки и хранить данные из защищенного кода вне его, и наоборот, что необходимо для защищенного кода для защиты.
Ответ 3
Мьютекс - это особая реализация semaphore. В частности, это двоичный семафор.
Семафор (очевидно, в контексте компьютерной науки) может быть реализован как целочисленная переменная и аппаратная или программная (оперативная система) примитив, которые являются атомарными (не могут быть прерваны).
Представьте себе что-то вроде (код псевдо-высоты):
int mutex = 1; // The mutex is free when created (free=1, occupied=0).
// in a concurrency block
{
:try-again
// test if mutex is 1 (is free).
if (mutex > 0) {
// mutex WAS free so now I set as occupied (set 0)
--mutex;
// Now I've 'acquired' the mutex and since the mutex is 0 or 1
// only me can be here.
// Do something in mutual exclusion
// Done.
// Unlock the mutex
++mutex;
// Now the mutex is free so other threads can acquire it.
} else {
// Mutex is 0 so I tried but it already occupied.
// Block the thread and try again.
goto try-again;
}
}
Очевидный на чистом высокоуровневом языке этот подход не может работать, потому что поток может быть прерван после того, как он протестировал мьютекс, и он может быть установлен как занятый.
По этой причине семафоры и так мьютекс реализуются с помощью примитивных инструкций, которые реализуют эти "тестовые и заданные" операции в "одном часе" (атомарно).
В качестве примера можно привести примитив test-and-set
.
Ответ 4
Нет такой умности вообще, и заставить ее работать правильно - ответственность программиста.
Мьютекс похож на запираемую дверь в доме, в котором нет стен.
Все, что вы можете сделать с этим, не позволяет другим войти в дом через дверь, когда он заблокирован.
Дверь полезна только в том случае, если все согласятся исключительно войти в дом через дверь и что, когда дверь заблокирована, подождите, пока человек внутри откроет дверь и выйдет.
Ничто не запрещает плохому человеку войти в дом через несуществующие стены, кроме соглашения, которое вы не должны.
Ответ 5
В терминах использования ресурсов компилятору было все равно, о mutex.lock()
. Обязанности программистов - обеспечить доступ к ресурсам при правильной блокировке/разблокировке.
Однако компилятор заботится о мьютексе таким образом, что он не знает, как оптимизировать некоторые конструкции, которые в противном случае были бы - но вы, вероятно, сейчас не заинтересованы в этом.