Есть ли у нас гарантии от компилятора и от процессора, что toolRunner будет создан после того как будет создан tool ?
Насколько я понимаю, компилятор вправе переупорядочить создание если нет видимого эффекта.
В данном случае нет разницы что будет создано первым, а значит можно поменять местами в целях оптимизации.
Исходя из этого напрашивается вывод, каждое создание std::thread должно быть ограничено барьером, чтобы запретить переупорядочивание как компилятором так и процессором и получить нужный нам порядок.
Здравствуйте, _NN_, Вы писали:
_NN>Есть ли у нас гарантии от компилятора и от процессора, что toolRunner будет создан после того как будет создан tool ?
Насколько помню, порядок инициализации переменных внутри одной единицы трансляции вполне определён. Проблема возникает, когда переменные определены в разных модулях
_NN>Насколько я понимаю, компилятор вправе переупорядочить создание если нет видимого эффекта.
Здравствуйте, _NN_, Вы писали:
_NN>Здравствуйте, Marty, Вы писали:
_NN>>>Насколько я понимаю, компилятор вправе переупорядочить создание если нет видимого эффекта.
M>>Не уверен
_NN>Почему не может? _NN>Скажем у нас есть
_NN>
_NN>int a{ 1 };
_NN>int b{ 2 };
_NN>
_NN>Порядок ведь не обязан быть сначала a потом b. _NN>Так и объекты посложнее если компилятор сможет понять, что видимый эффект не меняется от перестановки.
там по разному для разных типов
ключевое слово "точка следования"
_NN>Исходя из этого напрашивается вывод, каждое создание std::thread должно быть ограничено барьером, чтобы запретить переупорядочивание как компилятором так и процессором и получить нужный нам порядок.
Если барьеры добавляют наблюдаемое поведение, то наверное, а если нет — то как они запретят?
Здравствуйте, _NN_, Вы писали:
_NN>Есть ли у нас гарантии от компилятора и от процессора, что toolRunner будет создан после того как будет создан tool ?
_NN>Насколько я понимаю, компилятор вправе переупорядочить создание если нет видимого эффекта. _NN>В данном случае нет разницы что будет создано первым, а значит можно поменять местами в целях оптимизации.
Нет разницы, это если компилятору доступны в этой точке как конструктор Tool так и std::thread,
а также код всех функций которые вызывают эти конструкторы, в том числи и тело функции типа "pthread_create",
если речь идет от Linux платформе.
Иначе как компилятор может понять что можно переупорядочивать конструкторы?
Ведь невидимые ему реализации функций могут использовать одни и те же глобальные переменные,
и таким образом зависеть друг от друга.
_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.
По возможности, избавляться от кода, выполняемого до main. Будь то нетривиальные конструкторы глобальных объектов, или функции, помеченные атрибутом [constructor]].
Синглетоны Мейерса — меньшее зло.
Здравствуйте, _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.
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.
Здравствуйте, _NN_, Вы писали:
_NN>Переменные они локальные в функции.
Ааа, так это были локальные переменные? Тогда гарантия последовательности точно есть.
Переупорядочивание возможно лишь в том случае, если компилятор уверен в отсутствии побочных эффектов (что с std::thread, очевидно, уже не так). Но если побочных эффектов нет, то и пользователю незаметно, переупорядочил там что-то компилятор или сохранил.
_NN>Даже если не локальные по видимому можно переупорядочить тоже.
В одной единице трансляции — нет.
На уровне разных единиц трансляции — можно.
На уровне разных библиотек — недетерминированно, может даже варьироваться от запуска к запуску.
И вот именно поэтому, из-за того, что возможны гонки инициализации — стоит избавляться от энергичных синглетонов в пользу ленивых.
Здравствуйте, 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.
Здесь аргумент toolRunner'а никак не связан с tool, поэтому вышеприведённая цитата не относится к делу. Вот если эти объекты связать, то тогда — да, синхронизация будет:
BFE>Если конструктор Tool не имеет побочных эффектов, то нет гарантии, что вызов функции printf("Starting tool"); будет произведён после (что бы это не значило) создания объекта tool. Вообще говоря стандартом не запрещается параллельное выполнение этого кода, скажем, на двух разных процессорах: на одном создание объекта tool, а на втором выполнение функции printf.
Как я понимаю даже если компилятор понимает, что порядок менять нельзя, присваивание значения 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); } );
Здравствуйте, _NN_, Вы писали:
_NN>А если у нас есть инициализация переменной ?
Простая инициализация сама по себе мало что меняет, а вот использование переменной — да.
_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 изменился:
— будет выведено либо 10, либо 11.
Это даже на прямую не связано с многопоточностью, а с тем что если переменная участвует в выражении, то значение переменной берётся из вычисления предыдущего выражения. В данном случае предыдущее выражение — это инициализация. Поэтому инициализация должна быть завершена до того, как переменную будут использовать через ссылку.
Если я правильно понимаю, то проблема с not thread-safe инициализацией может возникнуть только при использовании не static глобальной переменной в нескольких потоках. Или если саму переменную пытаются использовать прямо в выражении инициализации типа такого:
Здравствуйте, B0FEE664, Вы писали:
BFE>Здравствуйте, _NN_, Вы писали:
_NN>>А если у нас есть инициализация переменной ? BFE>Простая инициализация сама по себе мало что меняет, а вот использование переменной — да.
_NN>>
_NN>>Как я понимаю даже если компилятор понимает, что порядок менять нельзя, присваивание значения 10 может не отразится при чтении из другого потока. Верно ? BFE>Нет. В этом случае i гарантированно будет инициализировано до создания ссылки на i.
Хорошо, i содержит 10, ссылка ссылается на памить i.
Но что если у нас два процессора где первый записал себе значение, но оно не дошло до памяти и второй процессор ещё его не видит ?
Кто даст гарантию если не явный барьер памяти, что есть видимость во всех процессорах ?
Здравствуйте, _NN_, Вы писали:
_NN>Хорошо, i содержит 10, ссылка ссылается на памить i. _NN>Но что если у нас два процессора где первый записал себе значение, но оно не дошло до памяти и второй процессор ещё его не видит ? _NN>Кто даст гарантию если не явный барьер памяти, что есть видимость во всех процессорах ?
я конечно подотстал в новых стандартах в с++.
Но у тебя в коде указан сначала вызов конструктора для Tool, а потом для std::thread
Я не вижу отличий между вызовом двух конструкторов последовательно в одном потоке один за другим (что ты и делаешь — у тебя конструкторы в одном потоке вызываются) и просто вызовом двух функций последовательно в одном потоке.
Например
a();
b();
как компилятор может сначала вызвать b() перед a()?
у меня это в голове не укладывается. Такого быть не должно.
Поэтому мой ответ такой. Конструктор Tool полностью отработает до того как вызовется конструктор std::thread
и поэтому printf("Starting tool") вызовется когда tool уже создан.
Здравствуйте, 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) должен создавать барьер.
Здравствуйте, ·, Вы писали:
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 .
Здравствуйте, 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.