thread_local в С++ 11 несовместим

Почему вывод не соответствует 1, когда я объявил объект Counter как thread_local?

#include <iostream>
#include <thread>
#define MAX 10
    
class Counter {
    int c;
public:
    Counter() : c(0) {}

    void increment() {
        ++c;
    }

    ~Counter() {   
        std::cout << "Thread " << std::this_thread::get_id() << " having counter value " << c << "\n";  
    }
};
    
thread_local Counter c;
    
void doWork() {
    c.increment();
}
    
int main() {
    std::thread t[MAX];
    for ( int i = 0; i < MAX; i++ )
        t[i] = std::thread(doWork);
    for ( int i = 0; i < MAX; i++ )
        t[i].join();    
    return 0;
}

Выход :

Thread 2 having counter value 19469616
Thread 3 having counter value 1
Thread 4 having counter value 19464528
Thread 5 having counter value 19464528
Thread 7 having counter value 1
Thread 6 having counter value 1
Thread 8 having counter value 1
Thread 9 having counter value 1
Thread 10 having counter value 1
Thread 11 having counter value 1

person user3798283    schedule 01.03.2021    source источник
comment
Сообщить об ошибке компилятора   -  person M.M    schedule 01.03.2021


Ответы (1)


Единственная интересная вещь, которую я извлек из небольшого изучения, это то, что MSVC работает иначе, чем clang/gcc.

Вот вывод MSVC для последнего компилятора:

Thread 34316 having counter value 1
Thread 34312 having counter value 1
Thread 34320 having counter value 1
Thread 34328 having counter value 1
Thread 34324 having counter value 1
Thread 34332 having counter value 1
Thread 34336 having counter value 1
Thread 34340 having counter value 1
Thread 34344 having counter value 1
Thread 34348 having counter value 1
Thread 29300 having counter value 0

c:\dv\TestCpp\out\build\x64-Debug (default)\TestCpp.exe (process 27748) exited with code 0.

Примечание. Поток 29300 имеет значение счетчика 0. Это основной поток. Поскольку doWork() никогда не вызывался в основном потоке, значение счетчика равно 0. Это работает так, как я ожидал, что все будет работать. c — это глобальный объект Counter — он должен создаваться во всех потоках, включая основной поток.

Однако, когда я запускаю (а godbolt, похоже, не любит запускать многопоточные программы, AFAICT) под clang и gcc с использованием rextester.com, я получаю:

Thread 140199006193408 having counter value 1
Thread 140198906181376 having counter value 1
Thread 140198806169344 having counter value 1
Thread 140198706157312 having counter value 1
Thread 140199106205440 having counter value 1
Thread 140199206217472 having counter value 1
Thread 140199306229504 having counter value 1
Thread 140199406241536 having counter value 1
Thread 140199506253568 having counter value 1
Thread 140199606265600 having counter value 1

Примечание. Нет основного потока со значением счетчика, равным 0. Объект c Counter никогда не создавался и не уничтожался в основном потоке в gcc и clang, однако он создавался и уничтожался в основном потоке в MSVC.

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

Кажется (изучая https://en.cppreference.com/w/cpp/language/storage_duration), что MSVC может делать это неправильно, поскольку к объекту thread_local Counter c никогда не было явного доступа в основном потоке, и тем не менее объект был создан и уничтожен.

Посмотрим, что они скажут - у меня есть еще несколько ошибок, ожидающих их решения...

Дальнейшее исследование: справочная статья cpp, по-видимому, указывает на то, что, поскольку это чисто глобальный объект, то есть это не локальный объект статического потока, например:

Foo &
GetFooThreadSingleton()
{
  static thread_local Foo foo;
  return foo;
}

тогда я прочитал статью cppreference так: это глобальный, как и любой другой глобальный, и в этом случае MSVC делает это правильно. Кто-нибудь, пожалуйста, поправьте меня, если я ошибаюсь. Спасибо.

Кроме того, не запуская его изначально в Ubuntu внутри gdb и не проверяя, я не могу быть уверен, что глобальный объект на самом деле не был создан и уничтожен в основном потоке под gcc/clang, потому что могло случиться так, что мы просто пропустили этот вывод по какой-то причине. Я признаю, что это кажется маловероятным. Я попробую это позже, потому что этот вопрос меня немного заинтриговал.

Я запустил его изначально в Ubuntu и получил то, что ожидал: глобальный счетчик c создается и уничтожается только в том случае, если к нему обращаются в основном потоке. Итак, я думаю, что на данный момент это ошибка в gcc/clang.

person David Bien    schedule 02.03.2021
comment
Неважно, я не видел, что это был деструктор, а не increment(), который делал регистрацию. - person Remy Lebeau; 02.03.2021