LINUX.ORG.RU

Порядок событий нескольких relaxed atomic'ов

 ,


0

3

Привет. Сперва код, затем пояснение и вопрос:

#include <cassert>
#include <thread>
#include <atomic>
#include <vector>
#include <iostream>
using namespace std;

enum mtx_state {
   e_free,
   e_busy,
   e_block
};
std::atomic<mtx_state> mtx = e_free;
std::atomic_int cnt = 0;
std::atomic_flag barrier;
int result;

void t0() {
   while (! barrier.test(memory_order_relaxed));

   mtx_state expected = e_free;
   while (! atomic_compare_exchange_strong_explicit(&mtx, &expected, e_busy,
            memory_order_relaxed, memory_order_relaxed))
      if (expected == e_block)
         return;
      else
         expected = e_free;

   cnt.fetch_add(1, memory_order_relaxed);

   expected = e_busy;
   if (! atomic_compare_exchange_strong_explicit(&mtx, &expected, e_free,
         memory_order_relaxed, memory_order_relaxed))
      terminate();
}

void t1() {
   while (! barrier.test(memory_order_relaxed));

   mtx_state expected = e_free;
   while (! atomic_compare_exchange_strong_explicit(&mtx, &expected, e_block,
            memory_order_relaxed, memory_order_relaxed))
      expected = e_free;
   result = cnt.load(memory_order_relaxed);
}

int main() {
   if (true) {
      vector<jthread> th;
      for (int i = 0;  i != 30;  ++i)
         th.emplace_back(t0);

      th.emplace_back(t1);

      barrier.test_and_set();
   }

   assert(cnt.load(memory_order_relaxed) == result);
   cout << result << endl;
}

Идея примера - стартует 30 потоков, на входе долбятся о барьер для одновременного запуска. Задача каждого из них - взять «мьютекс» (атомик с несколькими состояниями - {свободно, занято, блокировка}, если блокировка, то поток завершается без выполнения работы) и инкрементировать счетчик. Также вместе с этими 30 стартует 1, который блокирует «мьютекс» и читает значение из счетчика в итоговый result.

Дело всё вот в чем - взятие и освобождение мьютекса - relaxed операция. Значит когда блокирующий поток финально захватывает мьютекс, то он как бы не обязан увидеть все изменения, которые возглавлял последний release мьютекса, очевидно, что среди них находится инкремент счетчика.

Если счетчик не атомик, то вопроса бы не было, но он атомик. Возможна ли ситуация, когда последнее (блокирующее) взятие мьютекса увидит непоследнее состояние счетчика? Т.е. блокирующий поток читает счетчик, 10 например, кладет его в result, блокирует мьютекс, дальше каким-то чудом счетчик инкрементится, срабатывает assert, это возможно?


Ответ на: комментарий от anonymous

Речь про многопоток. Обычно синхронизация обеспечивается путем - есть поток X, который что-то писал куда-то, в конце он делает release запись. Есть поток Y, он acquire читает записанное в атомик потоком X и видит всё, что он видел на момент записи данного значения в атомик.

Но тут случай особый - запись в атомик есть, но она не является release, а чтение не acquire, а значит при чтении Y не получает в свою «видимость» контекст потока X в момент записи. Если счетчик в моем примере (cnt) был бы обычным int’ом - то всё было бы ясно, но счётчик и сам атоимк, может ли у атомика быть различная наблюдаемая история модификаций (с точки зрения различных потоков)? Как тогда делать инкремент? Например, один поток увидел 5, прибавил 1, не может же другой поток опять увидеть 5 и вновь прибавить 1? С другой стороны операции с атомиками могут быть с тагом memory_order_seq_cst, что как бы устанавливает один общий порядок модификации с точки зрения всех потоков (выходит, что иначе он разный?).

В общем я не знаю, у меня есть аргументы как «за», так и «против»

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

Теперь понял.

memory_order_relaxed - гарантирует только атомарность конкретного атомика.

Для гарантий видимости изменениий других переменных/атомиков надо смотреть другие memory_order_*, например memory_order_release

https://en.cppreference.com/w/cpp/atomic/memory_order#Release-Acquire_ordering

All memory writes (including non-atomic and relaxed atomic) that happened-before the atomic store from the point of view of thread A, become visible side-effects in thread B. That is, once the atomic load is completed, thread B is guaranteed to see everything thread A wrote to memory. This promise only holds if B actually returns the value that A stored, or a value from later in the release sequence.

anonymous
()
Ответ на: комментарий от anonymous2

а я утверждал что шаред мютекс не работает?

я говорю о производительности

просто мютекс обгоняет шаред мютекс

ситуации когда это наоборот, сильно специфические

был видос с какой то конфы где это один чел показывал и изобретал свой

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

просто мютекс обгоняет шаред мютекс

какой просто мютекс? где ты его здесь увидел?

и ты же конечно в курсе зачем для std::shared_mutex сделали shared_lock и почему его нет в обычном мютекс?

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

обычный мютекс это обычный мютекс

а шаред мютекс это обычный мютекс + кондишин вариабл

либо в зависимости от имплементации это шаред/еклюзив лок

вообщем загляни уже что ли в имплементации шаред мютекса

и не в одну, в во все stl gcc/clang/msvc

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

ты не ответил зачем сделали shared_lock для shared_mutex - потому что в этом уже есть ответ, перевел тему на реализации, и вообще хватит уже прятаться под аноном чего ты зассал?

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

std::shared_lock сделали для игры с методами (lock/unlock)_shared в std::shared_mutex

меня нет на форуме, не вижу смысла регистрироваться

а зазвездевшемуся анонимусу лучше подучить теорию

ну и самое полезное это по больше практики и по меньше теории

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

std::shared_lock сделали для игры с методами (lock/unlock)_shared в std::shared_mutex

сути ответа от тебя нет (или ты его просто не знаешь и это хорошо, тупаны пусть подметают улицы), иди играй дальше

нужно просто почитать что дает shared_lock для shared_mutex - а потом подумать чем же таким занимаются 29 нитей из 30 из топика задачи.

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

Ну в общем-то да, в самом же cppreference дан ответ (assert не стработает только seq_cst):

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
}

Т.е. несмотря на то, что у атомика обязан быть одна единственная история собственнх модификаций, нет никаких гарантий что чтение в некотором потоке вернет последнюю модификацию. Это несколько контринтуитивно, ведь как тогда делать fetch_add(…)? Ведь нужно сначала получить последнее значение, а лишь потом делать add. Видимо, запись в атомик производит какие-то доп синхронизации кэшей, который отсутствую при голом чтении

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