LINUX.ORG.RU

Атомарные операции

 ,


0

1

Здесь написано следующее:

All writes in other threads that release the same atomic variable are visible in the current thread

All writes in the current thread are visible in other threads that acquire the same atomic variable

Это про acquire и release. Это обобщённая модель или существует/существовало железо, где действительно можно было не увидеть изменения из другого потока? Если да, то с какой целью такое поведение у железа было сделано?

Это обобщённая модель или существует/существовало железо, где действительно можно было не увидеть изменения из другого потока? Если да, то с какой целью такое поведение у железа было сделано?

Чо?

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

hateyoufeel ★★★★★
()

Это обобщённая модель или существует/существовало железо, где действительно можно было не увидеть изменения из другого потока? Если да, то с какой целью такое поведение у железа было сделано?

Конечно, это абсолютно любое адекватное железо мало-мальски умеющее в многоядерность. Ты же не будешь просто так на каждое изменение произвольного значения в локальном кэше дёргать другие ядра?

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

Ну х86 как-то без внешней помощи с кешами управляется. Или это плохой пример в контексте многоядерности? Какое железо тут будет уместно рассмотреть?

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

Насчёт железа - у бытовых x86 с этим всё более-менее в порядке. На серверных многопроцовых платах могут быть проблемы, да. Но железо это только часть проблем, кроме него их может создать оптимизатор в компиляторе - он может переставлять местами разные части исходного кода, если конкретно в этом треде это не повлияет на результат. А вот влияние на то, что в итоге будут видеть другие треды, никак не гарантируется. Если же ты явно используешь инструменты синхронизации то компилятор их учтёт и будет знать, чего нельзя делать.

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

На серверных многопроцовых платах могут быть проблемы, да.

Как показывает опыт, если софт явно не умеет NUMA, лучше его прибивать гвоздями к одному физическому процессору. А то может быть бо-бо.

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

Про компилятор я в курсе. Про железо не в курсе. Выше сказали про кеши как причину. Если это так, то должно быть достаточно запустить что-то такое

#include <threads.h>
int main() {
  int f(void* data) { return *(volatile int*)data = 1, 0; }
  volatile int data = 0;
  thrd_create(&(thrd_t){0}, f, &data);
  while (!data);
  return !(data == 1);
}

и получить !0. Я попробовал для ppc и arm, но не прокатило. Но это на godbolt и там наверное эмулятор какой-то, а не настоящее железо.

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

Не понял как этот пример должен сломаться. Разве что между концом while и началом return главный тред перекинет на другой физический проц, который ещё не знает про data==1. Шансы очень малы, надо очень много раз запускать. Возможно ещё какие-то условия надо соблюсти.

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

Это вообще как работать должно, по-твоему? «Что-то такое» должно быть нормальным приложением с несколькими потоками достаточно долго работающими параллельно и прибитыми к разным ядрам. Ну и да, никто на годболте не даст тебе ни многоядерку, ни настоящее железо - там qemu.

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

И вообще, тебе зачем это проверять? Или ты из секты неверующих в UB пока сами его не увидят? Если да, то просто считай что никакого UB нет, volatile хватит всем, и живи спокойно пока тебя не уволят по статье нахрен. А если нет, то всё что нужно написано в доке, прими это к сведению и пиши безопасную многопоточку.

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

Под рукой нет. Но глюки я с этим всем реально ловил пару раз. Но это в вендовом софте было.

Проблемы с NUMA проявляются обычно в двух вариантах:

  1. проблемы с производительностью, когда разные треды одного процесса раскиданы по разным процессорам и вынуждены сбрасывать память всё время и синхронизироваться. Особенно тупо может выйти, если тред вынужден брать локи в памяти на другой ноде. Это просто полная залупа (тут стоить гуглить «NUMA-aware locks», если подобное ожидается).
  2. из-за предыдущего пункта, если в коде есть гонка, которая пусть даже не проявляется на однопроцессорной системе, она вполне может проявиться и привести к интересным последствиям на многопроцессорной. Особенно потому что большую часть софта в таких условиях никто не тестирует.

Такие дела.

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

А какой-нить короткий наглядный пример можно, когда просто мультитред-примитивов недостаточно, а с помощью libnuma можно всё исправить?

Если ты просишь какой-то пример, то ты не понимаешь что такое NUMA, иди лучше и прочитай. Там не нужны новые примитивы, просто одна и та же операция с памятью по latency и throughput отличается в разы в зависимости от того локальная это память или чужая. libnuma позволяет этим явно управлять.

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

Про производительность понятно, но я не про неё спрашивал а про нарушения логики работы. Если какая-то гонка уже была, но из-за пониженной производительности увеличились шансы её поймать - тоже не считается, гонку надо фиксить в любом случае и numa тут ни при чём, разве что только как способ получше протестировать и выявить её.

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

Там не нужны новые примитивы

Ну, как не нужны… перцы из Оракла с тобой очень несогласны, анон.

https://arxiv.org/pdf/1810.05600

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

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

Если какая-то гонка уже была, но из-за пониженной производительности увеличились шансы её поймать - тоже не считается, гонку надо фиксить в любом случае и numa тут ни при чём, разве что только как способ получше протестировать и выявить её.

Открою тебе секрет: подавляющее большинство разрабов не будут фиксить то, что не проявляется в их целевой среде. Ты вот так же к UB относишься. Сам мне тут писал, что хардкодишь версию компилятора и в ус не дуешь, и срать что у тебя код кишит UB.

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

Доп. поток на другом ядре должен запуститься. По нормальному там конечно sched_setaffinity или подобное надо. Как-то вот так:

#include <sched.h>
#include <stdlib.h>
#include <threads.h>
int main() {
  int f(void* data) {
    cpu_set_t s;
    CPU_ZERO(&s);
    CPU_SET(1, &s);
    if (sched_setaffinity(0, sizeof(cpu_set_t), &s)) abort();
    return *(volatile int*)data = 1, 0;
  }
  volatile int data = 0;
  cpu_set_t s;
  CPU_ZERO(&s);
  CPU_SET(0, &s);
  if (sched_setaffinity(0, sizeof(cpu_set_t), &s)) abort();
  thrd_create(&(thrd_t){0}, f, &data);
  while (!data);
  return !(data == 1);
}
qweururu
() автор топика
Ответ на: комментарий от hateyoufeel

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

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

volatile

Ооо, у нас тут сеньёр принципал волатайл девелопер. Ты же понимаешь, что такое ещё как-то прокатывает на x86, где инты атомарные и в целом всё построже по модели памяти, но на других архитектурах может жопу оторвать? Никаких volatile для многопоточной синхронизации использовать нельзя. Барьеры-и-точка.

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

перцы из Оракла с тобой очень несогласны

А это, случаем, не те же чудаки на букву «м», которые не в курсе про то, что memcpy нельзя использовать для пересекающихся областей памяти?

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

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

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

memcpy можно было использовать для пересекающихся областей

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

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

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

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

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

Да, он там дал инструкцию как починить адекватное поведение memcpy в виду копирования снизу вверх. А «умники» это те, кто его перед этим сломал, ссылаясь на буквы мануала.

firkax ★★★★★
()

А ты как сам думаешь, на сайте cppreference будет описана логика языка высокого уровня (интересно, какого? может, раста) или конкретного железа без ссылки на него?

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

Кстати а какой смысл НЕ прибивать к одному процессору? Ну кроме случая, когда один процесс своими тредами в один процессор не влезает. Ведь если это не сделать, но процесс так или иначе будет иногда лазить в какие-то участки своей памяти с не того проца.

Вообще, я думаю что это должно на уровне планировщика ОС решаться - не заставлять разные треды одного процесса расползаться по разным прцоессорам без нужды, ну и память этому процессу выделять сразу правильную по дефолту.

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

Кстати а какой смысл НЕ прибивать к одному процессору?

Смысла может быть много. Например, виртуальные машины типа BEAM (Erlang) делают изолированную кучу на каждый поток, шаренной памяти там почти нет, а потому им норм. Базы данных и некоторый другой софт так же сами такое разруливают.

Вообще, я думаю что это должно на уровне планировщика ОС решаться

Думаешь? На самом деле, оно и правда решается, только далеко не всегда хорошо.

Плюс, бывают довольно вырожденные ситуации, когда у разных NUMA-нод разный доступ к ресурсам, и вот там приходится задрачиваться. Например, у многих POWER-процессоров и у некоторых x86 (Threadripper 2990wx, например) только часть ядер имеют прямой доступ к памяти, другие же ядра вынуждены через NUMA обращаться вообще в любую оперативу.

ну и память этому процессу выделять сразу правильную по дефолту.

Звучит как «делойте харашо, плоха ни делойте!»

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

Будешь читать наркоманские стандарты крестов, выдуманные шизиками - сойдешь с ума. У процессоров конкретных архитектур всё написано у них в мануалах, четко и прямо, читай их и смотри через че сделаны конкретные функции в крестах. Через какие интринсики

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

Звучит как «делойте харашо, плоха ни делойте!»

Нет. ОС знает какому ядру к какой памяти хороший доступ, и в простых случаях вполне может запомнить на уровне планировщика выбранное ядро и выделять ему именно ту память, которая у него быстрая. Конечно, это не всегда прокатит в автоматическом режиме, а где-то и в ручном не выйдет (если память сильно расшаренная по разным процессам), но простые случаи то можно обработать.

firkax ★★★★★
()