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

131

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

polapts
источник

Ответы:

151

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

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

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

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

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

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

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

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

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

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

paxdiablo
источник
4
Использование локального потока не решает проблем с strtok. strtokне работает даже в однопоточной среде.
Джеймс Канце
11
Извините, позвольте мне перефразировать это. Никаких новых проблем со strtok не возникает :-)
paxdiablo
7
Фактически, это rозначает «повторный вход», который не имеет ничего общего с безопасностью потоков. Верно, что вы можете заставить некоторые вещи работать поточно-безопасно с помощью локального хранилища потока, но вы не можете сделать их реентерабельными.
Kerrek SB
5
В однопоточной среде функции необходимо повторно использовать только в том случае, если они являются частью цикла в графе вызовов. Листовая функция (та, которая не вызывает другие функции) по определению не является частью цикла, и нет веских причин для strtokвызова других функций.
MSalters
3
это while (something) { char *next = strtok(whatever); someFunction(next); // someFunction calls strtok }
испортило
135

Когда вы объявляете переменную, 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».

Энтони Уильямс
источник
1
Я думаю, важно отметить, что локальная для потока копия переменной - это вновь инициализированная копия переменной. То есть, если добавить g()вызов в начало threadFunc, то на выходе будет 0304029либо какая-то другая перестановка пар 02,03 и 04. То есть, несмотря на то, что 9 назначается iдо создания потоков, потоки получают только что созданную копию iwhere i=0. Если iприсвоено thread_local int i = random_integer(), то каждый поток получает новое случайное целое число.
Mark H
Не совсем перестановка 02, 03, 04, могут существовать и другие последовательности , как020043
Hongxu Чена
Интересный лакомый кусочек, который я только что обнаружил: GCC поддерживает использование адреса переменной thread_local в качестве аргумента шаблона, но другие компиляторы этого не делают (на момент написания; пробовал clang, vstudio). Я не уверен, что об этом говорится в стандарте, или это неуказанная область.
jwd
23

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