LINUX.ORG.RU

Вопрос к мультитред-UB-теоретикам

 , ,


0

5

Допустим есть такой код:

flag = 0;
while(!flag) {
  /* ... тут много кода, у компилятора нет шансов его оттрассировать до конца ... */
}
flag - обычный int, не атомик, и даже не volatile

Где-то в другом треде однажды ставится flag = 1 (вокруг этой операции тоже много кода). И всё это без межтредовой синхронизации (точнее, где-то в другом коде она может быть, но сама по себе и с переменной flag явно не связана).

У меня есть подозрение, что формально это некоторые могут посчитать UB, так ли это? Если да, то можете ли привести пример, с любыми дикими опциями оптимизатора и любым плеванием на здравый смысл разработчиков компилятора, но в рамках реальности, при каких условиях подобная логика может сломаться?

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

Поскольку возникло непонимание вопроса, уточняю:

static int flag;
static void * threadfunc(void *p) {
  flag = 0;
  while(!flag) {
    /* много кода */
  }
  return NULL;
}

extern void set_flag_1(void) {
  flag = 1;
}

★★★★★

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

У меня есть подозрение, что формально это некоторые могут посчитать UB, так ли это?

Код полностью приведи. Как минимум, не видно объявления flag и непонятно, передаётся ли этот flag по адресу в сторонние функции.

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

Что значит «нет шансов»?

hateyoufeel ★★★★★
()

flag точно должен быть выровнен, иначе на каком-нить arm/risc/sparc фокус не удастся.

подобное применяю. Ведущая программа не ведает про atomic (на таком вот сделано), а плагины из разных тредов так ей сигналят. Пациент жив

MKuznetsov ★★★★★
()

То, что само объявление без volatile это не конец света.

IMHO чтобы избежать применение компилятором своего искусственного интелекта при оптимизации я бы явно преобразовал тип при обращении

while(!(volatile int)flag) 
vel ★★★★★
()
Ответ на: комментарий от hateyoufeel

Код полностью приведи. Как минимум, не видно объявления flag и непонятно, передаётся ли этот flag по адресу в сторонние функции.

[static] int flag;

В сторонние, допустим, не передаётся.

Что значит «нет шансов»?

Например это значит что там внутри вызовы extern функций, скомпилированных (до конца, без lto) в другом модуле или вообще импортированные из .so. А вообще не знаю зачем это уточнять.

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

Ты наверно имел ввиду это:

*(volatile int*)&flag
потому что без указателя этот тайпкаст вообще ни на что не повлияет. Но нет, я специально уточнил что он не volatile т.к. вопрос касается именно оптимизирующего слишком умного компилятора в самом плохом смысле.

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

Таким, что рядом имеется функция которая ставит =1 и компилятор не может знать, что её никто не вызовет из неоттрассированного кода.

Мы сейчас будем тут три часа абстрактную хероту обсуждать. Просто приведи минимальный рабочий пример кода уже.

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

Да то что это по факту работает то понятно, но вопрос именно к теоретикам которые везде агитируют за UB.

и ещё от практиков - flag не int. С точным указанием int32_t или int64_t. Иначе можно наестся кексов, с разной размерностью у разных компиляторов.

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

Таким, что рядом имеется функция которая ставит =1 и компилятор не может знать, что её никто не вызовет из неоттрассированного кода.

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

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

Да то что это по факту работает то понятно, но вопрос именно к теоретикам которые везде агитируют за UB.

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

hateyoufeel ★★★★★
()

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

Update: volatile тоже не поможет, разве только как тут привели выше while(!(volatile int)flag), но надо посмотреть как это выглядит на уровне ассемблера. Я не сишник и не системный программист, но я бы использовал lock/mutex/критическую секцию.

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

Это C? Сишка вроде про такое ничего не говорит, strict UB тут нет, работать будет, но как - каждый раз по разному :)

C++ standard 1.10 (23.2): The execution of a program contains a data race if it contains two potentially concurrent conflicting actions, at least one of which is not atomic, and neither happens before the other, except for the special case for signal handlers described below. Any such data race results in undefined behavior

spbzip
()

код для ленивых задниц из https://en.cppreference.com/w/cpp/atomic/atomic

#include <atomic>
#include <iostream>
#include <thread>
#include <vector>

std::atomic_int acnt;
int cnt;

void f()
{
    for (int n = 0; n < 10000; ++n)
    {
        ++acnt;
        ++cnt;
    }
}

int main()
{
    std::vector<std::thread> pools;

    for (int n = 0; n < 10; ++n)
        pools.emplace_back(f);

    for (auto & th: pools)
        if(th.joinable()) th.join();

    std::cout << "The atomic counter is " << acnt << '\n'
              << "The non-atomic counter is " << cnt << '\n';

    return 0;
}

выхлоп например такой:

he atomic counter is 100000
The non-atomic counter is 39975
anonymous2 ★★★★★
()

А си тут притом, что?
У вас код на С++. P.S. Если что-то меняется вне треда - обязано быть volatile иначе компилятор может соптимизировать операции с аргументом.
P.P.S. Если используете флаг для сигнализации - лучше, если он будет atomic. Для значений 0/1 не особо критично, но если начнете использовать больше одного байта - могут появиться проблемы.

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

Преимущество использования int в том, что так код можно будет использовать и там, где int всего 16 бит.

сделаешь лёгким движением

void *my_thread(void *ptr) {
  int *common_counter=(int *)ptr;
  ...
}

и получишь забавные спец-эффекты, если твой my_thread собирался gcc, а использовался из MSVC и динамической загрузки

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

Для значений 0/1 не особо критично, но если начнете использовать больше одного байта - могут появиться проблемы.

Да ну? Чтение и запись идет не побайтово, какой сейчас обычно alignment на 64-х битных CPU? Кажись 8 байт, значит и чтение/запись в память идет сегментами кратными 8 байтам.

Во времена i32 без volatile было не обойтись, потому как long и double существовали во множестве языков, только чтение и присвоение значений таким переменным было не атомарным из-за природы 32-x битных CPU, теперь же везде x64.

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

Преимущество использования int в том, что так код можно будет использовать и там, где int всего 16 бит.

А это где?

Недостаток в том, что signed integer overflow как раз таки UB (потому что сишники ненавидят себя), а 16-битный инт переполнить куда легче.

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

На некоторых TI DSP:А что uint так и не сделают стандартом в С и С++ ? (комментарий).

Давай будем честны: шансы, что чей-то тут код окажется на этом DSP, ещё меньше чем твои шансы на секс с человеческой женщиной.

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

Это C? Сишка вроде про такое ничего не говорит, strict UB тут нет, работать будет, но как - каждый раз по разному :)

Пи*ц, сам привёл цитату где явно написано про UB, и «UB тут нет».

anonymous
()

У меня есть подозрение, что формально это некоторые могут посчитать UB, так ли это?

Не формально, а UB в чистом виде это и есть, цитату выше привели.

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

Этот вопрос - уже роспись в непонимании UB, а следовательно в неумении программировать. Не будет тебе примеров, и что ты сделаешь? Будешь писать код с UB? Ну пиши.

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

тело может выполниться лишний раз

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

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

А си тут притом, что? У вас код на С++

Чего-чего? Ну он конечно скомпилируется С++ компилятором, но это случайное совпадение. Речь про Си, впрочем ответы про С++ тоже принимаются и я сомневаюсь что там будет что-то другое.

Если используете флаг для сигнализации - лучше, если он будет atomic.

Опять повторю - речь не про то, как это правильно сделать, а про то, что «ужас, тут UB, всё сломано» и просьба пояснить что конкретно сломается.

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

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

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

Ведь, даже, с одним компилятором возникают проблемы с DLL с прилинкованным статически рантаймом, на что я уже и наталкивался: Potential errors passing CRT objects across DLL boundaries.

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

Круто, только это не то о чём я писал. У тебя туда единицу никто и не сможет записать никогда.

https://godbolt.org/z/TqPKjK6P6

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

Задавать вопрос теоретикам и употреблять фразу «в рамках реальности» это провокация.

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

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

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

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

Кстати из-за этого шланг устроил UB с протеканием main_thread_job() в main() без return-а.

Если спрятать одну из функций цикла то проверка флага на месте оказывается.

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

Опять не то

Все то.

у тебя весь цикл легко трассируется.

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

Если спрятать одну из функций цикла то проверка флага на месте оказывается.

Не спрятать, а вынести туда где компилятор не может сделать проверку на изменение flag. Ты убираешь UB этим, если мыслить в рамках одного потка.

// не UB
set_flag_p(&flag);
while (flag) {
  switch_flag(void);
  ...
}

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

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

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

Виден компилятору = легко трассируется. С чем ты споришь? И да, это условие (невозможность легко трассировать) указано в старте темы.

Не спрятать, а вынести туда где компилятор не может сделать проверку на изменение flag.

Вынести туда где компилятор не видит = спрятать. Опять непонятно с чем спор.

Неужели ты не допускаешь возможности ситуации, когда весь код одного треда содержится в одном .c файле?

Допускаю, конечно, но тема про другой случай. С «тредом в одном файле» мне и так всё ясно, я про него не спрашивал.

Ты убираешь UB этим, если мыслить в рамках одного потка.

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

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

У меня есть подозрение, что формально это некоторые могут посчитать UB, так ли это?

Это не UB, но компилятор может заменить while(!flag) на while(true).

Но если у тебя из за этого возникает data race (там где много кода), то это уже UB.

при каких условиях подобная логика может сломаться?

Зачем тебе специально искать условия, при которых твой невалидный код сломается, когда можно просто зафиксить этот код, объявив std::atomic_flag flag.

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

Зачем тебе специально искать условия, при которых твой невалидный код сломается, когда можно просто зафиксить этот код, объявивstd::atomic_flag flag.

Потому что у него компилятор под венду 97 года: зависимость выдаваемого асм-кода от нефункциональных вставок

:DDDDDDD

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

Это не UB, но компилятор может заменить while(!flag) на while(true).

У него требование трассировки, в однопоточной среде передача указателя на &flag, означает что внутренний код может вызвать *flag = 1; поэтому вечный цикл отменяется.

MOPKOBKA ★★★★
()

Я слышал тут ещё от архитектуры зависит. на x86 десктопе скорее всего прокатит. Но на многопроцессорных системах кеши разных процессоров могут не договориться и что-то пойдёт не так.

Вобщем так делать не надо.

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

UB - полной поломки логики выполнения, или хотя бы бесконечного зацикливания цикла

Согласно стандарту (опять же C++), цикл увидит изменение переменной «in a reasonable period of time». Сишка про это вообще ничего не говорит =\

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

Но на многопроцессорных системах кеши разных процессоров могут не договориться и что-то пойдёт не так.

На многопроцессорных системах у разных процессоров почти наверняка разная память. NUMA – это просто ад и холокост, там куча обычного софта напрочь едет.

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

куча обычного софта напрочь едет.

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

sergej ★★★★★
()

но в рамках реальности, при каких условиях подобная логика может сломаться?

сломаться может, если будешь делать, что-то вроде

// do some action
flag = 1; // marker that action is done

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

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

Vovka-Korovka ★★★★★
()

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

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

Что такое «требование трассировки»

Я тоже не заметил, там в комментариях в ОП посте.

Так же не вижу в ТС посте никаких &flag.

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

MOPKOBKA ★★★★
()