Меня смущает описание thread_local
в C ++ 11. Насколько я понимаю, каждый поток имеет уникальную копию локальных переменных в функции. К глобальным / статическим переменным могут получить доступ все потоки (возможно, синхронизированный доступ с использованием блокировок). И переменные thread_local
видны всем потокам, но могут быть изменены только тем потоком, для которого они определены? Это правильно?
Что означает thread_local в C ++ 11?
Ответы (3)
Продолжительность локального хранения потока - это термин, используемый для обозначения данных, которые кажутся глобальными или статическими (с точки зрения используемых функций), но на самом деле существует одна копия на поток.
Он добавляет к текущему автоматическому (существует во время блока / функции), статическому (существует на время выполнения программы) и динамическому (существует в куче между выделением и освобождением).
Что-то, что является локальным для потока, создается при создании потока и удаляется, когда поток останавливается.
Ниже приведены некоторые примеры.
Подумайте о генераторе случайных чисел, в котором начальное число должно поддерживаться для каждого потока. Использование начального числа локального потока означает, что каждый поток получает свою собственную последовательность случайных чисел, независимую от других потоков.
Если бы ваше семя было локальной переменной в случайной функции, оно инициализировалось бы каждый раз, когда вы его вызывали, давая вам каждый раз одно и то же число. Если бы он был глобальным, потоки вмешивались бы в последовательности друг друга.
Другой пример - это что-то вроде strtok
, где состояние токенизации хранится в зависимости от потока. Таким образом, один поток может быть уверен, что другие потоки не испортят его усилия по токенизации, при этом сохраняя состояние при нескольких вызовах strtok
- это в основном делает strtok_r
(поточно-ориентированная версия) избыточным.
Оба этих примера позволяют локальной переменной потока существовать внутри функции, которая ее использует. В предварительно запрограммированном коде это будет просто статическая переменная продолжительности хранения внутри функции. Для потоков это изменено на продолжительность локального хранилища потока.
Еще один пример - что-то вроде errno
. Вам не нужно, чтобы отдельные потоки изменяли errno
после сбоя одного из ваших вызовов, но до того, как вы сможете проверить переменную, и все же вам нужна только одна копия на поток.
На этом сайте есть разумное описание различных спецификаторов продолжительности хранения.
strtok
. strtok
не работает даже в однопоточной среде.
- person James Kanze; 16.08.2012
r
означает повторный вход, который не имеет ничего общего с безопасностью потоков. Верно, что вы можете заставить некоторые вещи работать с потокобезопасностью с помощью локального хранилища потоков, но вы не можете сделать их реентерабельными.
- person Kerrek SB; 16.08.2012
strtok
должен вызывать другие функции.
- person MSalters; 16.08.2012
while (something) { char *next = strtok(whatever); someFunction(next); // someFunction calls strtok }
- person japreiss; 26.06.2014
strtok
в одном потоке; скажем, если вы обрабатываете две строки одновременно. Вот где пригодятся реентерабельные варианты (плюс они чище - глобальные переменные не доступны).
- person Tim Čas; 11.02.2015
thread_local object
свой освободитель в конце потока?
- person Dr. Jekyll; 27.01.2017
strtok
. Я проверил glibc из подсказки, реализация strtok
в две строчки и вызывает strtok_r
.
- person haxpor; 16.10.2019
Когда вы объявляете переменную 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».
g()
в начало threadFunc
, то на выходе будет 0304029
или другая перестановка пар 02
, 03
и 04
. То есть, даже если 9 присваивается i
перед созданием потоков, потоки получают только что созданную копию i
, где i=0
. Если i
присваивается thread_local int i = random_integer()
, то каждый поток получает новое случайное целое число.
- person Mark H; 12.06.2017
02
, 03
, 04
, могут быть другие последовательности, такие как 020043
- person Hongxu Chen; 17.09.2018
Локальное хранилище потока во всех аспектах подобно статическому (= глобальному) хранилищу, только то, что каждый поток имеет отдельную копию объекта. Время жизни объекта начинается либо при запуске потока (для глобальных переменных), либо при первой инициализации (для локальной статики блока) и заканчивается, когда поток завершается (т.е. когда вызывается 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;
}
};