Инициализация статической локальной переменной в многопоточной среде

Предположим, что есть функция (возможно, функция-член)

SomeType foo()
{
    static SomeType var = generateVar();
    return var;
}

Как будет инициализирован var, если foo будет вызываться "впервые" из нескольких потоков одновременно?

  1. Гарантируется ли, что generateVar() будет вызываться только один раз в любом сценарии (если, конечно, используется)?
  2. Гарантируется ли, что foo вернет одно и то же значение при многократном вызове в любом сценарии?
  3. Есть ли разница в поведении для примитивных или непримитивных типов?

person Andrew    schedule 24.05.2013    source источник


Ответы (1)


Относительно C++03:

Абстрактная машина, определенная стандартом C++03, не содержит формального определения того, что такое поток и каким должен быть результат программы, если к объекту осуществляется одновременный доступ.

Не существует понятия примитив синхронизации, упорядочения операций, выполняемых в разных потоках, гонки данных и так далее. Поэтому по определению каждая многопоточная программа на C++03 содержит неопределенное поведение.

Конечно, на практике реализации обеспечивают задокументированное поведение, но в Стандарте нет ничего, определяющего, каким должно быть это поведение. Поэтому я бы сказал, что это зависит от вашего компилятора.

Остальная часть ответа будет посвящена С++ 11, который определяет семантику параллельных операций.

Относительно C++11:

Гарантируется ли, что generateVar() будет вызываться только один раз в любом сценарии (если, конечно, используется)?

Нет, ни в каком сценарии.

Инициализация var гарантированно будет потокобезопасной, поэтому generateVar() не будет вводиться одновременно, но если исключение будет вызвано generateVar() или конструктором копирования или конструктором перемещения SomeType (если SomeType является UDT, конечно) , тогда повторная попытка инициализации будет предпринята в следующий раз, когда поток выполнения войдет в объявление, что означает, что generateVar() будет вызван снова.

Согласно параграфу 6.7/4 стандарта C++11 об инициализации переменных области блока с статической продолжительностью хранения:

[...] Если инициализация завершается выдачей исключения, инициализация не завершена, поэтому она будет предпринята снова при следующем входе элемента управления в объявление. Если управление вводит объявление одновременно с инициализацией переменной, параллельное выполнение должно дождаться завершения инициализации. Если элемент управления повторно входит в объявление рекурсивно во время инициализации переменной, поведение не определено. [...]

Относительно вашего следующего вопроса:

Гарантируется ли, что foo вернет одно и то же значение при многократном вызове в любом сценарии?

Если удастся вернуть значение (см. выше), то да.

Есть ли разница в поведении для примитивных или непримитивных типов?

Нет, не существует, за исключением того, что не существует такой вещи, как конструктор копирования или конструктор перемещения для примитивных типов, поэтому также нет риска, что инициализация копированием приведет к возникновению исключения (если, конечно, generateVar() не выдает).

person Andy Prowl    schedule 24.05.2013
comment
инициализация будет предпринята в следующий раз? Как это достигается? Насколько мне известно, статическая переменная инициализируется при загрузке модуля, и это происходит только один раз за время жизни процесса. - person Devolus; 24.05.2013
comment
@Devolus: статическая переменная в области блока инициализируется, когда ее объявление встречается в потоке выполнения. Я добавил цитату из Стандарта - person Andy Prowl; 24.05.2013
comment
@AndyProwl: спасибо за ответ. Не могли бы вы уточнить, что произойдет в C++98? - person Andrew; 24.05.2013
comment
Благодарю за разъяснение. - person Devolus; 24.05.2013
comment
Пожалуйста, помните, что VS (2012, текущая версия) поддерживает только подмножество C++11, а не эту «волшебную статику». Таким образом, для значительной части мира программирования это еще не применимо в реальном коде :-( - person Mike Vine; 24.05.2013
comment
@Andrew: C++98, как и C++03, не определяет понятие потока, поэтому каждая многопоточная программа имеет (формально) неопределенное поведение. Я думаю, все остальное зависит от реализации - person Andy Prowl; 24.05.2013
comment
@Andrew: я расширил свой ответ - person Andy Prowl; 24.05.2013
comment
@AndyProwl: возможно, вы захотите коснуться повторного входа. Если generateVar вызовет foo во время инициализации var, произойдут плохие вещи. В gcc генерируется конкретное исключение. - person Matthieu M.; 24.05.2013
comment
@MatthieuM.: Действительно, это неопределенное поведение, но в последнем предложении абзаца стандарта, который я цитировал в ответе, упоминается, что - person Andy Prowl; 24.05.2013