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

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


person polapts    schedule 16.08.2012    source источник


Ответы (3)


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

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

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

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

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

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

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

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

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

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

person paxdiablo    schedule 16.08.2012
comment
Использование локального потока не решает проблем с strtok. strtok не работает даже в однопоточной среде. - person James Kanze; 16.08.2012
comment
Извините, позвольте мне перефразировать это. Не вызывает никаких новых проблем со strtok :-) - person paxdiablo; 16.08.2012
comment
Фактически, r означает повторный вход, который не имеет ничего общего с безопасностью потоков. Верно, что вы можете заставить некоторые вещи работать с потокобезопасностью с помощью локального хранилища потоков, но вы не можете сделать их реентерабельными. - person Kerrek SB; 16.08.2012
comment
В однопоточной среде функции должны повторно входить только в том случае, если они являются частью цикла в графе вызовов. Листовая функция (та, которая не вызывает другие функции) по определению не является частью цикла, и нет веских причин, по которым strtok должен вызывать другие функции. - person MSalters; 16.08.2012
comment
это испортит: while (something) { char *next = strtok(whatever); someFunction(next); // someFunction calls strtok } - person japreiss; 26.06.2014
comment
@MSalters: Проблемы возникают, если вы (пытаетесь) переплетать две последовательности strtok в одном потоке; скажем, если вы обрабатываете две строки одновременно. Вот где пригодятся реентерабельные варианты (плюс они чище - глобальные переменные не доступны). - person Tim Čas; 11.02.2015
comment
Вызывает ли thread_local object свой освободитель в конце потока? - person Dr. Jekyll; 27.01.2017
comment
+1 Отличный пример для 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».

person Anthony Williams    schedule 16.08.2012
comment
Я думаю, важно отметить, что локальная для потока копия переменной - это вновь инициализированная копия переменной. То есть, если вы добавите вызов g() в начало threadFunc, то на выходе будет 0304029 или другая перестановка пар 02, 03 и 04. То есть, даже если 9 присваивается i перед созданием потоков, потоки получают только что созданную копию i, где i=0. Если i присваивается thread_local int i = random_integer(), то каждый поток получает новое случайное целое число. - person Mark H; 12.06.2017
comment
Не совсем перестановка 02, 03, 04, могут быть другие последовательности, такие как 020043 - person Hongxu Chen; 17.09.2018
comment
Я только что нашел интересный лакомый кусочек: GCC поддерживает использование адреса переменной thread_local в качестве аргумента шаблона, но другие компиляторы этого не делают (на момент написания; пробовал clang, vstudio). Я не уверен, что об этом говорится в стандарте, и если это неуказанная область. - person jwd; 11.07.2020

Локальное хранилище потока во всех аспектах подобно статическому (= глобальному) хранилищу, только то, что каждый поток имеет отдельную копию объекта. Время жизни объекта начинается либо при запуске потока (для глобальных переменных), либо при первой инициализации (для локальной статики блока) и заканчивается, когда поток завершается (т.е. когда вызывается 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;
     }
};
person Kerrek SB    schedule 16.08.2012