LINUX.ORG.RU

Мультитред, чтение меняющейся переменной без локов

 ,


0

4

Допустим, есть переменная, полностью обычная (просто char a; - для определённости пусть будет однобайтовая). Ещё до начала совместного к ней доступа она инициализируется либо нулём, либо не нулём. Если ноль - то дальше она не меняется. Если не ноль - то дальше в неё могут записываться другие ненулевые значения в произвольные моменты времени. Другой тред читает эту переменную, не утруждая себя межтредовой синхронизацией, но единственное что ему нужно - выяснить ноль в ней или нет. Как мне кажется, никаких проблем это создать не должно ни при каких обстоятельствах. Однако может быть я что-то упустил? И второй вопрос, отдельный: где формально написано что так можно?

★★★★★

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

по атомным часам

в его примере «совместный доступ» не будет иметь места как минимум, очевидно, до старта второго потока (до этих пор его программа однопоточная).

является, ли иницированный t1, cтарт t2 потока SA «парным» к первому стейтмент t2 в Си - не знаю, вероятно. в таком случае, запись в его программе была до старта - то всё понятно.

если же корректно-синхронизированная запись согласно замысла фактически произойдет после старта, то действительно: первые корректно-синхронизированные чтения будут чтением синхронизированного мусора какое-то время. и правда, в этом случае, выданных ранее рекомендаций будет недостаточно. тогда ему в t2 надо стоять на кондишене или примитиве аналогичного действия

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

Начало доступа разумеется синхронизируется. Вопрос не про него а про дальнейшие чтения.

Если так интересно - в реальной задаче это вообще не переменная а поле структуры, структура в какой-то момент malloc-ится и заполняется данными, потом добавляется в хеш-таблицу (под мютекстом) и только после этого кто-то может начать её читать, взяв (тоже под мютексом) указатель на неё из этой таблицы. А вот после того как указатель уже есть у двух или более тредов, уже могут возникнуть мультитредовые ситуации вокруг этого поля. Если в поле 0 - значит некая логика отключена, а если не 0 - то включена и своё текущее состояние запоминает в том же поле. Суть в том, что иногда можно захотеть узнать факт её включённости не тратя времени на мютексы или даже спинлоки её данных. Можно было бы без проблем сделать это отдельным константным булевым полем, но это некрасиво.

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

Не надо придираться к словам

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

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

взяв (тоже под мютексом) указатель

Это не гарантия, что «a» инициализирован до совместного доступа. Это гарантия только того, что адрес известен до начала доступа.

Вообщем, постановка задачи неправильная, любой ответ соответствующий (постановке задачи).

anonymous
()

Мне кажется время потраченное на обсуждение «будет ли работать этот undefined behaviour или нет» эффективнее было потратить на RTFM атомиков. И задачу бы закрыл, и знания капитализировал.

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

malloc-ится и заполняется данными, потом добавляется в хеш-таблицу (под мютекстом)

Любая операция на mutex - это full memory barrier. Я бы не переживал. Думаю - всё у вас там чистенько, даже с точки зрения «закона».

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

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

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

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

какой смысл писать не ноль, если там уже не ноль? причем никого не волнует, что там за «не_ноль»?

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

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

выдающийся дизайн. инициализация защищена мьютексом,… дальнейший доступ - нет. дальше вопросы - кааак читать бул?

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

С переупорядочением борятся барьерами памяти. Но они нужны не всегда, потому что есть правила переупорядочения. Компилятор и процессор не переупорядочивают инструкции в определённых сценариях. Если сценарий именно таков, то не нужны барьеры. Списки правил размазаны тонким слоем по интернетам. Тут вот в первом ответе в главе x86 re-ordering чувак что-то написал: https://stackoverflow.com/questions/50307693/does-an-x86-cpu-reorder-instructions

Тут первый коммент интересен https://www.reddit.com/r/programming/comments/to192/memory_reordering_caught_in_the_act/

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

В целом чуваки поговаривают, что x86 довольно мало чего-то переупорядочивает, то есть совсем ничего. То есть, «переупорядочивание» можно вообще забыть в контексте X86.

А вот ARM совсем другое дело, там режут и воруют гусей просто вагонами!

Возможно интереснее поизучать что там переупорядочит компилятор Вот за этим и придуманы std::memory_order. Можно сказать, что X86 вам никогда ничего не переупорядочит ВООБЩЕ от слова СОВСЕМ, но вот компилятор скотина гораздо более дикая и вот именно компилятор и надо обуздывать. То есть, бороться c x86 не нужно вообще, бороться надо с

  • компилятором
  • ARM
lesopilorama
()
Последнее исправление: lesopilorama (всего исправлений: 2)

Хотя бы барьеры памяти поставить желательно. Даже если оно заработает у тебя - нет гарантии, что везде. Я во временном коде тоже просто пишу volatile bool вместо атомика, но потом переделаю на atomic/sync билтины для надёжности. К слову говоря, сейчас даже на arm оно правильно работает без всяких атомиков т.к у меня код не чувствителен к когерентности, но если где-то логика поменяется или что-то не так соптимизируется - никаких гарантий

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

это к тому что никто к ней не обратится до того как в поле записано первоначальное значение

Допустим.

Мультитред, чтение меняющейся переменной без локов (комментарий)

Что мешает в потоке один раз проверить в начале флагозависимой логики и выставить константный локальный для потока флаг bool isZero и не дергать синхронизацию потоков ради чтения этого не меняющегося флага?

anonymous
()

никаких проблем это создать не должно ни при каких обстоятельствах.

segfault или просто всё в дым и кору, это не проблема конечно :-)

архитектуры бывают разные - вдруг у вас char не выровнен, а рядом что-то такое-же ещё подобное.

MKuznetsov ★★★★★
()

Из стандарта C11 (раздел 5.1.2.4, параграф 25):

The execution of a program contains a data race if it contains two conflicting actions in different threads, at least one of which is not atomic, and neither happens before the other. Any such data race results in undefined behavior.

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

Это мютекс на консистентность хеш-таблицы структур

Абсолютно логично.

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

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

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

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

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

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

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

volatile нужен для доступа к аппаратным ресурсам. Для межпоточной синхронизации нужно использовать atomic.

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

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

Объясни, зачем ты это делаешь? atomic_bool даже писать короче, чем volatile bool.

Писать может и короче, но

$ gcc xxx.c
xxx.c:1:1: error: unknown type name ‘atomic_bool’
    1 | atomic_bool x;

И вообще тема не об этом. Тема про чтение обычной переменной, безо всяких volatile в том числе.

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

При использовании в описанном варианте проблем возникнуть не должно, при следующих условиях:

  • Переменная не пересекает границу кеш-линии используемой машины. Если переменная 1 байт, то это, по понятным причинам, не произойдёт, но если больше, то ситуация возможна.
  • Ненулевые значения не используются для любого взаимодействия между потоками. То есть, если ненулевые значения несут любую смысловую нагрузку для нескольких потоков, то могут возникнуть проблемы. Например ABA.
  • Запись инициализирующего значения гарантированно происходит до межпоточного доступа к переменной. То есть, если A - событие инициализации переменной, а B - событие доступа к переменной из любого «рабочего» потока, то должно выполняться условие happensBefore(A, B). Этого можно достичь с помощью других видов синхронизации, например запустив рабочие потоки (имея ввиду нативные потоки ОС), только после записи инициализирующего значения.
QsUPt7S ★★★
()
Ответ на: комментарий от vbr

не проще. В си всё равно доступ к нему через длинные-длинные функции, в c++ с stl действительно проще было т.к там уже есть класс-обёртка, но в проекте без stl всё равно писать реализацию.
А основная побочка от volatile bool вместо атомика - отсутствие гарантии когерентности. На данном этапе не критично, ео потом всё равно переделаю. Мне там вообще эта когерентность не сильно нужна, нужно только чтобы новое значение когда-нибудь попало в другой тред

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

В си всё равно доступ к нему через длинные-длинные функции

Нет, ты можешь использовать обычные чтения/присваивания. Функции нужны только когда нужен конкретно их функционал. Для простых случаев это не обязательно. Уж если тебе volatile хватает, то тебе это не надо.

Иными словами ты любой код с volatile можешь заменить на atomic, получив генерацию кода с барьерами памяти.

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

Но код с volatile bool и доступом с разных тредов - априори вещь некрасивая

Почему?
Имхо, в описанном случае и модификатор volatile излишен - хватит обычного «plain»-доступа, если есть гарантии, что компилятор/интерпретатор не переместит инициализатор за код запуска потоков. Атомики же вообще порождают sequentially consistent total order, что означает и присутствие «тяжёлых» store-load барьеров.

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

А при plain доступе есть гарантия, что значение не останется в регистре? Инлайнинг порой творит чудеса, а у меня весь код целиком инлайнится в пару функций
Конечно хотелось бы найти гарантированный способ, который поможет избежать тяжёлых load барьеров при чтении (как минимум не выкидывать cache line из кэша без необходимости), но чтобы при этом данные всё же были доставлены, но я пока не разбирался, потому для простоты временно указан volatile bool

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

А при plain доступе есть гарантия, что значение не останется в регистре

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

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

хотелось бы найти гарантированный способ, который поможет избежать тяжёлых load барьеров

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

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

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

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

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

asdpm
()

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

И второй вопрос, отдельный: где формально написано что так можно?

Нигде. В программировании презумция разрешённости - если явно не запрещено, то можно. А дальше зависит только от количества мозгов у программиста.

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

А при plain доступе есть гарантия, что значение не останется в регистре? Инлайнинг порой творит чудеса, а у меня весь код целиком инлайнится в пару функций

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

Конечно хотелось бы найти гарантированный способ, который поможет избежать тяжёлых load барьеров при чтении

В Java, в описанном кейсе, так точно можно, так как моделью памяти актуальных версий Java гарантируется, что обращение к методу запуска потока happens-before любых действий этого потока. Я не настолько хорошо знаком с моделью памяти C/C++, но и там, насколько я знаю, конструктор потока даёт аналогичные гарантии.

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

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

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

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

Регистры чужого треда посмотреть в общем случае невозможно.

В актуальных наборах машинных команд действительно так. Но я пытался сказать не о том, что другой поток может влезть в регистры, а о том, что ЯВУ (как впрочем и набор команд Ассемблера) по сути представляют абстрактную машину, не привязанную к реальному исполнителю. К примеру, исполнителем может быть стековая машина, программно реализуемая поверх регистровой машины, которая является абстракцией набора команд реализуемой SSA-микроархитектурой.

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

Я имею в виду после. А так - да, что до старта потока было сделано, то поток, конечно, увидит.

Я пытался найти какие-то гарантии того, что запись в char будет атомарной (без atomic), но не нашёл. Поэтому предполагаю, что в теории может быть такое, что запись в char будет вестись по одному биту (или любым иным причудливым образом) и один поток может увидеть несуществующее значение, прочитав его в середине этой записи. Конечно в реальности такое вряд ли где-то есть, но тем не менее запрета на такое я не нашёл, поэтому если играть в языковых юристов, то так. Ну или помогите Даше найти то, что запись в char всегда атомарна.

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

Я пытался найти какие-то гарантии того, что запись в char будет атомарной (без atomic), но не нашёл.

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

никаких «потоков» проц не знает. знает только прерывания.

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

Ну или помогите Даше найти то, что запись в char всегда атомарна.

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

вот тут некое обсуждение со ссылками на доки. ищите сами.

https://stackoverflow.com/questions/53687178/interrupting-instruction-in-the-middle-of-execution

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

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

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

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

JaM
()