Поясните за деструкторы thread_local (3.6.3/1)
От: Alexander G Украина  
Дата: 15.03.17 16:40
Оценка:

The completions
of the destructors for all initialized objects with thread storage duration within that thread are sequenced
before the initiation of the destructors of any object with static storage duration.


А если мы подождём завершения нити прямо в деструкторе статического объекта?
Тогда я бы ожидал деструктор thread_local во время деструктора статического объекта.
И реализации (MSVC и gcc) так и делают.

Это undefined behavior, реализации не правы, или ошибка стандарта, или я чего-то не понимаю?

  код
#include <thread>
#include <atomic>
#include <chrono>
#include <iostream>

thread_local struct Z
{
    ~Z()
    {
         std::cout << "Destroyed Z\n";
    }

    void f() {}
} z;

static struct X
{
    X(): thd([this]
        {
            while ( ! stop.load() )
                std::this_thread::sleep_for(std::chrono::milliseconds(200));
            z.f();
        })
    {
        z.f();
    }

    ~X()
    {
        std::cout << "Destroying X\n";
        stop.store(true);
        thd.join();
        std::cout << "Destroyed X\n";
    }
    
    std::atomic_bool stop;
    std::thread thd;
} x;

int main()
{
    std::cout << "Exiting\n";
    return 0;
}


  вывод
Exiting
Destroyed Z
Destroying X
Destroyed Z
Destroyed X
Русский военный корабль идёт ко дну!
Re: Поясните за деструкторы thread_local (3.6.3/1)
От: Кодт Россия  
Дата: 15.03.17 17:40
Оценка: 10 (1)
Здравствуйте, Alexander G, Вы писали:

AG>

AG>The completions
AG>of the destructors for all initialized objects with thread storage duration within that thread are sequenced
AG>before the initiation of the destructors of any object with static storage duration.


Тут, скорее, речь вот о чём.
Главный поток (который main() ± конструкторы-деструкторы-exit-функции) — это тоже поток.
И его завершение происходит в таком порядке:
— вышли из main
— выполнили деструкторы TLS-объектов
— выполнили деструкторы SS-объектов, инициализированных по требованию
— выполнили деструкторы SS-объектов

http://ideone.com/B8XhTJ
#include <iostream>
using namespace std;

int g_tab = 1;
struct X {
    int tab;
    const char* name;
    X(const char* name) : name(name), tab(g_tab++) {
        cout << string(tab, '\t') << name << " : ctor" << endl;
    }
    ~X() {
        cout << string(tab, '\t') << name << " : dtor" << endl; }
};

X g1("g1");
thread_local X t1("t1");
thread_local X t2("t2");
X g2("g2");

int main() {
    cout << "start!" << endl;
    static X s1("s1");
    cout << "test " << t1.name << endl;  // первое обращение к TLS провоцирует инициализацию всех TLS-объектов
    static X s2("s2");
    cout << "finish!" << endl;
}

    g1 : ctor
        g2 : ctor
start!
            s1 : ctor
                t1 : ctor
                    t2 : ctor
test t1
                        s2 : ctor
finish!
                    t2 : dtor
                t1 : dtor
                        s2 : dtor
            s1 : dtor
        g2 : dtor
    g1 : dtor

Вот здесь таится опасность: нарушение порядка.
Перекуём баги на фичи!
Отредактировано 15.03.2017 18:05 Кодт . Предыдущая версия .
Re[2]: Поясните за деструкторы thread_local (3.6.3/1)
От: Alexander G Украина  
Дата: 15.03.17 19:25
Оценка:
Здравствуйте, Кодт, Вы писали:

К>Тут, скорее, речь вот о чём.

К>Главный поток (который main() ± конструкторы-деструкторы-exit-функции) — это тоже поток.

Ну если в этом смысле, то хорошо.
Я уже начал думать, что нельзя иметь синглтон с потоком и с его завершением в деструкторе не только в DLL, но и в С++...

К>И его завершение происходит в таком порядке:

К>- вышли из main
К>- выполнили деструкторы TLS-объектов
К>- выполнили деструкторы SS-объектов, инициализированных по требованию
К>- выполнили деструкторы SS-объектов

Последнее и предпоследнее не может вперемешку выполнятся?

Вроде реализации не разделяют:
http://ideone.com/yq6NJr
#include <iostream>
#include <string>
using namespace std;
 
int g_tab = 1;
struct X {
    int tab;
    const char* name;
    X(const char* name) : name(name), tab(g_tab++) {
        cout << string(tab, '\t') << name << " : ctor" << endl;
    }
    ~X() {
        cout << string(tab, '\t') << name << " : dtor" << endl; }
};
 
struct Y {
    Y() {
        static X s2("s2");
    }
};
 
Y y;
 
X g1("g1");
X g2("g2");
 
 
int main() {
    cout << "start!" << endl;
    static X s1("s1");
    cout << "finish!" << endl;
}

    s2 : ctor
        g1 : ctor
            g2 : ctor
start!
                s1 : ctor
finish!
                s1 : dtor
            g2 : dtor
        g1 : dtor
    s2 : dtor



К>// первое обращение к TLS провоцирует инициализацию всех TLS-объектов


А в MSVC сразу при старте потока (из TLS Callback). Это implementation-defined, или кто-то нарушает?
Русский военный корабль идёт ко дну!
Re[3]: Поясните за деструкторы thread_local (3.6.3/1)
От: Кодт Россия  
Дата: 16.03.17 10:10
Оценка: 5 (1)
Здравствуйте, Alexander G, Вы писали:

К>>- выполнили деструкторы SS-объектов, инициализированных по требованию

К>>- выполнили деструкторы SS-объектов

AG>Последнее и предпоследнее не может вперемешку выполнятся?


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

К>>первое обращение к TLS провоцирует инициализацию всех TLS-объектов


AG>А в MSVC сразу при старте потока (из TLS Callback). Это implementation-defined, или кто-то нарушает?


Implementation-defined.
Более того, и статические, и потоковые переменные (нелокальные) должны быть инициализированы до первого использования.
А будет это до входа в main или нет, — можно делать и так, и этак, и даже вперемешку!

3.6.2 Initialization of non-local variables [basic.start.init]

4
It is implementation-defined whether the dynamic initialization of a non-local variable with static storage
duration is done before the first statement of main. If the initialization is deferred to some point in time
after the first statement of main, it shall occur before the first odr-use (3.2) of any function or variable
defined in the same translation unit as the variable to be initialized.
[ Example:

// - File 1 -
#include "a.h"
#include "b.h"
B b;
A::A(){
b.Use();
}
// - File 2 -
#include "a.h"
A a;
// - File 3 -
#include "a.h"
#include "b.h"
extern A a;
extern B b;
int main() {
a.Use();
b.Use();
}

It is implementation-defined whether either a or b is initialized before main is entered or whether the
initializations are delayed until a is first odr-used in main. In particular, if a is initialized before main is
entered, it is not guaranteed that b will be initialized before it is odr-used by the initialization of a, that is,
before A::A is called. If, however, a is initialized at some point after the first statement of main, b will be
initialized prior to its use in A::A. — end example ]

5
It is implementation-defined whether the dynamic initialization of a non-local variable with static or thread
storage duration is done before the first statement of the initial function of the thread. If the initialization
is deferred to some point in time after the first statement of the initial function of the thread, it shall occur
before the first odr-use (3.2) of any variable with thread storage duration defined in the same translation
unit as the variable to be initialized.


Технически проще и экономнее создать одну мега-функцию конструирования всех статических переменных в единице трансляции, и парную к ней мега-функцию деструирования, и дёргать их до и после main, соответственно, — чем заниматься тем же самым с каждой переменной индивидуально. Но не возбраняется и индивидуально.
Перекуём баги на фичи!
Re[4]: Поясните за деструкторы thread_local (3.6.3/1)
От: Alexander G Украина  
Дата: 16.03.17 12:44
Оценка:
Здравствуйте, Кодт, Вы писали:

К>Implementation-defined.

К>Более того, и статические, и потоковые переменные (нелокальные) должны быть инициализированы до первого использования.
К>А будет это до входа в main или нет, — можно делать и так, и этак, и даже вперемешку!

Получается, это может не работать

static GlobalData g_GlobalData;
static thread_local ThreadData g_ThreadData; // ThreadData использует g_GlobalData


т.к.
1. g_ThreadData может быть сконструирован перед g_GlobalData (g_GlobalData отложенная, а g_ThreadData нет. Или треды в статических объектах)
2. g_ThreadData может жить дольше g_GlobalData (Треды в статических объектах)

Чтобы работало надо

std::shared_ptr<GlobalData> GetGlobalData() 
{
   static auto instance = std::make_shared<GlobalData>();
   return instance;
}

static thread_local ThreadData g_ThreadData; // ThreadData в своём конструкторе сохраняет у себя результат GetStaticData()



А чтобы работать в Windows, ещё нужно, чтобы ThreadData по умолчанию был "зомби", и получал свою GlobalData только когда фактически используется.
Потому что из-за стороннего софта к нам могут заходить левые треды в любомй момент, особенно в начале работы, до инициализации рантайма, см тред ниже
[MSVC] Грабли: thread_local могут конструироваться до обычных глобальных
Русский военный корабль идёт ко дну!
Re[5]: Поясните за деструкторы thread_local (3.6.3/1)
От: Кодт Россия  
Дата: 16.03.17 15:54
Оценка:
Здравствуйте, Alexander G, Вы писали:

AG>Получается, это может не работать

AG>static GlobalData g_GlobalData;
AG>static thread_local ThreadData g_ThreadData; // ThreadData использует g_GlobalData

Ну да.

Советую заглянуть в книгу Александреску Modern C++ Design (Современное программирование на С++).
Там целая глава посвящена разным синглетонам. И то, он не учитывал работу с динамическими модулями (dll/so), из-за чего его библиотека Loki крешилась. Но общую картину осветил хорошо, — просто не сделал последнего шага.

По сути, стандарт говорит, что компилятор может втихую заменить нелокальные переменные на синглетоны Мейерса. Со всеми вытекающими из этого последствиями.
А чтобы посторонние потоки не создавали экземпляры ThreadData без нужды, вот тут явные синглетоны по требованию — хоть Мейерса, хоть что-то более продвинутое, — пригодятся.

А что касается shared_ptr<GlobalData>,

Если к нему обращаться в конструкторе и в той же единице трансляции, то компилятор просто обязан создать GlobalData (и зарегистрировать его в atexit) до выхода из конструктора ThreadData и, соответственно, до регистрации того в atexit.
Поэтому деструкторы всех ThreadData выполнятся до деструктора GlobalData. То есть, shared_ptr избыточен.
Если запоминать shared_ptr как член при первом реальном обращении, — то нужно лишь обеспечить, что это первое реальное обращение не во время atexit. Либо делать синглетон-феникс.

Однако, есть рацио в том, чтобы делать именно член — shared_ptr. Это если объекты раскиданы по разным единицам трансляции.
Тут уже компилятор не будет гарантировать правильный порядок создания нелокальных объектов, а синглетоны Мейерса — вынужденно будут.
Перекуём баги на фичи!
Re[6]: Поясните за деструкторы thread_local (3.6.3/1)
От: Alexander G Украина  
Дата: 16.03.17 17:38
Оценка:
Здравствуйте, Кодт, Вы писали:

К>А что касается shared_ptr<GlobalData>,


К>Если к нему обращаться в конструкторе и в той же единице трансляции, то компилятор просто обязан создать GlobalData (и зарегистрировать его в atexit) до выхода из конструктора ThreadData и, соответственно, до регистрации того в atexit.


Я не представляю, как это отработает, если мы созданим потоки до main, и первый, кому понадобился GlobalData — не основной поток.

К>Поэтому деструкторы всех ThreadData выполнятся до деструктора GlobalData. То есть, shared_ptr избыточен.

К>Если запоминать shared_ptr как член при первом реальном обращении, — то нужно лишь обеспечить, что это первое реальное обращение не во время atexit. Либо делать синглетон-феникс.

Да, я ещё страхуюсь, что какой-то поток будет остановлен в atexit другого объекта, когда GlobalData уже уничтожен.
Логика такова, что в некоторых случаях после использования GlobalData в ThreadData нужен GlobalData в деструкторе ThreadData.

К>Однако, есть рацио в том, чтобы делать именно член — shared_ptr. Это если объекты раскиданы по разным единицам трансляции.


Ах, ну и да, ещё и поэтому, у меня это статики шаблонного класса, т.е. каждый в какой-то единице трансляции...
Русский военный корабль идёт ко дну!
Re: Поясните за деструкторы thread_local (3.6.3/1)
От: vdimas Россия  
Дата: 18.03.17 22:26
Оценка:
Здравствуйте, Alexander G, Вы писали:

AG>А если мы подождём завершения нити прямо в деструкторе статического объекта?


Ненадежно.
Есть же ф-ия atexit(), куда можно подписаться до того, как будут вызваны деструкторы глобальных и TLS объектов.

#include <thread>
#include <atomic>
#include <chrono>
#include <iostream>

void do_stop();

thread_local struct Z {
    ~Z() { std::cout << "Destroyed Z\n"; }
    void f() {}
} z;

static struct X {
    X(): thd([this] {
            while ( ! stop.load() )
                std::this_thread::sleep_for(std::chrono::milliseconds(200));
            z.f();  }) {
       std::atexit(do_stop);
       z.f();
    }

    void stop() {
        std::cout << "Destroying X\n";
        stop.store(true);
        thd.join();
        std::cout << "Destroyed X\n";
    }
    
    std::atomic_bool stop;
    std::thread thd;
} x;

void do_stop() {
    x.stop();
}

int main() {
    std::cout << "Exiting\n";
    return 0;
}
Re[4]: Поясните за деструкторы thread_local (3.6.3/1)
От: vdimas Россия  
Дата: 18.03.17 23:10
Оценка:
Здравствуйте, Кодт, Вы писали:

К>Если один объект из своего конструктора или из инициализирующей функции создал другой статический объект, тот добавится вперёд него


Сам добавиться?
Re[2]: Поясните за деструкторы thread_local (3.6.3/1)
От: Alexander G Украина  
Дата: 19.03.17 12:46
Оценка: +1 :)
Здравствуйте, vdimas, Вы писали:

V>Ненадежно.

V>Есть же ф-ия atexit(), куда можно подписаться до того, как будут вызваны деструкторы глобальных

Не, atexit и статические и глобальные объекты — та же LIFO-очередь.

Код с atexit падает и в MSVC и в gcc через terminate -> abort.
Получается, что do_stop регистрируется в atexit очереди раньше деструктора X.
Поэтому do_stop будет вызывается позже деструктора X.
Т.е. до do_stop выполнение не доходит: попытка уничтожить работающий std::thread thd; приводит к terminate.

V>и TLS объектов.


Ну соответственно и TLS объекты exit-потока разрушаются раньше вызовов atexit.
Русский военный корабль идёт ко дну!
Re: Поясните за деструкторы thread_local (3.6.3/1)
От: EyeGem Россия https://vk.com/enginya
Дата: 20.03.17 10:06
Оценка: +1 -1
Здравствуйте, Alexander G, Вы писали:

AG>

AG>Поясните за деструкторы


Простите, но предлог "эа" в русском языке таким образом НЕ употребляется.
Это абсолютное проявление безграмотности и пофигизма.

Правильно: Поясните про декструкторы
Правильно: Расскажите о декструкторах

за ~ за что? => наказать за ошибку, играть за Россию
за ~ за кого? => ложку за папу, ложку за маму
за ~ за чем? => (где?) пёс за забором, (какова цель?) сходить за колбасой
зачем? => (по какой причине?) потому что балбес

^__^
^__^
Re[2]: Поясните за деструкторы thread_local (3.6.3/1)
От: LuciferSaratov Россия  
Дата: 20.03.17 19:17
Оценка:
Здравствуйте, EyeGem, Вы писали:

EG>Простите, но предлог "эа" в русском языке таким образом НЕ употребляется.

EG>Это абсолютное проявление безграмотности и пофигизма.

не простим.
во-первых, правилами сайта запрещено докапываться до грамматики и орфографии.
во-вторых, в русском языке предлог "за" таким образом употребляют на юге России.
Re[3]: Поясните за деструкторы thread_local (3.6.3/1)
От: EyeGem Россия https://vk.com/enginya
Дата: 20.03.17 22:52
Оценка: +1
Здравствуйте, LuciferSaratov, Вы писали:

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


EG>>Простите, но предлог "эа" в русском языке таким образом НЕ употребляется.

EG>>Это абсолютное проявление безграмотности и пофигизма.

LS>не простим.

LS>во-первых, правилами сайта запрещено докапываться до грамматики и орфографии.
LS>во-вторых, в русском языке предлог "за" таким образом употребляют на юге России.

Согласно https://ru.wiktionary.org/wiki/%D0%B7%D0%B0 это причерноморск. болг. региональное отклонение.
В нормальном русском языке этот предлог так не употребляется.

И я не докапываюсь до автора, мне хочется чтобы другие понимали что таким образом лучше не изъясняться.
А автор что, его вряд ли исправишь )
^__^
Re[5]: Поясните за деструкторы thread_local (3.6.3/1)
От: Кодт Россия  
Дата: 21.03.17 10:52
Оценка:
Здравствуйте, vdimas, Вы писали:

К>>Если один объект из своего конструктора или из инициализирующей функции создал другой статический объект, тот добавится вперёд него

V>Сам добавиться?

Ну как сам.
Рядом с каждым статическим объектом есть две незримых функции — первая его инициализирует и добавляет в atexit вторую, а вторая разрушает.
Если очень грубо, то
// static X x (a,b,c);

using X_Data = aligned_storage<sizeof(X), alignof(X)>::type;

static X_Data x_data;
static bool x_created = false;

static X& x = (X&)x_data;

void touch_x() {
  if (!x_created) {  // сейчас не будем морочить себе голову вопросами реентера и многопоточности
    new (&x) X(a,b,c);
    atexit(kill_x);
    x_created = true;
  }
}
void kill_x() {
  if (x_created)
    x.~X();
}

X& take_x() { touch_x(); return x; }

//////// зависимый от него static Y y(x);

void touch_y() {
  if (!y_created) {
    new (&y) Y(take_x()); // в рамках take_x -> touch_x мы добавим kill_x
    atexit(kill_y);       // вперёд добавления kill_y
    y_created = true;
  }
}


Для локальных объектов (синглетоны Мейерса) всё это добро существует физически.
Для нелокальных — если компилятор предпочитает пакетную инициализацию всех переменных в единице трансляции, то он может сократить слагаемые — родить две такие функции touch_everything, kill_everything на всю единицу.
Перекуём баги на фичи!
Re[6]: Поясните за деструкторы thread_local (3.6.3/1)
От: Alexander G Украина  
Дата: 21.03.17 11:22
Оценка:
Здравствуйте, Кодт, Вы писали:

К>Для нелокальных — если компилятор предпочитает пакетную инициализацию всех переменных в единице трансляции, то он может сократить слагаемые — родить две такие функции touch_everything, kill_everything на всю единицу.


kill_everything точно может?

Реализации выводят строки 1, 2, 3.
C kill_everything, получается, будет 1, 3, 2
и UB, если разкомментировать x.f
(да и с закоментированным тоже, cout тоже объект, попадающий в kill_everything).

#include <iostream>
using namespace std;

struct X
{
    ~X() { std::cout << "3\n";  }
    
    void f() {}
} x;


struct Y
{
    ~Y() 
    {
        static struct Z 
        {
            ~Z() 
            {
                //x.f();
                std::cout << "2\n";
            } 
        } z;
        
        std::cout << "1\n";  
    }
} y;

int main() 
{
    return 0;
}

http://ideone.com/wMuUGA
Русский военный корабль идёт ко дну!
Отредактировано 21.03.2017 11:33 Alexander G . Предыдущая версия . Еще …
Отредактировано 21.03.2017 11:26 Alexander G . Предыдущая версия .
Re[6]: Поясните за деструкторы thread_local (3.6.3/1)
От: vdimas Россия  
Дата: 21.03.17 11:53
Оценка:
Здравствуйте, Кодт, Вы писали:

К>Ну как сам.

К>Рядом с каждым статическим объектом есть две незримых функции — первая его инициализирует и добавляет в atexit вторую, а вторая разрушает.

Да.
Только это работает на уровне модуля, т.к. гарантируется строгий порядок создания/разрушения глобальных переменных, определённых в модуле.


К>Если очень грубо, то


Я наблюдал поведение, описанное у тебя в коде, только для статических переменных в ф-иях. Именно они инициализируются "по-запросу", т.е. при первом обращении к ф-ии, и я на это регулярно полагаюсь в разработке (с точностью до обыгрывания трюков многопоточной такой инициализации, если такая -фия вызывается из нескольких потоков).

Или еще известный трюк — некие объекты саморегистрируются в каком-то глобальном связанном списке, при том, что прямого доступа к конкретной зарегестрированной переменной в явном виде нет, есть только при обходе такого списка. Такой трюк надёжно работает только для глобальных переменных, объявленных в модулях из целевого образа, т.е. если эти переменные идут из библиотеки, то это не всегда сработает (зависит от наличия других явных зависимостей из этого модуля).


К>Для нелокальных — если компилятор предпочитает пакетную инициализацию всех переменных в единице трансляции


Ну, если он должен обеспечить строгую взаимную очередность инициализации внутри модуля, то логика обыгрывания инциализации переменных модуля "по запросу" становится слишком сложна. ИМХО, обеспечение операции "проинициализируй всё в модуле до конкретной переменной" — это уже перебор. )) Действительно ли такое где-то реализовано в существующих компиляторах?
Re[7]: Поясните за деструкторы thread_local (3.6.3/1)
От: Кодт Россия  
Дата: 21.03.17 12:05
Оценка:
Здравствуйте, vdimas, Вы писали:

К>>Для нелокальных — если компилятор предпочитает пакетную инициализацию всех переменных в единице трансляции


V>Ну, если он должен обеспечить строгую взаимную очередность инициализации внутри модуля, то логика обыгрывания инциализации переменных модуля "по запросу" становится слишком сложна. ИМХО, обеспечение операции "проинициализируй всё в модуле до конкретной переменной" — это уже перебор. )) Действительно ли такое где-то реализовано в существующих компиляторах?


Чёрт их знает. Так-то было бы логичнее делать пакетную инициализацию, но поштучную деструкцию (потому что туда могут затесаться внешние зависимости).
Или поштучные синглетоны, но пакетный забег по ним — первое касание.
Перекуём баги на фичи!
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.