Упорядочение неатомарных операций за счет использования атомарных операций в качестве основы для средств синхронизации более высокого уровня
Сначала я привожу описание Энтони Уильямса "Параллельность в C++":
class spinlock_mutex
{
std::atomic_flag flag;
public:
spinlock_mutex():
flag(ATOMIC_FLAG_INIT)
{}
void lock()
{
while(flag.test_and_set(std::memory_order_acquire));
}
void unlock()
{
flag.clear(std::memory_order_release);
}
};
Операция lock () представляет собой цикл в flag.test_and_set() с использованием std::memory_ order_acquire, а unlock () - это вызов flag.clear() с std:: memory_order_release. Когда первый поток вызывает lock(), флаг изначально очищается, поэтому первый вызов test_and_set () установит флаг и вернет false, указывая, что этот поток теперь имеет блокировку, и заканчивая цикл. Затем поток может свободно изменять любые данные, защищенные мьютексом. Любой другой поток, который вызывает lock () в это время, найдет уже установленный флаг и будет заблокирован в цикле test_and_set ().
Когда поток с блокировкой завершил изменение защищенных данных, он вызывает unlock(), который вызывает flag.clear() с семантикой std:: memory_order_release. Затем он синхронизируется с (см. Раздел 5.3.1) последующим вызовом flag.test_and_set() из вызова lock () в другом потоке, потому что этот вызов имеет семантику std:: memory_order_acquire. Поскольку модификация защищенных данных обязательно секвенируется перед вызовом unlock(), эта модификация происходит перед unlock () и, таким образом, происходит перед последующим вызовом lock () из второго потока (из-за соотношения между синхронизацией и разблокировкой). () и блокировки ()) и происходит до того, как любой доступ к этим данным из этого второго потока, как только он получил блокировку.
Q: Если есть только два потока, и поток A имеет объект m1
Запускает lock()
впервые, и поток B имеет объект m1
Запускает lock()
впервые перед m1
ссылающееся unlock()
в теме, почему flag.test_and_set(std::memory_order_acquire)
получить истинное, а не ложное (начальное значение), когда m1
Запускает lock
функция в потоке B?
Я знаю последовательность выпуска, но для создания последовательности выпуска требуется атомарный объект, вызывающий атомарную операцию с std::memory_order_release
и нет операции, вызванной с std::memory_order_release
,
2 ответа
acquire
а также release
семантика относится к другому (защищенному) ресурсу, здесь не показан. В частности, не перемещайте доступ после блокировки или до разблокировки. Сами атомные операции полностью упорядочены.
Поскольку операции полностью упорядочены, ваш гипотетический порядок A:lock, B:lock, A:unlock
рассматривается в одном и том же порядке обеими нитями. Следовательно, когда поток B вызывает lock
Видит только lock
от А, а не unlock
,
Нить, делающая вещи друг перед другом, на самом деле не имеет смысла, кроме поведения, которое вы хотели бы знать. Memory_order не входит в это. Он определяет порядок упорядочения неатомарного доступа к памяти вокруг атомарной операции.
Причины для этого в том, что если вы делаете:
lock();
foo();
unlock();
В двух потоках foo в одном потоке не может прочитать или сразу перед блокировкой или после разблокировки рассматриваемого потока. Это в сочетании с атомарностью самих замков и разблокировок дает ожидаемое нами поведение. (т.е. нет одновременного доступа из foo).
Здесь только один std::atomic_flag
, В любой момент это либо установлено (true
) или очистить (false
).
std::atomic_flag::test_and_set
определяется как
Атомно меняет состояние
std::atomic_flag
установить (true
) и возвращает значение, которое оно удерживало ранее.
Когда А позвонил lock
, он изменил установленный флаг, поэтому состояние, возвращаемое, когда B пытается заблокировать, установлено. Это оценивается как условие while
так что цикл продолжается. Поток B будет продолжать вращаться в этом цикле, пока не будет снята блокировка.
Наконец, когда A вызывает разблокировку, флаг меняется на очистить. Затем B может проверить снова, и false
заканчивает цикл