LINUX.ORG.RU

Sequentially-consistent ordering для мьютексов

 ,


0

4

Приветствую. Мьютексы не обеспечевуют sequentially-consistent порядок, т.е. если существуют два мьютекса, которые контролируют доступ каждый к своим данным, в некотором потоке сначала берем первый мьютекс, модифицируем, берем второй мьютекс, модифицируем. Для данного потока порядок четкий - сначала 1 затем 2, для остальных же потоков вполне может быть сначала 2 затем 1.

Вопрос, как сделать sequentially-consistent порядок для всех потоков? Идею выражу в коде:

#include <thread>
#include <mutex>
#include <cassert>
using namespace std;

#define WITH_FENCE 0

int a_data, b_data;
mutex a_mtx, b_mtx;
atomic_int cnt = 0;

void write_a() {
   lock_guard lck(a_mtx);
   a_data = 5;
#if WITH_FENCE
   atomic_thread_fence(std::memory_order_seq_cst);
#endif
}

void write_b() {
   lock_guard lck(b_mtx);
   b_data = 5;
#if WITH_FENCE
   atomic_thread_fence(std::memory_order_seq_cst);
#endif
}

void check() {
   while (true) {
      lock_guard lck(a_mtx);
#if WITH_FENCE
      atomic_thread_fence(std::memory_order_seq_cst);
#endif
      if (a_data) {
         lock_guard lck_1(b_mtx);
#if WITH_FENCE
         atomic_thread_fence(std::memory_order_seq_cst);
#endif
         cnt.fetch_add(1, memory_order_relaxed);
         return;
      }
   }
}

int main() {
   jthread t0(check);
   jthread t1(check);
   jthread t0(write_a);
   jthread t1(write_b);
   assert (cnt == 0  || cnt == 2);
}

Если WITH_FENCE == 0, то порядка не будет точно. Поможет ли WITH_FENCE = 1? Если поможет, то что будет, если будут и другие потоки, которые будут брать данные мьютексы без барьера, будет ли сохранен порядок пусть и с какими-то вмешательствами из других потоков в рандомных местах (вне единого порядка, значит каждый поток видит их по-разному), но все же можно будет сказать в отношении упорядоченных событий (которые с барьером), что условное событие 2 не происходит раньше 1? В общем велком, если написал спутанно последнюю идею, дайте знать, приведу пример.

UPD: для main нет четкого порядка, код поправил



Последнее исправление: kvpfs_2 (всего исправлений: 3)

брать данные мьютексы без барьера

Мьютекс - это абсолютно точно full-barrier, если в этом вопрос.

bugfixer ★★★★
()
Ответ на: комментарий от bugfixer

x86 да, там это вообще sequentially-consistent доп инструкций не генерит. Но хочу я хочу разобраться как правильно с точки зрения крестов. Вообще по идее после взятия/отпускания мьютекса можно делать чтение/запись atomic с sequentially-consistent меткой, но ведь и барьеры для чего-то нужны, почему бы не здесь? Ну и вопросы по смешиванию упорядоченных и неупор-ых операций не отпадают.

kvpfs_2
() автор топика

В Linux мьютексы работают через механизм, называемый «фьютексами» (futex). Если в данный момент есть несколько потоков, желающих захватить конкретный мьютекс, то делается системный вызов futex(2). В таком случае память полностью синхронизируется, ибо переключение контекста. Если конкуренции нет, то всё может остаться в юзерспейсе, но тогда и надобности в упорядочении нет. В других ОС реализация может быть такой, что системный вызов делается всегда. А вот чтобы его совсем не было — вряд ли такое можно себе представить.

annulen ★★★★★
()
Ответ на: комментарий от annulen

Ребят, акцент именно на валидности с точки зрения крестов хотелось бы сделать, нужен sequentially-consistent порядок для двух мьютексов средствами и гарантиями плюсов.

kvpfs_2
() автор топика
Ответ на: комментарий от kvpfs_2

Но хочу я хочу разобраться как правильно с точки зрения крестов.

Если Вам интересно как оно «по букве закона» - потереблю наших «гуру», но они все разбежались на Xmas пока. Так что до 3го января - скорее всего ответа не будет.

bugfixer ★★★★
()

У тебя каша в голове. Переупорядочивание мьютексов - надо же такое придумать. Ты, похоже, слышал звон. С многими мьютексами есть другая проблема, которую решает std::lock.

rupert ★★★★★
()

Предлагаю std::scoped_lock

The class scoped_lock is a mutex wrapper that provides a convenient RAII-style mechanism for owning zero or more mutexes for the duration of a scoped block.

When a scoped_lock object is created, it attempts to take ownership of the mutexes it is given. When control leaves the scope in which the scoped_lock object was created, the scoped_lock is destructed and the mutexes are released. If several mutexes are given, deadlock avoidance algorithm is used as if by std::lock.

https://en.cppreference.com/w/cpp/thread/scoped_lock

imatveev13
()
Ответ на: комментарий от rupert

У тебя каша в голове.

Вопрос не в переупорядочнивании мьютексов. Предположим 2 потока на разных ядрах (а ещё лучше - на разных сокетах) прошли через lock. Является ли это полным барьером или нет даже если спать не приходилось? Я думаю в этом вопрос. Лично я всегда думал что да, но что-то меня заставили сомневаться. Я весьма уверен на x86 оно так, но что там стандарт говорит по этому поводу - я реально не знаю.

bugfixer ★★★★
()
Последнее исправление: bugfixer (всего исправлений: 1)
Ответ на: комментарий от rupert

Ты точно понял суть вопроса и что такое Sequentially-consistent ordering? Вставлю из справочника

This example demonstrates a situation where sequential ordering is necessary. Any other ordering may trigger the assert because it would be possible for the threads c and d to observe changes to the atomics x and y in opposite order.

#include <atomic>
#include <cassert>
#include <thread>
 
std::atomic<bool> x = {false};
std::atomic<bool> y = {false};
std::atomic<int> z = {0};
 
void write_x()
{
    x.store(true, std::memory_order_seq_cst);
}
 
void write_y()
{
    y.store(true, std::memory_order_seq_cst);
}
 
void read_x_then_y()
{
    while (!x.load(std::memory_order_seq_cst))
        ;
    if (y.load(std::memory_order_seq_cst))
        ++z;
}
 
void read_y_then_x()
{
    while (!y.load(std::memory_order_seq_cst))
        ;
    if (x.load(std::memory_order_seq_cst))
        ++z;
}
 
int main()
{
    std::thread a(write_x);
    std::thread b(write_y);
    std::thread c(read_x_then_y);
    std::thread d(read_y_then_x);
    a.join(); b.join(); c.join(); d.join();
    assert(z.load() != 0); // will never happen
}

Та же петрушка с разными мьютексами, порядок модификации (взятия/освобождения) может отличаться с точки зрения разных потоков, во всяком случае я не видел ничего их в этом ограничивающего.

kvpfs_2
() автор топика
Последнее исправление: kvpfs_2 (всего исправлений: 1)
Ответ на: комментарий от bugfixer

А разница? С точик зрения плюсов, гарантируется только synchronizes-with между unlock/lock, это простая atomic(release/aquire) операция без всякой конситсентности. Полностью отвечающий стандарту мьютекс можно сделать на spin lock’e.

kvpfs_2
() автор топика
Ответ на: комментарий от kvpfs_2

С точки зрения плюсов, гарантируется только synchronizes-with между unlock/lock

Полностью отвечающий стандарту мьютекс можно сделать на spin lock’e

CAS на самом деле хватит. Но я не понимаю какое это имеет отношение к изначальному вопросу?

bugfixer ★★★★
()
Ответ на: комментарий от bugfixer

Но я не понимаю какое это имеет отношение к изначальному вопросу?

Так все ведь об одном. Если mutex по гарантиям не отличается от aquire/release, то пример выше показывает, когда единого порядка нет, значит нет его и у mutex’a. Значит нужно что-то костылить дополнительно.

Вот бы какую-нибудь архитектуру на руках, где всё очень-очень weak ordered и потестить свои какие-то идеи. А то получается, голая теория на кофейной гуще. Может есть какой-то эмулятор?

kvpfs_2
() автор топика
Ответ на: комментарий от kvpfs_2

Вот бы какую-нибудь архитектуру на руках, где всё очень-очень weak ordered

Зачем?

и потестить свои какие-то идеи. А то получается, голая теория на кофейной гуще.

Вы начали с мьютекса. И гарантий на memory ordering связанный с этим. А потом свалились в очень low-level synchronization primitives. Вы уж определитесь чего хочется.

Может есть какой-то эмулятор?

Без руля. Более того - практического применения не вижу.

bugfixer ★★★★
()
Ответ на: комментарий от kvpfs_2

Если mutex

Это очень и очень высокоуровневый примитив синхронизации. Вы можете озвучить чего добиться хочется?

bugfixer ★★★★
()
Ответ на: комментарий от bugfixer

Вы начали с мьютекса. И гарантий на memory ordering связанный с этим. А потом свалились в очень low-level synchronization primitives. Вы уж определитесь чего хочется.

Да мьютекс не далеко ушёл, по гарантиям, это та же дрочка atomic флага в aquire цикле и release запись при освобождении, всё, там больше ничего нет (с точки зрения гарантируемы эффектов). Если он там как-то иначе реализован где-то конкретно, то не правило.

kvpfs_2
() автор топика
Ответ на: комментарий от kvpfs_2

Да мьютекс не далеко ушёл, по гарантиям

Умоляю.

та же дрочка atomic флага в aquire цикле и release запись при освобождении, всё, там больше ничего нет

Вы глубоко заблуждаетесь.

Озвучьте таки фундаментальную проблему что вы решаете?

bugfixer ★★★★
()
Ответ на: комментарий от bugfixer

Это очень и очень высокоуровневый примитив синхронизации. Вы можете озвучить чего добиться хочется?

Простая задача - два объекта - А, Б. Каждый под своим мьютексом, сначала модифицируем А, затем Б. Все остальные потоки должны увидеть именно такой порядок, а не Б, а следом А. Можно вкрутить общий мьютекс для этого, но я захотел сделать sequentially-consistent порядок, для этого он и нужен в конце то концов.

kvpfs_2
() автор топика

Sequentially-consistent ordering для мьютексов

А по-русски?

Для данного потока порядок четкий - сначала 1 затем 2, для остальных же потоков вполне может быть сначала 2 затем 1.

Порядок одних и тех же событий во всех потоках всегда одинаковый. Проблема только в том, что он может отличаться от написанного в исходном коде.

Идею выражу в коде:

У тебя ж там race, практически гарантированный.

main_thread: write_a();

тут все мютексы свободны

check_thread: lck(a_mtx);
check_thread: if (a_data) - true
check_thread: lck_1(b_mtx);
check_thread: assert(b_data); - false

Чтобы избежать надо оба write под общий мютекс.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)

Ребята, я ведь вообще жестко затупил. Если мы из одного потока по очереди берем два разных мьютекса, то никогда второй не станет виден другим раньше первого. Т.е. это примерно то же что и:

atomic_int i1, i2;
i1.store(4, release);
i2.store(4, release);

Всё, о чем я писал, может произойти при записи из разных потоков. В общем меня это не сильно то волнует, мне нужен порядок при записи в одном.

Спасибо.

Но если кто-то хочет провести ликбез по sequentially-consistent, то велком. Я вот не очень понимаю что если миксовать его с relaxed, например. Ну и в качестве учебного кейса - вкрутить данный порядок в первоночальный пост, я его исправил, теперь запись происходит из разных потоков.

kvpfs_2
() автор топика
Ответ на: комментарий от kvpfs_2

Ребята, я ведь вообще жестко затупил.

тут главное тупить, круто обмазавшись терминологией из англоязычной литературы.

там и проблемы-то нет вообще, а ее таки нашли и обозвали, прости господи, «sequentially-consistent ordering».

если тред A ждет мьютекс a, B - мьютекс b, и их отпустили в порядке a, b, это вовсе не значит что A получит управление раньше чем B. Порядок тут зависит от разных причин, например стратегии активации ожидающих тредов, приоритетов, и т.п. Это естественно и не проблемой не является.

alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 1)
Ответ на: комментарий от alysnix

Порядок тут зависит от разных причин

Пример остался старый с новыми элементами, тут неверно ставить ассерт, надо инкрементить счетчик. Конкретный порядок тут незивестен, но в случае, прости господи, «sequentially-consistent ordering» он будет одним для всех.

PS: исправил код

kvpfs_2
() автор топика
Последнее исправление: kvpfs_2 (всего исправлений: 1)
Ответ на: комментарий от kvpfs_2

может вы имеете ввиду вовсе не это, но порядок запуска тредов и получения ими управления зависит от стратегии планировщика, а они могут быть от наивных до хитрож.ых.

все что гарантируется для мьютексов, это лишь то, что мьютекс X будет захвачен ожидающим тредом строго после того, как владеющий его освободит. на этом и строится вся синхронизация.

но вовсе не гарантируется что, что если тред A освободил мьютекс, то сразу будет переключение на тред B, что ожидает его освобождения. В общем смысле B попадает в список активных тредов и ждет своей очереди. в зависимости от наличия иных активных тредов, их приоритетов и прочих параметров, и стратегии планировщика, реально будет переключение на тред B, лишь когда планировщик этого захочет.

alysnix ★★★
()
Ответ на: комментарий от raspopov

Ознакомьтесь, пожалуйста, например, со статьёй «Lock-free структуры данных. Основы: Модель памяти» и особенно вдумчиво раздел «Барьеры компилятора».

Отличная статья, вернее цикл статей. Очень многое для меня прояснилось. Спасибо за ссылку.

blex ★★
()

Для данного потока порядок четкий - сначала 1 затем 2, для остальных же потоков вполне может быть сначала 2 затем 1.

Нет не может быть.

Порядок захвата блокировок должен быть один всегда, иначе словишь дэдлок.

Все нормальные люди документируют порядок захвата блокировок, а за его несоблюдение кодерков сношают на ближайшем ревью кода.

Для тех, кто не достаточно дисциплинирован, придумали мониторы.

shkolnick-kun ★★★★★
()
Последнее исправление: shkolnick-kun (всего исправлений: 1)
Ответ на: комментарий от shkolnick-kun

Речь была не о порядке захвата мьютексов, а о порядке появления событий в разных потоках. Эту задачу и призваны решать sequentially-consistent теги в соответствующих функциях.

У меня ощущение, что в сети это вообще мало кто понимает, куча неосвещенных моментов. По-моему, надо эмулировать какой-нибудь ARM условным QEMU и запускать тесты. А ещё лучше - какой-нибудь спец эмулятор, который будет жестко «драть» за любые ошибки синхронизации, всю дичь которая возможна - он делает.

kvpfs_2
() автор топика
Последнее исправление: kvpfs_2 (всего исправлений: 1)
Ответ на: комментарий от kvpfs_2

Речь была не о порядке захвата мьютексов, а о порядке появления событий в разных потоках. Эту задачу и призваны решать sequentially-consistent теги в соответствующих функциях.

Что, простите???

Порядок появления каких событий, случайных? Серьёзно?

Или ты не про события говоришь?

shkolnick-kun ★★★★★
()
Ответ на: комментарий от shkolnick-kun

Я о порядке наблюдаемых модификаций из разных потоков (которые запущены на разных ядрах), когда данные модификации не возглавлены какой-либо общей для всех release операцией. Я уже цитировал справочник Sequentially-consistent ordering для мьютексов (комментарий), суть показывает.

kvpfs_2
() автор топика
Ответ на: комментарий от kvpfs_2

А что мешало прочесть предыдущие два абзаца на cppreference.com???

Sequential ordering may be necessary for multiple producer-multiple consumer situations where all consumers must observe the actions of all producers occurring in the same order.

Total sequential ordering requires a full memory fence CPU instruction on all multi-core systems. This may become a performance bottleneck since it forces the affected memory accesses to propagate to every core.

Это нужно для решения проблемы «производитель-потребитель», когда каждый потребитель должен знать что сделал каждый производитель.

Так вот, если верить педивикии, то в случае не атомиков проблема решается либо одним мьютеском и двумя условными переменными, либо одним мьютексом и двумя счётными семафорами, а не кучей мьютексов, как у тебя.

Причём решение на атомиках аналогично, ибо требует глобального барьера памяти, т.е. глобальной блокировки на уровне железа.

shkolnick-kun ★★★★★
()
Последнее исправление: shkolnick-kun (всего исправлений: 2)
Ответ на: комментарий от shkolnick-kun

Соррян, сейчас не могу вступать в дискуссию, голова зибита другим. То, что можно обойтись другими путями - наверняка, в общем-то я так и делаю - тупо избегаю Sequentially-consistent операций, из-за непонимания их на должном уровне. Например, как уже писал, если операции над атомиками состоят из микса послед-консистентых и нет операций, чего ожидать, какие гарантии в таких случаях.

kvpfs_2
() автор топика
Ответ на: комментарий от kvpfs_2

Атомики нужны, когда нет mutex. Например, для всяких lock-free структур данных (а также сами mutex внутри реализуются на атомиках так или иначе). Если ты работаешь с данными под mutex-ами, ты можешь забить на атомики и работать с данными как если бы других потоков не было, пока держишь mutex. Вызов функции захвата/освобождения mutex уже сам по себе обвешан всеми необходимыми метками синхронизации данных.

KivApple ★★★★★
()
Ответ на: комментарий от kvpfs_2

конечно, было бы интересно

Я обещал - я сделал. Любая операция на mutex это full memory barrier. Инфа напрямую от C++ committee member.

bugfixer ★★★★
()
Ответ на: комментарий от bugfixer

Благодарю за хлопоты. Но не уверен, что мы говорим об одном

kvpfs_2
() автор топика
Для того чтобы оставить комментарий войдите или зарегистрируйтесь.