Порядок создания объектов
От: _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 . Предыдущая версия .
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.