Pull to refresh

Comments 4

Модель памяти Linux не считает такие ситуации отношением “happens-before” между потоками, ведь ни одна из операций не имеет acquire или release-семантики и порядок между ними, строго говоря, не определён.

А транзитивность здесь не поможет? В том смысле, что WRITE_ONCE happens-before smp_mb, smp_mb happens-before READ_ONCE => WRITE_ONCE happens-before READ_ONCE.
Или вокруг smp_mb happens-before тоже не определяют?


(Их модель уже с ~10 отношениями настолько запутана, что боюсь потерять какую-то мелкую, но критическую деталь.)

Или вокруг smp_mb happens-before тоже не определяют?

smp_mb() сам по себе не влияет на отношения между потоками (в частности, на smp_mb() в другом потоке). Внутри одного потока для кода


    WRITE_ONCE(a, 1);
    x = READ_ONCE(b);

и так есть WRITE_ONCE happens-before READ_ONCE — в пределах одного потока, не для других:


    WRITE_ONCE(a, 1);
           |
           v
    x = READ_ONCE(b);

Но! здесь нет happens-before чего-либо в другом потоке, так что другой поток — в отсутствие smp_mb() — может наблюдать произвольный порядок этих операций относительно своих.


Когда добавляется smp_mb(), то он формирует отношение “program-order” — исполнение сторого в порядке следования в программе (наблюдаемое в том числе в других потоках). Он, во-первых, не даёт компилятору переносить инструкции через барьер (в обе стороны), а во-вторых, вставляет нужную магию для процессора, чтобы все записи были сделаны и видны после барьера другим процессорам. Это как раз обеспечивает транзитивность, чтобы операции перед барьером были видны.


При этом никаких отношений happens-before между потоками не было и нет, в строгом смысле модели памяти Linux, которая предписывает операциям исполняться в определённом предсказуемом порядке. Если по модели выходит R-1 ->hb W-2 — то ядро хочет, чтобы так было всегда (при «одновременном» исполнении их процессорами), а не как повезёт.


Есть “program-order” между READ_ONCE() и WRITE_ONCE() каждого из потоков отдельно. Есть “from-reads” между READ_ONCE() и WRITE_ONCE() в разных потоках. Нет “happens-before” нигде.

Я понимаю, о чём вы, но давайте посмотрим вот на что: пусть мы сравним с отношением между release и acquire. Например, так:
Тред 1:
foo = 222;
smp_store_release(flag, 1);
тред 2:
if (smp_load_acquire(flag) == 1) {
printk("foo=%d\n", foo);
}


Запись и чтение foo связаны по happens-before только если тред 2 получил на чтении flag==1 и соотственно пошёл читать foo, так? Но он может и не получить такой flag и не пойти в соответствующее чтение. То есть, happens-before тут возникает только если ещё есть какое-то дополнительное условие — что звёзды легли именно как нужно в этот момент (ну или мы описываем за счёт условия входа в чтение foo — если мы его читаем, то условие выполнилось).


Вот теперь ровно при этом же условии — можем ли мы полагать, что за счёт барьеров у нас есть такая же гарантия в рассматриваемом коде? Вы пишете дальше:


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

то есть сами утверждаете, что эта гарантия тут есть, так?
Тогда чем она отличается, кроме (возможно) названия?
Если у неё своё название, то какое оно?

Запись и чтение foo связаны по happens-before только если тред 2 получил на чтении flag==1 и соотственно пошёл читать foo, так?

Не совсем: в случае с acquire-release потоки связаны happens-before независимо от того, что увидит второй поток во flag. Это не как в том анекдоте, что могу увидеть динозавра, а могу не увидеть — значит вероятность 50%.


Связь “A happens-before B” — это не утверждение «A в потоке 1 происходит до B из потока 2, будь хоть потоп, хоть война». B не ждёт A. Happens-before — это термин из модели памяти. Отношение happens-before задаёт порядок только если операции происходят «одновременно» — в смысле, рассмотренном в первой статье — то есть если они уже не упорядочены чем-то ещё: например, другими acquire-release операциями и барьерами перед/после этих, из-за которых они происходят уже в »разное» время.


Например, если первый поток запускается после того, как второй делает smp_store_release() — между ними уже есть отношение порядка. И happens-before между smp_store_release() и smp_load_acquire() не влияет на порядок исполнения, потому что они не происходят одновременно. И конечно же в этом случае второй поток видит в flag ноль, и happens-before «не работает» — но никуда не девается. Happens-before — это не порядок исполнения в общем случае, это порядок исполнения при «одновременности».


Другой аспект, из-за которого второй поток может увидеть не единицу — это если есть ещё третий поток, который записывает в флаг тройку и успевает (или нет) перезаписать единицу, записанную первым потоком.


поток 1                             поток 3
---------------------------         ---------------------------
foo = 111;                          foo = 333;
smp_store_release(flag, 1);         smp_store_release(flag, 3);
       |                                          |
       |                 поток 2                  |
       |     --------------------------------     |
       +---> if (smp_load_acquire(flag) > 0) <---+
                 printk("foo=%d\n", foo);

Между потоком 1 и потоком 2 есть happens-before. Между потоком 2 и потоком 3 есть happens-before. Между потоком 1 и потоком 3 нет happens-before, потому что release и release в разных потоках никаких отношений не формируют. И о значении flag они ничего не говорят. Если всё это происходит «одновременно», то вы не можете сказать, что именно напечатает printk() — между “foo = 111” и “foo = 333” нет happens-before.


Что вы можете сказать — так это что в foo будет видно либо 111, либо 333 (если никто кроме этих двух туда ничего не может записать «одновременно» со всей этой чехардой), а не предыдущее значение, скажем.


Похожие «гарантии» дают полные барьеры памяти — модель памяти ядра называет это “preserved program order”. Они гарантируют, что все предыдущие операции выполнились. Если вы сделаете


поток 1                             поток 3
---------------------------         ---------------------------
WRITE_ONCE(foo, 111);               WRITE_ONCE(foo, 333);
smp_mb();                           smp_mb();
WRITE_ONCE(flag, 1);                WRITE_ONCE(flag, 3);
smp_mb();                           smp_mb();
    |                 поток 2          |
    |     ------------------------     |
    +---> if (READ_ONCE(flag) > 0) <---+
              printk("foo=%d\n", foo);

то тоже можете рассчитывать увидеть в foo либо 111, либо 333. Без гарантий конкретного значения и порядка применения, но с гарантиями, что операции потоков 1 и 3 видны.


Обратите внимание на отсутствие гарантий порядка. Барьеры упорядочивают операции скопом: «всё что идёт до барьера happens-before того того, что идёт после барьера». Если сделать


поток 1                             поток 3
---------------------------         ---------------------------
WRITE_ONCE(foo, 111);               WRITE_ONCE(foo, 333);
WRITE_ONCE(foo, 222);               WRITE_ONCE(foo, 444);
smp_mb();                           smp_mb();
WRITE_ONCE(flag, 1);                WRITE_ONCE(flag, 3);
smp_mb();                           smp_mb();
    |                 поток 2          |
    |     ------------------------     |
    +---> if (READ_ONCE(flag) > 0) <---+
              printk("foo=%d\n", foo);

то на x86 вы увидите либо 222, либо 444. Но на менее строгих архитектурах, типа Alpha, может выйти и 111, и 333 (потому что записи попали в разные кусочки кеша и поток 2 увидел один раньше другого).


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

Sign up to leave a comment.

Articles