LINUX.ORG.RU

Синхронизация

 ,


1

6

Здравствуйте. Ребята, накопилось у меня некоторое количество вопросов по многопоточности.
1.

atomic<bool> f{false};
void a(){
  // получаем эксклюзивный доступ
  bool expected = false;
  while(! f.compare_exchange_weak(expected, true, memory_order_relaxed,
                                  memory_order_relaxed)) {expected = false;}

  cout << "mutex lock - ok\n";
  // ... какие-то действия

  // освобождаем мьютекс
  expected = true;
  while(! f.compare_exchange_weak(expected, false, memory_order_relaxed,
                                  memory_order_relaxed)) {expected = true;}
}

int main(){
  thread t1(a);
  thread t2(a);
  t1.join(); t2.join();
}
Такой мьютекс ведь будет валидным (понимаю, что relaxed не даёт гарантий относительно side эффектов не atomic)? Т.е. если один поток записал true, то другой гарантировано не увидит false из своего кэша? Если посмотреть в доки, то они что-то нагоняют сомнений:

4) Write-read coherence: if a side effect (a write) X on an atomic object M happens-before a value computation (a read) B of M, then the evaluation B shall take its value from X or from a side effect Y that follows X in the modification order of M

Т.е. чтобы read следовал за write необходимо организовать happens-before, которого сейчас в коде нет?
2. По f.compare_exchange_weak. spuriously fail - что за ерудна такая. Есть ли у меня такая гарантия: если compare_exchange_weak вернула false, то f не была изменена этим вызовом (судя по всему да, но х.з)?

★★

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

Ни разу не специалист по многопоточности, но так как вопросы достаточно простые...

Такой мьютекс ведь будет валидным (понимаю, что relaxed не даёт гарантий относительно side эффектов не atomic)?

Неа. relaxed даёт гарантии, что текущий поток не выполнит эти операции над этой переменной в обратном порядке.

Т.е. если один поток записал true, то другой гарантировано не увидит false из своего кэша?

Может и увидеть старое значение. Там гарантия в стиле: если какой-то поток увидел новое значение, то больше старое он не увидит.

От relaxed без более сильных вариантов memory order толку мало. Его можно применять, когда есть несколько обновлений переменных и тогда последнее обновление с более сильным memory order сделает видимым и обновления с relaxed. Это вообще странная штука, которая на практике встречается редко (при fail может чаще).

Есть ли у меня такая гарантия: если compare_exchange_weak вернула false, то f не была изменена этим вызовом (судя по всему да, но х.з)?

Написано, что валится проверка, а она должна выполнятся, чтобы обмен произошёл. Так что тело цикла может быть пустым.

P.S. Лучше не заморачиваться с relaxed и weak, а использовать seq_cst или acquire/release как максимум. С атомиками и так очень легко написать неработающий код, а такие вот попытки оптимизации ещё больше повышают вероятность сделать неправильно.

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

relaxed, seq_cst, acquire/release - это всё не о самом атомике, а об окружающих его side эффектах. С этим вопросов нет. Вопрос в том, что существует ли у атомиков свой единый для всех потоков порядок изменений? Если нет порядка, то как вы реализуете std::atomic_fetch_and()? Один поток плюсанул к атомику из своего кэша, а другой к атомику из своего кэша, так выходит? Единый порядок у атомиков должен быть (независимо от memory_order), думаю, вот только не могу найти никаких разъяснений на эту тему.

pavlick ★★
() автор топика

Я тут подумал: compare_exchange_weak/strong - это случайно не о том, что weak возвращает false видя рассинхрон кэшей и не ожидая синхронизации, а strong ожидает?

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

relaxed, seq_cst, acquire/release - это всё не о самом атомике, а об окружающих его side эффектах. С этим вопросов нет.

Как минимум с relaxed это не так. Я уже сказал, какие там гарантии, по сути только атомарность и стабильность появления обновлений.

Вопрос в том, что существует ли у атомиков свой единый для всех потоков порядок изменений?

Само по себе его нет. seq_cst делает total order для всего, вроде как. А release/acquire на одной и той же переменной создают отношение happens-before между парой потоков.

Если нет порядка, то как вы реализуете std::atomic_fetch_and()? Один поток плюсанул к атомику из своего кэша, а другой к атомику из своего кэша, так выходит?

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

Единый порядок у атомиков должен быть (независимо от memory_order), думаю, вот только не могу найти никаких разъяснений на эту тему.

Я же говорю не стоит гадать по таким вопросам, легко ошибиться. Надо читать книги или развёрнутые статьи. Я читал «C++ Concurrency in Action» Anthony Williams, там объясняется разница между memory_order и как они работают.

Я тут подумал: compare_exchange_weak/strong - это случайно не о том, что weak возвращает false видя рассинхрон кэшей и не ожидая синхронизации, а strong ожидает?

Это о том, что не все операции нативно поддерживаются на всех архитектурах и иногда могут не выдать нужный результат, если там не одна атомарная инструкция, а несколько и у потока забрали управление посередине (как один из возможных примеров).

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

Думаю, что вы не правы. Я тут полистал стандарт, давайте посмотрим (если не согласны с моей интерпретацией, то давайте обсудим):

All modifications to a particular atomic object M occur in some particular total order, called the modification order of M. If A and B are modifications of an atomic object M and A happens before (as defined below) B, then A shall precede B in the modification order of M , which is defined below.

Т.е. все операции (и relaxed в том числе) над одним атомиком имеют единый total order. Если А в total oreder раньше, чем В, то А happens before В (т.е. happens before зависит от total order, а не наоборот, как я подумал раньше. Сухие и формальные определения не способствуют пониманию). Ну и дальше read/write coherence подчёркивают связь между общим порядком и happens before связями в духе: первый в total order - значит happens before.

Само по себе его нет. seq_cst делает total order для всего, вроде как. А release/acquire на одной и той же переменной создают отношение happens-before между парой потоков.

Вы путаете, повторю - release/acquire создают связи для сайд эффектов предшествующим операциям над атомиками, но каждого у атомика есть свой порядок модификаций (над ним самим) даже если это relaxed модификации. А seq_cst берёт все порядки отдельных модификаций атомиков и формирует из них единый порядок.

Я читал «C++ Concurrency in Action» Anthony Williams, там объясняется разница между memory_order и как они работают.

Я её тоже читал, не понравилась.

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

А seq_cst берёт все порядки отдельных модификаций атомиков и формирует из них единый порядок.

void thread_1()
{
    x.store(true, memory_order_release);
}
 
void thread_2()
{
    y.store(true, memory_order_release);
}

Сейчас нельзя делать никаких предположений на тему: либо первая запись перед второй, либо вторая перед первой. Для разных потоков может быть различный вариант. Но если пометить записи как memory_order_seq_cst, то все смогут наблюдать единый порядок модификаций.

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

Я не претендую на правоту, могу и сильно ошибаться. Но что-то в этих рассуждениях мне кажется неправильным.

release/acquire создают связи для сайд эффектов предшествующим операциям над атомиками, но каждого у атомика есть свой порядок модификаций (над ним самим) даже если это relaxed модификации.

Насколько я понял, порядок модификаций это одно, а что увидят другие потоки — другое. Т.е. при

atomic.store(1, std::memory_order_relaxed); // time == t1
atomic.store(2, std::memory_order_relaxed); // time == t2
в потоке №1, поток №2 может отработать так:
assert(atomic.load(std::memory_order_relaxed) == 1); // time > t2
Но, на самом деле, это здесь не особо важно так как exchange это read-modify-write операция и она всегда работает с последним значением переменной. Так что код из первого поста работать будет, нюанс с read-modify-write забыл и ошибся. (Сейчас прочитал 1.10 в стандарте, но как-то сильно много ясности по этим вопросам не появилось.)

xaizek ★★★★★
()

расходимся, посоны

1.10.7: In addition, there are relaxed atomic operations, which are not synchronization operations

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.