Порядок создания объектов
От: _NN_ www.nemerleweb.com
Дата: 29.05.23 21:23
Оценка:
Всплыл такой вопрос, положим у нас есть код вида:

Tool tool{};

std::thread toolRunner([]() {
    printf("Starting tool");
});


Есть ли у нас гарантии от компилятора и от процессора, что toolRunner будет создан после того как будет создан tool ?

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

Какие мысли ?
http://rsdn.nemerleweb.com
http://nemerleweb.com
Re: Порядок создания объектов
От: Marty Пират https://www.youtube.com/channel/UChp5PpQ6T4-93HbNF-8vSYg
Дата: 29.05.23 21:46
Оценка:
Здравствуйте, _NN_, Вы писали:

_NN>Есть ли у нас гарантии от компилятора и от процессора, что toolRunner будет создан после того как будет создан tool ?


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


_NN>Насколько я понимаю, компилятор вправе переупорядочить создание если нет видимого эффекта.


Не уверен
Маньяк Робокряк колесит по городу
Re[2]: Порядок создания объектов
От: _NN_ www.nemerleweb.com
Дата: 29.05.23 21:50
Оценка:
Здравствуйте, Marty, Вы писали:

_NN>>Насколько я понимаю, компилятор вправе переупорядочить создание если нет видимого эффекта.


M>Не уверен


Почему не может?
Скажем у нас есть

int a{ 1 };
int b{ 2 };


Порядок ведь не обязан быть сначала a потом b.
Так и объекты посложнее если компилятор сможет понять, что видимый эффект не меняется от перестановки.
http://rsdn.nemerleweb.com
http://nemerleweb.com
Re[3]: Порядок создания объектов
От: sts  
Дата: 29.05.23 22:05
Оценка:
Здравствуйте, _NN_, Вы писали:

_NN>Здравствуйте, Marty, Вы писали:


_NN>>>Насколько я понимаю, компилятор вправе переупорядочить создание если нет видимого эффекта.


M>>Не уверен


_NN>Почему не может?

_NN>Скажем у нас есть

_NN>
_NN>int a{ 1 };
_NN>int b{ 2 };
_NN>


_NN>Порядок ведь не обязан быть сначала a потом b.

_NN>Так и объекты посложнее если компилятор сможет понять, что видимый эффект не меняется от перестановки.

там по разному для разных типов
ключевое слово "точка следования"
Re[4]: Порядок создания объектов
От: _NN_ www.nemerleweb.com
Дата: 29.05.23 22:08
Оценка: 2 (1)
Здравствуйте, sts, Вы писали:


sts>там по разному для разных типов

sts>ключевое слово "точка следования"


Начиная со стандарта C++11, в языке C++ больше не существует понятия точки следования.

https://ru.m.wikipedia.org/wiki/%D0%A2%D0%BE%D1%87%D0%BA%D0%B0_%D1%81%D0%BB%D0%B5%D0%B4%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F#:~:text=%D0%9D%D0%B0%D1%87%D0%B8%D0%BD%D0%B0%D1%8F%20%D1%81%D0%BE%20%D1%81%D1%82%D0%B0%D0%BD%D0%B4%D0%B0%D1%80%D1%82%D0%B0%20C%2B%2B,%D1%83%D0%BF%D0%BE%D1%80%D1%8F%D0%B4%D0%BE%D1%87%D0%B5%D0%BD%D0%BE%20%D0%B4%D0%BE%20%D0%B8%20%D1%83%D0%BF%D0%BE%D1%80%D1%8F%D0%B4%D0%BE%D1%87%D0%B5%D0%BD%D0%BE%20%D0%BF%D0%BE%D1%81%D0%BB%D0%B5
http://rsdn.nemerleweb.com
http://nemerleweb.com
Re: Порядок создания объектов
От: night beast СССР  
Дата: 30.05.23 06:33
Оценка: +3
Здравствуйте, _NN_, Вы писали:

_NN>Всплыл такой вопрос, положим у нас есть код вида:


_NN>Какие мысли ?


насколько понимаю, в пределах единицы трансляции последовательно

Static initialization [basic.start.static]
Dynamic initialization of non-local variables [basic.start.dynamic]
Re: Порядок создания объектов
От: σ  
Дата: 30.05.23 07:40
Оценка:
_NN>Исходя из этого напрашивается вывод, каждое создание std::thread должно быть ограничено барьером, чтобы запретить переупорядочивание как компилятором так и процессором и получить нужный нам порядок.

Если барьеры добавляют наблюдаемое поведение, то наверное, а если нет — то как они запретят?
Re: Порядок создания объектов
От: Zhendos  
Дата: 30.05.23 09:09
Оценка: +1
Здравствуйте, _NN_, Вы писали:

_NN>Есть ли у нас гарантии от компилятора и от процессора, что toolRunner будет создан после того как будет создан tool ?


_NN>Насколько я понимаю, компилятор вправе переупорядочить создание если нет видимого эффекта.

_NN>В данном случае нет разницы что будет создано первым, а значит можно поменять местами в целях оптимизации.

Нет разницы, это если компилятору доступны в этой точке как конструктор Tool так и std::thread,
а также код всех функций которые вызывают эти конструкторы, в том числи и тело функции типа "pthread_create",
если речь идет от Linux платформе.

Иначе как компилятор может понять что можно переупорядочивать конструкторы?
Ведь невидимые ему реализации функций могут использовать одни и те же глобальные переменные,
и таким образом зависеть друг от друга.
Re: Порядок создания объектов
От: B0FEE664  
Дата: 30.05.23 09:12
Оценка: 8 (2)
Здравствуйте, _NN_, Вы писали:

_NN>Всплыл такой вопрос, положим у нас есть код вида:

_NN>
_NN>Tool tool{};
_NN>std::thread toolRunner([]() {
_NN>    printf("Starting tool");
_NN>});
_NN>

_NN>Есть ли у нас гарантии от компилятора и от процессора, что toolRunner будет создан после того как будет создан tool ?
В этом вопросе самое важное слово "после". Вот в каком смысле "после"? Как вы это "после" понимаете, если учесть, что от этого "после" ничего не зависит и нет никаких способов, в некотором смысле физических, изнутри потока выполнения инструкций процессора узнать, что произошло раньше, а что позже?

_NN>Насколько я понимаю, компилятор вправе переупорядочить создание если нет видимого эффекта.

да.

_NN>В данном случае нет разницы что будет создано первым, а значит можно поменять местами в целях оптимизации.

да.

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

Возможно. Это зависит от того, какое поведение вы хотите запрограммировать.

_NN>Какие мысли ?

У меня есть подозрение, что вы не разделяете создание объекта toolRunner и запуск на выполнение — это две разные операции. Сначала создаётся объект toolRunner и продолжается выполнение основного потока, потом, параллельно, возможно намного позже, будет запущена нитка для toolRunner и ещё позже будет вызвана функция printf.

Более того, если убрать нитку, то ничего не поменяется:
Tool tool{};
printf("Starting tool");

Если конструктор Tool не имеет побочных эффектов, то нет гарантии, что вызов функции printf("Starting tool"); будет произведён после (что бы это не значило) создания объекта tool. Вообще говоря стандартом не запрещается параллельное выполнение этого кода, скажем, на двух разных процессорах: на одном создание объекта tool, а на втором выполнение функции printf.
И каждый день — без права на ошибку...
Re: Порядок создания объектов
От: Кодт Россия  
Дата: 30.05.23 13:27
Оценка: +1
Здравствуйте, _NN_, Вы писали:

_NN>Какие мысли ?


По возможности, избавляться от кода, выполняемого до main. Будь то нетривиальные конструкторы глобальных объектов, или функции, помеченные атрибутом [constructor]].
Синглетоны Мейерса — меньшее зло.
Перекуём баги на фичи!
Re: Порядок создания объектов
От: andrey.desman  
Дата: 30.05.23 13:43
Оценка:
Здравствуйте, _NN_, Вы писали:

_NN>Есть ли у нас гарантии от компилятора и от процессора, что toolRunner будет создан после того как будет создан tool ?


По идее да, ведь

The completion of the invocation of the constructor synchronizes-with (as defined in std::memory_order) the beginning of the invocation of the copy of f on the new thread of execution.

https://en.cppreference.com/w/cpp/thread/thread/thread

А

Synchronizes with
If an atomic store in thread A is a release operation, an atomic load in thread B from the same variable is an acquire operation, and the load in thread B reads a value written by the store in thread A, then the store in thread A synchronizes-with the load in thread B.

Also, some library calls may be defined to synchronize-with other library calls on other threads.

https://en.cppreference.com/w/cpp/atomic/memory_order
Re[2]: Порядок создания объектов
От: _NN_ www.nemerleweb.com
Дата: 30.05.23 13:45
Оценка:
Здравствуйте, Кодт, Вы писали:

Переменные они локальные в функции.
Даже если не локальные по видимому можно переупорядочить тоже.
http://rsdn.nemerleweb.com
http://nemerleweb.com
Re[3]: Порядок создания объектов
От: Кодт Россия  
Дата: 30.05.23 16:32
Оценка:
Здравствуйте, _NN_, Вы писали:

_NN>Переменные они локальные в функции.


Ааа, так это были локальные переменные? Тогда гарантия последовательности точно есть.
Переупорядочивание возможно лишь в том случае, если компилятор уверен в отсутствии побочных эффектов (что с std::thread, очевидно, уже не так). Но если побочных эффектов нет, то и пользователю незаметно, переупорядочил там что-то компилятор или сохранил.

_NN>Даже если не локальные по видимому можно переупорядочить тоже.


В одной единице трансляции — нет.
На уровне разных единиц трансляции — можно.
На уровне разных библиотек — недетерминированно, может даже варьироваться от запуска к запуску.
И вот именно поэтому, из-за того, что возможны гонки инициализации — стоит избавляться от энергичных синглетонов в пользу ленивых.
Перекуём баги на фичи!
Re[2]: Порядок создания объектов
От: B0FEE664  
Дата: 31.05.23 09:42
Оценка:
Здравствуйте, andrey.desman, Вы писали:

_NN>>Есть ли у нас гарантии от компилятора и от процессора, что toolRunner будет создан после того как будет создан tool ?

AD>По идее да, ведь
AD>

AD>The completion of the invocation of the constructor synchronizes-with (as defined in std::memory_order) the beginning of the invocation of the copy of f on the new thread of execution.

AD>https://en.cppreference.com/w/cpp/thread/thread/thread

Не-а.
Посмотрите на код:
Tool tool{};

std::thread toolRunner([]() {
    printf("Starting tool");
});

Здесь аргумент toolRunner'а никак не связан с tool, поэтому вышеприведённая цитата не относится к делу. Вот если эти объекты связать, то тогда — да, синхронизация будет:
Tool tool{};

std::thread toolRunner([tool]() {
    printf("Starting tool %d", tool.GetId());
});
И каждый день — без права на ошибку...
Re[2]: Порядок создания объектов
От: _NN_ www.nemerleweb.com
Дата: 31.05.23 10:10
Оценка:
Здравствуйте, B0FEE664, Вы писали:

BFE>Более того, если убрать нитку, то ничего не поменяется:

BFE>
BFE>Tool tool{};
BFE>printf("Starting tool");
BFE>

BFE>Если конструктор Tool не имеет побочных эффектов, то нет гарантии, что вызов функции printf("Starting tool"); будет произведён после (что бы это не значило) создания объекта tool. Вообще говоря стандартом не запрещается параллельное выполнение этого кода, скажем, на двух разных процессорах: на одном создание объекта tool, а на втором выполнение функции printf.

А если у нас есть инициализация переменной ?

int i{ 10 };
std::thread t( [&i] { printf("%d", i); } );


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


В таком случае atomic<int> также не спасает ведь инициализация не автомарная:
std::atomic<int> i{ 10 }; // 2) Initializes the underlying object with desired. The initialization is not atomic.
std::thread t( [&i] { printf("%d", i); } );
http://rsdn.nemerleweb.com
http://nemerleweb.com
Re[3]: Порядок создания объектов
От: andrey.desman  
Дата: 31.05.23 14:22
Оценка:
Здравствуйте, B0FEE664, Вы писали:

BFE>Здесь аргумент toolRunner'а никак не связан с tool


Вообще не факт.
Re[3]: Порядок создания объектов
От: B0FEE664  
Дата: 31.05.23 15:34
Оценка:
Здравствуйте, _NN_, Вы писали:

_NN>А если у нас есть инициализация переменной ?

Простая инициализация сама по себе мало что меняет, а вот использование переменной — да.

_NN>
_NN>int i{ 10 };
_NN>std::thread t( [&i] { printf("%d", i); } );
_NN>

_NN>Как я понимаю даже если компилятор понимает, что порядок менять нельзя, присваивание значения 10 может не отразится при чтении из другого потока. Верно ?
Нет. В этом случае i гарантированно будет инициализировано до создания ссылки на i.

Так как i — ссылка, то напечатано будет фактическое значение i на момент копирования значения i при передаче в функцию printf. Т.е. вот такой код:
int i{ 10 };
std::thread t( [&i] { printf("%d", i); } );
i += 1;

Может напечатать 11, (а может любое другое значение, на архитектуре, где i не атомарная переменная).

_NN>То есть даже в таком коде нам нужен барьер памяти, чтобы поток прочитал именно то, что нужно.

Если переменная i не меняется, то никаких проблем.

Ну и вот такой код однозначно выведет 10:

int i{ 10 };
std::thread t( [i] { printf("%d", i); } );
i += 1;


_NN>В таком случае atomic<int> также не спасает ведь инициализация не автомарная:

_NN>
_NN>std::atomic<int> i{ 10 }; // 2) Initializes the underlying object with desired. The initialization is not atomic.
_NN>std::thread t( [&i] { printf("%d", i); } );
_NN>

Этот код не скомпилируется, но если написать
std::atomic<int> i{ 10 }; // 2) Initializes the underlying object with desired. The initialization is not atomic.
std::thread t( [&i] { printf("%d", i.load()); } );

то будет выведено значение 10 или другое корректное значение, если i изменился:
std::atomic<int> i{ 10 }; 
std::thread t( [&i] { printf("%d", i.load()); } );
i += 1;

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

Если я правильно понимаю, то проблема с not thread-safe инициализацией может возникнуть только при использовании не static глобальной переменной в нескольких потоках. Или если саму переменную пытаются использовать прямо в выражении инициализации типа такого:
std::atomic<int> i{[&i]{ std::thread t( [&i] { printf("%d", i.load()); } ); return 2;}()};

— это, вообще говоря, разновидность UB вида int i = 1 + i;
И каждый день — без права на ошибку...
Re[4]: Порядок создания объектов
От: _NN_ www.nemerleweb.com
Дата: 31.05.23 15:47
Оценка:
Здравствуйте, B0FEE664, Вы писали:

BFE>Здравствуйте, _NN_, Вы писали:


_NN>>А если у нас есть инициализация переменной ?

BFE>Простая инициализация сама по себе мало что меняет, а вот использование переменной — да.

_NN>>
_NN>>int i{ 10 };
_NN>>std::thread t( [&i] { printf("%d", i); } );
_NN>>

_NN>>Как я понимаю даже если компилятор понимает, что порядок менять нельзя, присваивание значения 10 может не отразится при чтении из другого потока. Верно ?
BFE>Нет. В этом случае i гарантированно будет инициализировано до создания ссылки на i.

Хорошо, i содержит 10, ссылка ссылается на памить i.
Но что если у нас два процессора где первый записал себе значение, но оно не дошло до памяти и второй процессор ещё его не видит ?
Кто даст гарантию если не явный барьер памяти, что есть видимость во всех процессорах ?
http://rsdn.nemerleweb.com
http://nemerleweb.com
Re[5]: Порядок создания объектов
От: andrey.desman  
Дата: 31.05.23 19:51
Оценка: 18 (1)
Здравствуйте, _NN_, Вы писали:

_NN>Хорошо, i содержит 10, ссылка ссылается на памить i.

_NN>Но что если у нас два процессора где первый записал себе значение, но оно не дошло до памяти и второй процессор ещё его не видит ?
_NN>Кто даст гарантию если не явный барьер памяти, что есть видимость во всех процессорах ?

Создание потока и есть этот барьер.
Re: Порядок создания объектов
От: CRT  
Дата: 01.06.23 09:19
Оценка:
Здравствуйте, _NN_, Вы писали:

_NN>
_NN>Tool tool{};

_NN>std::thread toolRunner([]() {
_NN>    printf("Starting tool");
_NN>});
_NN>

_NN>Какие мысли ?

я конечно подотстал в новых стандартах в с++.
Но у тебя в коде указан сначала вызов конструктора для Tool, а потом для std::thread
Я не вижу отличий между вызовом двух конструкторов последовательно в одном потоке один за другим (что ты и делаешь — у тебя конструкторы в одном потоке вызываются) и просто вызовом двух функций последовательно в одном потоке.
Например
a();
b();

как компилятор может сначала вызвать b() перед a()?
у меня это в голове не укладывается. Такого быть не должно.

Поэтому мой ответ такой. Конструктор Tool полностью отработает до того как вызовется конструктор std::thread
и поэтому printf("Starting tool") вызовется когда tool уже создан.
Отредактировано 01.06.2023 13:11 CRT . Предыдущая версия . Еще …
Отредактировано 01.06.2023 9:59 CRT . Предыдущая версия .
Отредактировано 01.06.2023 9:30 CRT . Предыдущая версия .
Re[3]: Порядок создания объектов
От: CRT  
Дата: 01.06.23 10:19
Оценка:
Здравствуйте, _NN_, Вы писали:


_NN>
_NN>int i{ 10 };
_NN>std::thread t( [&i] { printf("%d", i); } );
_NN>


запуск потока это всегда барьер памяти. Поэтому у тебя гарантировано инициализация i числом 10 пройдет ДО запуска потока
Отредактировано 01.06.2023 11:58 CRT . Предыдущая версия . Еще …
Отредактировано 01.06.2023 10:31 CRT . Предыдущая версия .
Re[2]: Порядок создания объектов
От: · Великобритания  
Дата: 01.06.23 10:26
Оценка:
Здравствуйте, CRT, Вы писали:

CRT> как компилятор может сначала вызвать b() перед a()?

CRT> у меня это в голове не укладывается. Такого быть не должно.
Может, естественно, если компилятор знает, что это не влияет на наблюдаемое поведение.
Вот такая штука есть: https://en.wikipedia.org/wiki/Memory_model_(programming)
Тонкий момент, что "наблюдаемое поведение" — в пределах одного треда. Если ты будешь наблюдать это поведение из другого треда без должной синхронизации, то можно будет увидеть бардак.
Другое дело, что в начальном примере был запуск треда, который вроде как по современным стандартам (The memory model was then included in the next C++ and C standards, C++11 and C11) должен создавать барьер.
avalon/3.0.0
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[3]: Порядок создания объектов
От: CRT  
Дата: 01.06.23 11:09
Оценка:
Здравствуйте, ·, Вы писали:

CRT>> как компилятор может сначала вызвать b() перед a()?

CRT>> у меня это в голове не укладывается. Такого быть не должно.
·>Может, естественно, если компилятор знает, что это не влияет на наблюдаемое поведение.

чтобы это знать, компилятор должен учитывать всё что делается внутри a() и b() и так же внутри тех функций которые эти a() и b() в свою очередь вызывают и далее по цепочке. Что-то я сомневаюсь что компилятор копает так глубоко. Хотя кто их знает...


·>Другое дело, что в начальном примере был запуск треда, который вроде как по современным стандартам (The memory model was then included in the next C++ and C standards, C++11 and C11) должен создавать барьер.


это я знаю.

вообще автор задал вопрос неоднозначно

Есть ли у нас гарантии от компилятора и от процессора, что toolRunner будет создан после того как будет создан tool ?


Что он имел в виду: создание объекта std::thread toolRunner или начало работы потока который представляется этим объектом?
Это ж не одно и то же. Конструктор объекта toolRunner и его функция потока выполняются в разных потоках.

Если считать что компилятор может конструкторы вызывать не в том порядке в котором они указаны в коде, то объект toolRunner может быть создан раньше чем tool. Но тогда получается что барьер памяти будет не работать, потому что барьер то как раз должен завершить все операции записи...

Короче, однозначно что tool уже будет проинициализирован перед запуском потока toolRunner .
Отредактировано 01.06.2023 19:02 CRT . Предыдущая версия . Еще …
Отредактировано 01.06.2023 12:49 CRT . Предыдущая версия .
Отредактировано 01.06.2023 11:36 CRT . Предыдущая версия .
Отредактировано 01.06.2023 11:35 CRT . Предыдущая версия .
Отредактировано 01.06.2023 11:35 CRT . Предыдущая версия .
Отредактировано 01.06.2023 11:24 CRT . Предыдущая версия .
Отредактировано 01.06.2023 11:21 CRT . Предыдущая версия .
Отредактировано 01.06.2023 11:10 CRT . Предыдущая версия .
Re[4]: Порядок создания объектов
От: · Великобритания  
Дата: 01.06.23 11:41
Оценка:
Здравствуйте, CRT, Вы писали:

CRT> CRT>> как компилятор может сначала вызвать b() перед a()?

CRT> CRT>> у меня это в голове не укладывается. Такого быть не должно.
CRT> ·>Может, естественно, если компилятор знает, что это не влияет на наблюдаемое поведение.
CRT> чтобы это знать, компилятор должен учитывать всё что делается внутри a() и b() и так же внутри тех функций которые эти a() и b() в свою очередь вызывают и далее по цепочке. Что-то я сомневаюсь что компилятор копает так глубоко. Хотя кто их знает...
Ну во-первых, а кто сказал, что a() и b() это обязано быть чем-то глубоким.
А во-вторых, это работа такая у оптимизатора компилятора, стараться копать поглубже.

CRT> ·>Другое дело, что в начальном примере был запуск треда, который вроде как по современным стандартам (The memory model was then included in the next C++ and C standards, C++11 and C11) должен создавать барьер.

CRT> это я знаю.

CRT> вообще автор задал вопрос неоднозначно

Ну да... Тонкости в формулировке правильного вопроса есть.

CRT> Короче, однозначно что tool будет проинициализирован перед запуском потока.

+1.
Интересно как оно работало в старых версиях языка... Полагаю это было на уровне OS API.
avalon/3.0.0
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[5]: Порядок создания объектов
От: CRT  
Дата: 01.06.23 12:01
Оценка: +1
Здравствуйте, ·, Вы писали:

·>Интересно как оно работало в старых версиях языка... Полагаю это было на уровне OS API.


CreateThread() в WinAPI создает барьер памяти
в других ОС насколько знаю так же
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.