Що означає потік_локалу в C ++ 11?


131

Мене плутають з описом thread_localв C ++ 11. Наскільки я розумію, кожен потік має унікальну копію локальних змінних у функції. До глобальних / статичних змінних можна отримати доступ до всіх потоків (можливо, синхронізований доступ за допомогою блокування). І thread_localзмінні видимі для всіх потоків, але можуть змінюватися лише тим потоком, для якого вони визначені? Це правильно?

Відповіді:


151

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

Він додає до поточного автоматичного (існує під час блоку / функції), статичного (існує протягом тривалості програми) та динамічного (існує в купі між розподілом і делокацією).

Те, що є локальним потоком, створюється при створенні нитки і видаляється, коли нитка зупиняється.

Деякі приклади випливають.

Подумайте про генератор випадкових чисел, де насіння потрібно підтримувати на основі нитки. Використання локального насіння ниток означає, що кожен потік отримує власну послідовність випадкових чисел, незалежну від інших потоків.

Якщо ваше насіння було локальною змінною в межах випадкової функції, воно буде ініціалізуватися кожного разу, коли ви його викликали, даючи вам однакове число кожного разу. Якби це був глобальний, нитки заважали б послідовності один одного.

Інший приклад - це щось на зразок, strtokколи стан токенізації зберігається відповідно до потоку. Таким чином, один потік може бути впевнений, що інші потоки не виконуватимуть зусилля з токенізації, в той час як все ще в змозі підтримувати стан над декількома викликами до strtok- це в основному робить strtok_r(безпечна для потоків версія) надмірною.

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

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

На цьому веб-сайті є розумний опис різних специфікаторів тривалості зберігання.


4
Використання локального потоку не вирішує проблем із strtok. strtokруйнується навіть в одному різьбовому середовищі.
Джеймс Канзе

11
Вибачте, дозвольте перефразувати це. Не створює нових проблем зі strtok :-)
paxdiablo

7
Насправді, rстоїть "повторний абітурієнт", що не має нічого спільного з безпекою ниток. Це правда, що ви можете змусити деякі речі безпечно працювати з потоком з локальним сховищем, але ви не можете зробити їх повторними.
Керрек СБ

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створення ниток, потоки отримують свіжо сконструйовану копію iкуди i=0. Якщо iприсвоєно thread_local int i = random_integer(), то кожен потік отримує нове випадкове ціле число.
Марк Н

Не зовсім перестановка 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;
     }
};
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.