Является ли доступ к глобальной «статической» переменной из одного TU гарантированно безопасным?

Библиотека SFML инициализирует аудиоустройство при создании первого объекта типа AudioResource и деинициализирует его при уничтожении последнего.

Я пытаюсь упростить этот код, так как считаю, что текущее решение с использованием std::[shared/weak]_ptr является излишним.

Я предложил альтернативу, используя глобальную переменную static. Вот упрощенная версия (настоящая защищена std::mutex):

// AudioResource.hpp --------------------------------

struct AudioResource
{
     AudioResource();
    ~AudioResource();
    // ...
};
// AudioResource.cpp --------------------------------

#include "AudioResource.hpp"

static int deviceRC = 0;

 AudioResource() { if (deviceRC++ == 0) { /* init device   */ }; }
~AudioResource() { if (--deviceRC == 0) { /* deinit device */ }; }
// ...

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

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

Я прав, или возможно, что приведенный выше код приводит к неопределенному поведению из-за того, что каким-то образом осуществляется доступ к deviceRC до его инициализации?

Существует ли сценарий, при котором создание объектов AudioResource из нескольких TU или использование SFML в качестве общей библиотеки может вызвать проблемы?

Безопасен ли код, даже если deviceRC не инициализируется константой? Например.,

// SomeOtherTU.cpp --------------------------------

static AudioResource audioResource; 
    // Could `deviceRC` be accessed while uninitialized here?

Другие сопровождающие SFML предложили использовать здесь переменную области функции static:

// AudioResource.cpp --------------------------------

#include "AudioResource.hpp"

static int& deviceRC() 
{
    static int result = 0;
    return result;
}

 AudioResource() { if (deviceRC()++ == 0) { /* init device   */ }; }
~AudioResource() { if (--deviceRC() == 0) { /* deinit device */ }; }
// ...

Является ли подход с использованием переменной функциональной области static более безопасным и/или необходимым по сравнению с подходом, использующим переменную static файловой области?

Ваше понимание правильное. Время жизни глобальной переменной имеет статическую продолжительность хранения. Статическая функция локальной области действия не требуется, поскольку переменная не доступна другим единицам перевода (фиаско статической инициализации). Если используется многопоточность, вы можете использовать static std::atomic<int>. Дэйв С. говорит: «Привет!»

Eljay 19.06.2024 18:42

@JaMiT: в чем будет проблема, если deviceRC будет динамически инициализироваться во время выполнения? Единственное место, где к нему можно получить доступ, — это AudioResource.cpp TU, поэтому я ожидаю, что инициализация произойдет. Вы думаете о случае, когда глобальный объект AudioResource создается в другом TU?

Vittorio Romeo 19.06.2024 18:43

Конечно, порядок инициализации вступит в силу только в том случае, если другая единица перевода создаст глобальную переменную типа AudioResource или глобальную переменную, инициализатор которой создает экземпляр AudioResource.

Igor Tandetnik 19.06.2024 18:46

«настоящая переменная защищена std::mutex» Обратите внимание, что хотя static int deviceRC = 0 инициализируется статически, глобальная переменная типа std::mutex фактически инициализируется динамически. Таким образом, вы можете попытаться заблокировать мьютекс, который еще не был инициализирован.

Igor Tandetnik 19.06.2024 18:48

@IgorTandetnik: один из вариантов, что все может пойти не так, — это если TU создает глобальный объект типа AudioResource, и инициализация этого глобального объекта выполняется до инициализации deviceRC (или мьютекса в реальном случае). Верно? По сути: (1) у некоторых TU есть объект static AudioResource audioResource; (2) тело AudioResource::AudioResource() выполняется до инициализации deviceRC; (3) бум – неопределенное поведение?

Vittorio Romeo 19.06.2024 18:56

Да. Ну, этого не может произойти с deviceRC (он инициализируется значением 0, как только загружается исполняемый образ, до запуска любого кода); но это может произойти с мьютексом, к которому пытается получить доступ конструктор AudioResource. Обратите внимание, что создание локальной статики во вспомогательной функции не поможет - в этом случае построение будет в порядке, но разрушение станет проблемой; мьютекс будет уничтожен до того, как запустится деструктор ~AudioResource этой глобальной переменной (поскольку объекты уничтожаются в порядке, противоположном построению).

Igor Tandetnik 19.06.2024 19:00

@IgorTandetnik: спасибо. Я не осознавал, что деструктор также вызовет проблему. Полагаю, что единственный способ гарантировать правильность — это придерживаться исходной реализации std::shared_ptr + std::weak_ptr? Или есть способ избежать проблемы уничтожения мьютекса?

Vittorio Romeo 19.06.2024 19:04

@VittorioRomeo Я просто хотел прокомментировать именно этот сценарий. Если у другого TU есть глобальный AudioResource, вы не можете знать, был ли deviceRC уже инициализирован.

NathanOliver 19.06.2024 19:04

Что касается статической переменной fuction-scope, здесь она безопаснее и/или необходима? Ответ: да. Если бы оно не было статическим, оно не сохранялось бы между вызовами функций, и вы всегда получали бы новое целое число. У вас также будет висячая ссылка, поскольку вы ссылаетесь на объект, который уничтожается в конце функции.

NathanOliver 19.06.2024 19:06

@NathanOliver: Я понимаю, что означает создание переменной области функции static, я спрашивал, будет ли подход с использованием функции более безопасным по сравнению с подходом, использующим область действия файла static. Немного уточнил вопрос.

Vittorio Romeo 19.06.2024 19:09

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

Igor Tandetnik 19.06.2024 19:09

@IgorTandetnik: возможна ли преднамеренная утечка мьютекса? Например. static auto& getMutex() { static auto* p = new std::mutex{}; return *p; }

Vittorio Romeo 19.06.2024 19:11

Я использовал подход преднамеренной утечки для объекта (например, std::mutex), у которого нет ресурсов для очистки, кроме занимаемой памяти. static auto& getMutex() { static auto* intentional_leak_p = new std::mutex{}; return *intentional_leak_p; } Память будет освобождена ОС после завершения работы программы.

Eljay 19.06.2024 19:21

Да, это должно сработать. Если вы можете смириться с накладными расходами на блокировку (которые зависят от того, как часто AudioResource объекты создаются и уничтожаются).

Igor Tandetnik 19.06.2024 19:33

@IgorTandetnik: В итоге я нашел это решение, чтобы предотвратить разрушение мьютекса и при этом избежать динамического выделения. Если вы оставите свои комментарии в качестве ответа, я буду рад их принять! Спасибо.

Vittorio Romeo 19.06.2024 20:02

Важно не то, находится ли переменная в одном TU, а то, со сколькими двоичными файлами связана эта TU. Его можно определить в нескольких динамических библиотеках одновременно.

Mark Ransom 19.06.2024 20:03

@IgorTandetnik std::mutex имеет конструктор по умолчанию constexpr. Он будет иметь постоянную инициализацию. Беспокойство вызывает только разрушение, в зависимости от того, что именно означает eel.is/c++draft/basic.start.term#3.sentence-3.

user17732522 19.06.2024 20:08

@user17732522: user17732522: разве constinit не требуется для гарантии постоянной инициализации? К сожалению, мне нужно ориентироваться на C++17.

Vittorio Romeo 19.06.2024 20:19

@VittorioRomeo Нет, правила, определяющие, будет ли гарантированно происходить постоянная инициализация, не зависят от constinit. consinit вызывает диагностику только в том случае, если в результате будет выполнена динамическая инициализация.

user17732522 19.06.2024 20:20

@user17732522: user17732522: разве C++11 не гарантирует инициализацию области функций static для предотвращения гонки?

Vittorio Romeo 19.06.2024 20:21

@VittorioRomeo Извините, я пропустил static.

user17732522 19.06.2024 20:22

@IgorTandetnik: ты уверен насчет проблемы с приказом об уничтожении, о которой ты упоминал ранее? Я как бы принял это как должное, но после некоторого обсуждения (см. github.com/SFML/SFML/pull/3089#discussion_r1646608714) кажется, что здесь это не проблема. Мы что-то упускаем?

Vittorio Romeo 19.06.2024 23:13

Да, я ошибался. Демо. Каким-то образом два объекта создаются и уничтожаются в том порядке, который получается.

Igor Tandetnik 19.06.2024 23:49

Итак, вы почему-то считаете shared_ptr «излишним», и ваше решение — сделать собственный счетчик ссылок и спровоцировать огромную дискуссию о технических деталях?

Christian Stieber 20.06.2024 02:43

@ChristianStieber да

Vittorio Romeo 20.06.2024 10:34
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
25
128
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Если deviceRc инициализирован статически, код безопасен, потому что:

  • Вся статическая инициализация происходит до динамической инициализации.
  • Статическая инициализация может наблюдать значение, сохраненное какой-либо другой статической инициализацией, только в тех случаях, когда оба они встречаются в одной и той же единице перевода; в противном случае попытка прочитать значение ранее объявленной статической переменной не будет константным выражением, и вы не получите статическую инициализацию.

Если deviceRc не инициализирован статически, то у вас проблема. audioResource действительно может быть инициализирована раньше deviceRc, поскольку две переменные определены в разных TU. Когда audioResource инициализируется, возможно, что он увидит значение deviceRc, которое было до динамической инициализации, то есть ноль. Если deviceRc имеет динамическую инициализацию, то, по-видимому, инициализация более сложна, чем просто установка нуля. Таким образом, предположительно, программа некорректна, если значение deviceRc считывается до завершения динамической инициализации.

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

Обратите внимание, что этот код оказался небезопасным из-за этого конкретного случая использования: github.com/SFML/SFML/issues/3146

Vittorio Romeo 03.07.2024 14:59

Другие вопросы по теме