Что означает thread_local в C++11?
Меня смущает описание thread_local
в C++11. Насколько я понимаю, каждый поток имеет уникальную копию локальных переменных в функции. К глобальным / статическим переменным могут обращаться все потоки (возможно, синхронизированный доступ с использованием блокировок). И thread_local
переменные видны всем потокам, но могут быть изменены только тем потоком, для которого они определены? Это правильно?
3 ответа
Длительность локального хранилища потока - это термин, используемый для обозначения данных, которые кажутся глобальными или статическими с точки зрения продолжительности хранения (с точки зрения функций, использующих их), но на самом деле существует одна копия на поток.
Он добавляет к текущему автоматическому (существует во время блока / функции), статическому (существует для продолжительности программы) и динамическому (существует в куче между распределением и освобождением).
Нечто локальное для потока создается при создании потока и удаляется при его остановке.
Вот несколько примеров.
Подумайте о генераторе случайных чисел, где начальное число должно поддерживаться для каждого потока. Использование локального потока означает, что каждый поток получает свою собственную последовательность случайных чисел, независимую от других потоков.
Если ваше начальное число является локальной переменной внутри случайной функции, оно будет инициализироваться каждый раз, когда вы вызываете его, давая вам одно и то же число каждый раз. Если бы он был глобальным, потоки мешали бы последовательностям друг друга.
Другой пример что-то вроде strtok
где состояние токенизации хранится в зависимости от потока. Таким образом, один поток может быть уверен, что другие потоки не будут мешать его усилиям по токенизации, в то же время сохраняя возможность поддерживать состояние по нескольким вызовам strtok
- это в основном делает strtok_r
(потокобезопасная версия) избыточна.
Оба эти примера допускают существование локальной переменной потока внутри функции, которая ее использует. В предварительно поточном коде это просто будет статическая переменная продолжительности хранения внутри функции. Для потоков это изменено на продолжительность локального хранилища потоков.
Еще один пример будет что-то вроде errno
, Вы не хотите изменять отдельные потоки errno
после сбоя одного из ваших вызовов, но до того, как вы сможете проверить переменную, и при этом вам нужна только одна копия на поток.
Этот сайт имеет разумное описание различных спецификаторов длительности хранения.
Когда вы объявляете переменную 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;
}
void do_nothing(){}
int main(){
std::thread t1(do_nothing);
t1.join();
}
В этой программе есть 2 потока: основной поток и созданный вручную поток. Ни один поток не вызывает f
, Итак thread_local
объект никогда не используется. Поэтому не определено, будет ли компилятор создавать 0, 1 или 2 экземпляра my_class
и вывод может быть "", "hellohellogoodbyegoodbye" или "hellogoodbye".
Локальное хранилище потоков в каждом аспекте похоже на статическое (= глобальное) хранилище, только в том, что каждый поток имеет отдельную копию объекта. Время жизни объекта начинается либо при запуске потока (для глобальных переменных), либо при первой инициализации (для статики локального блока) и заканчивается, когда заканчивается поток (т.е. когда join()
называется).
Следовательно, только переменные, которые также могут быть объявлены static
может быть объявлен как thread_local
т. е. глобальные переменные (точнее: переменные "в области пространства имен"), статические члены класса и блочные статические переменные (в этом случае 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;
}
};