Что означает значение thread_local в С++ 11?

Меня смущает описание thread_local в С++ 11. Насколько я понимаю, каждый поток имеет уникальную копию локальных переменных в функции. К глобальным/статическим переменным могут обращаться все потоки (возможно, синхронизированный доступ с использованием блокировок). И переменные thread_local видны всем потокам, но могут быть изменены только тем потоком, для которого они определены? Это правильно?

Ответ 1

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

Он добавляет к текущему автоматическому (существует во время блока/функции), статический (существует для продолжительности программы) и динамический (существует в куче между распределением и освобождением).

То, что является поточно-локальным, создается при создании потока и удаляется, когда поток останавливается.

Ниже приводятся некоторые примеры.

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

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

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

Оба этих примера допускают существование локальной переменной потока внутри используемой функции. В коде с предварительной резьбой это просто будет переменной статической продолжительности хранения в функции. Для потоков, которые изменяются для локальной локализации хранилища.

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

Этот сайт содержит разумное описание различных спецификаций продолжительности хранения.

Ответ 2

Когда вы объявляете переменную thread_local, тогда каждый поток имеет свою собственную копию. Когда вы ссылаетесь на него по имени, то используется копия, связанная с текущим потоком. например,

thread_local int i=0;

void f(int newval){
    i=newval;
}

void g(){
    std::cout<<i;
}

void threadfunc(int id){
    f(id);
    ++i;
    g();
}

int main(){
    i=9;
    std::thread t1(threadfunc,1);
    std::thread t2(threadfunc,2);
    std::thread t3(threadfunc,3);

    t1.join();
    t2.join();
    t3.join();
    std::cout<<i<<std::endl;
}

Этот код выведет "2349", "3249", "4239", "4329", "2439" или "3429", но больше ничего. Каждый поток имеет свою собственную копию i, которая назначается, увеличивается и затем печатается. Поток, выполняющий main, также имеет свою собственную копию, которая назначается в начале, а затем остается неизменной. Эти копии полностью независимы, и у каждого есть свой адрес.

Только имя является особенным в этом отношении - если вы берете адрес переменной thread_local, то у вас просто есть нормальный указатель на обычный объект, который вы можете свободно передавать между потоками, например,

thread_local int i=0;

void thread_func(int*p){
    *p=42;
}

int main(){
    i=9;
    std::thread t(thread_func,&i);
    t.join();
    std::cout<<i<<std::endl;
}

Поскольку адрес i передается функции потока, то копия i, принадлежащая основному потоку, может быть назначена, даже если это thread_local. Таким образом, эта программа выведет "42". Если вы сделаете это, то вам нужно позаботиться о том, чтобы *p не был доступен после выхода из потока, которому он принадлежит, в противном случае вы получите висячий указатель и неопределенное поведение, как и в любом другом случае, когда указанный объект уничтожен.

thread_local переменные инициализируются "перед первым использованием", поэтому, если они никогда не будут затронуты данным потоком, они не обязательно будут инициализированы. Это позволяет компиляторам избегать создания каждой переменной thread_local в программе для потока, который является полностью автономным и не затрагивает ни одну из них. например,

struct my_class{
    my_class(){
        std::cout<<"hello";
    }
    ~my_class(){
        std::cout<<"goodbye";
    }
};

void f(){
    thread_local my_class unused;
}

void do_nothing(){}

int main(){
    std::thread t1(do_nothing);
    t1.join();
}

В этой программе есть 2 потока: основной поток и созданный вручную поток. Ни один из потоков не вызывает f, поэтому объект thread_local никогда не используется. Поэтому не определено, будет ли компилятор создавать 0, 1 или 2 экземпляра my_class, и выходные данные могут быть "", "hellohellogoodbyegoodbye" или "hellogoodbye".

Ответ 3

Потоковая локальная память хранится во всех аспектах, таких как static (= global), только для каждого потока есть отдельная копия объекта. Время жизни объекта начинается либо при запуске потока (для глобальных переменных), либо при начальной инициализации (для локально-локальной статики) и заканчивается, когда заканчивается поток (т.е. При вызове join()).

Следовательно, только переменные, которые также могут быть объявлены static, могут быть объявлены как thread_local, то есть глобальные переменные (точнее: переменные "в области пространства имен" ), статические члены класса и блок-статические переменные (в которых case static).

В качестве примера предположим, что у вас есть пул потоков и вы хотите знать, насколько сбалансирована ваша рабочая нагрузка:

thread_local Counter c;

void do_work()
{
    c.increment();
    // ...
}

int main()
{
    std::thread t(do_work);   // your thread-pool would go here
    t.join();
}

Это будет печатать статистику использования потоков, например. с реализацией следующим образом:

struct Counter
{
     unsigned int c = 0;
     void increment() { ++c; }
     ~Counter()
     {
         std::cout << "Thread #" << std::this_thread::id() << " was called "
                   << c << " times" << std::endl;
     }
};