LINUX.ORG.RU

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

 ,


0

4

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

★★★★★

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

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

Тебе зря так кажется. Любой тред может, например, прочитать или записать переменную один раз в регистр и всё.

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

INTERNATIONAL STANDARD ©ISO/IEC ISO/IEC 9899:2023
Programming languages — C
5.1.2.4 Multi-threaded executions and data races

d ★★★★★
()

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

Например,

Поток 1

  //do_something
  a = 0; 

Поток 2

   if (a == 0) {
       // assuming do_something happened
   }

Но в потоке 1, на самом деле, может сначала произойти присвоение переменной a, и только потом будет do_something.

Почитай про переупорядочивание операций и барьеры памяти.

Возможно, под твой use-case подойдут атомики, но с ними тоже осторожными нужно быть.

Vovka-Korovka ★★★★★
()

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

Я не вижу как это можно сломать, на x86 так точно. Зря int на char поменяли - с интами интереснее (alignment начинает играть роль, хотя достаточно границу cache-line не пересекать),

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

Intel optimization reference manual?

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

Я не вижу как это можно сломать, на x86 так точно.

Компилятор поменяет порядок операций, см. мой пример выше.

x86, кстати, тоже умеет переупорядочивать операции, хоть это и не случай, описанный ТС.

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

Компилятор поменяет порядок операций, см. мой пример выше.

И? Что самое плохое может произойти? Прочитается stale value. По постановке интересует ноль / не-ноль, и это состояние не меняется.

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

И? Что самое плохое может произойти?

Логика приложения сломается, вплоть до крэша. Пример (абстрактный)

Поток 1

   p->val = 42;
   a = 0;

Поток 2

   if (a == 0) {
      val = p->val;
      free(p); // don't need it anymore
   }

если компилятор переставит операции в первом потоке, то будет больно.

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

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

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

Тебе зря так кажется. Любой тред может, например, прочитать или записать переменную один раз в регистр и всё.

Пусть читает, я не против. Что сломается то?

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

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

Тут как мне видится единственное с чем надо разобраться что может прочитаться в момент такого неатомарного изменения. А точнее может ли там возникнуть 0 при чтении, если меняется не ноль на не ноль.

Но проще не разбираться, а просто использовать атомарные изменения везде где вы этот флаг используете. Тогда всё будет гарантировано.

unDEFER ★★★★★
()

Вопрос в том, почему ты не хочешь сделать нормально. Просто поставь atomic и забудь про этот вопрос.

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

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

Вопрос в том, почему ты не хочешь сделать нормально. Просто поставь atomic и забудь про этот вопрос.

Ещё раз повторю, вопрос не в том как делать а в последствиях того как описано.

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

С чего бы это? Впрочем, если он никуда не будет писать - тем более ничего не сломается.

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

Если очень хочется следовать «букве закона» то придётся заворачивать в atomic’и. Но я затрудняюсь представить настолько brain-damaged платформу чтобы даже с интами происходили какие-либо гадости, например faults если разные части инта случились «из разных поколений».

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

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

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

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

Если у тебя переменная выставлена до старта потока, то поток её видит в том состоянии, котором она была в этот момент.

Что ж народ такой невнимательный пошёл, спешу заметить что в изначальной постановке задачи это устраивает:

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

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

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

Собственно суть в том, что если компилятор достаточно умный, чтобы увидеть, что поток «теоретически» может никогда не увидеть изменений, то он может просто захардкодить 0 и выкинуть все условия. Это же размер кода уменьшит )

vbr ★★★★★
()

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

alysnix ★★★
()

смотри, записанное в одном треде становится видно в другой момент относительно других увиденных записей (даже если p1 видит их программном порядке), или становится видно частично или не становится видно никогда. чтобы изменения стали видны, и видны в порядке как в программе и видны в том объеме как было программе нужно в своем языке найти такие SA которые установят HB между чтением и записью. Си я знаю очень слабо, могу лишь сказать, что как правило свойством быть SA часто обладают переменные с модификатором типа «atomic» или «volatile», в языке это их свойство может быть побочным, но тогда должно быть гарантированным в документации. Вторым (из наиболее рутинных) распространенным действием часто имеющим побочным гарантированным эффектом быть еще и SA является пара «отпускание лока писателем» + «его захват читателем». то что изменения твоего флага иногда становится видны и просто так - не более чем случайность. в некоторых языках именно такой твой пример используют как анти-пример в книгах - в переменную пишут, но она так никогда и не меняется

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

смотри, записанное в одном треде становится видно в другой момент относительно других увиденных записей

Ещё один. Там же по русски написано что даже если читающий поток вообще никогда update не увидит логика не сломается.

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

Ещё раз: ноль остаётся нулём навсегда. Не-ноль может меняться, но остаётся не-нулём. Интересует исключительно ноль / не-ноль. Внимание, вопрос - о какой нафиг синхронизации и барьерах вообще речь может идти?

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

он говорит «до начала совместного к ней доступа она инициализируется» он не «говорит запись в нее происходит до старта t2» (даже если согласно правил языка старт потока это SA)

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

он так и будет видеть старое значение

он может увидеть и новое

поток её видит в том состоянии, котором она была в этот момент. Это тебе гарантируется

нет, он может увидеть ее и в третьем состоянии если в нее еще раз записали (даже если с t3 нет синхронизации), иначе это уже транзакционный repeatable read в озу получился бы

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

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

может просто захардкодить 0

Нет, первоначальное значение переменной может быть разным от запуска к запуску.

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