Здравствуйте, Alexander G, Вы писали:
AG>пара вопросов: AG>1. Нужна ли мне КС или инициализировать через do_once саму data_t — какие будут практические рекомендации для выбора ? AG>2. А вот ты бы оставил "опасный" вариант или делал бы настоящий do_once вроде этого ?
Лично я бы постарался всячески избавиться от той ситуации, когда недетерминизм многопоточности накладывается на недетерминизм инициализации статических объектов.
Раз у тебя загрузка модуля отложенная, то, наверно, я бы сделал так:
1) Процедуру загрузки — атомарной
2) Атомарность — lock-based (просто потому, что это проще для понимания и раздачи всяких гарантий)
3) Инфраструктуру для атомарности — т.е. мьютекс aka CRITICAL_SECTION — инициализировал бы отдельно (или заранее, в конструкторе статического объекта, или на механизме do_once)
4) Процедуру чтения — тут есть два варианта.
— Либо потребовать, чтобы загрузка была выполнена обязательно до использования, и тогда плевать на атомарность (мы же только читаем)
— Либо загрузка по требованию, каждый раз, и тогда для ускорения прикрутил бы внутрь процедуры загрузки ещё и двойную проверку.
Тут главное, чтобы каждая подзадача была прозрачна.
Ещё я бы постарался сгруппировать все эти загрузки деталек из модуля в одно целое, чтобы не городить по синглетону на каждую импортируемую функцию.
Здравствуйте, Alexander G, Вы писали:
AG>Я действительно хочу много странного, но именно это странным не является. Требование чтобы синглтон работал при конструировании глобальных объектов, в том числе и других синглтонов — стандартное требование. Фактически, самый простой известный синглтон — синглтон Мейерса — решает в первую очередь именно эту задачу — при этом создавая взамен проблемы с многопоточностью и с exception safety.
Понятно.
У меня просто другая философия.
Подобную глобальную инициализацию/деинициализацию предпочитаю делать явной,
например, создав специальный объект в main.
На зависимостях между глобальными данными уже достаточно много шишек набил.
Наверное таки сформулирую исходную задачу. Чтобы было понятно, почему у меня нет систематического подхода к инициализации объектов.
Я иногда анализирую креш-дампы. Как и автоматически создаваемые и отправляемые пользователями на https://winqual.microsoft.com/ , так и вручную создаваемые через MiniDumpWriteDump. Различные версии Windows дают различные возможности для этого:
Windows 98, ME
Нет ничего. Да и вряд ли стоит стараться для нескольки динозавров.
Windows 2000
Есть DbgHelp.dll . Возможно, той версии, в которой уже есть MiniDumpWriteDump. Так, можно в случае исключения, перехваченного через __except или SetUnhandledExceptionFilter записать креш-дамп. Возможностей по управлению этим крешдампом не очень много, и его стоит делать только в случае продвинутого пользователя, который сам потом его отправит нам и расскажет что за проблема.
Windows XP, 2003
В DbgHelp.dll точно есть MiniDumpWriteDump.
В случае необработанного исключения или зависания пользователь может оправить отчёт в майкрософт. Через тот самый диалог где "Send report" и "Do not send" (жаль что дефолтна последняя). API для управления этим отчётом не предоставляет ничего. Встречаются два вида дампов. Один содержит только эксепшн инфо, стек бектрейс, сегмент данных, инфу по модулям и тредам. Другой — полный дамп кучи, он реже бывает, обычно при зависаниях или по явному требованию после анализа микро-дампов.
Windows Vista, 2008
Имеется полный набор Wer* функций. Можно гра^H^H^H создавать и оправлять свои креш-репорты в winqual, как хочется, включять в них самостоятельно сгенерированные креш-дампы и другие файлы. А ещё можно через функции WerRegisterMemoryBlock, WerRegisterFile, WerSetFlags управлять генерацией автоматически создаваемых креш-дампов. А ещё можно сделать дамп прямо из такск менеджера.
Теперь, как я желаю этим воспользоваться. Во-первых, если надо поддерживать 2000, то всё прийдётся грузить динамически. Далее, дампы, которые делаются вручную, наверное стоит включать только через command-line option, т.к. они не попадут в Winqual и не будут систематизированы, пусть тогда эти дампы отправляются только продвинутыми пользователями по их желанию вместе с описанием проблемы.
Чтобы воспользоваться автоматическими дампами в ХР, неплохо иметь полезную информацию в data segment, т.е. в глобальной переменной. Полезна например будет достаточно свежая MEMORYSTATUS (это API функция вернула нулевой указатель, бо память закончилась — или таки логическая ошибка?). Отсюда требование располагать отладочную структуру в static storage.
В Vista большой интерес представляют WerRegisterMemoryBlock/WerUnregisterMemoryBlock. Можно попробовать зарегистрировать каждый синглтон. Отсюда требование — умение инициализироваться первым. Пока нет полного перехода всех пользователей на Висту, прийдётся находить их динамически из kernel32 (да, самое полезное не в Wer.dll, а в kernel32.dll) через GetProcAddress и иметь стабы для более раних систем. Это — одно из того, что предполагалось инициализировать без синхронизации.
Крешдамп может делаться в условиях испорченной кучи, в условиях нехватки памяти, в условиях переполнения стека. Поэтому сам обработчик UnhandledExceptionFilter пусть вообще использует только глобальные переменные.
Здравствуйте, Кодт, Вы писали:
К>Лично я бы постарался всячески избавиться от той ситуации, когда недетерминизм многопоточности накладывается на недетерминизм инициализации статических объектов.
Не, оно именно работает без детерминизма, вне зависимости от остальной программы, и фича — пытаться работать даже если у меня ошибка синхронизации. Я детализировал
Здравствуйте, Alexander G, Вы писали:
AG>Есть ли тут гонка ?
AG>
long i; // глобальная переменная
AG>void f() // вызывается из нескольких потоков выполнения одновременно
AG>{
AG> i = OSVersion(); // возврашает каждый раз одно и то же
AG> /* дальше используется i. больше нигде она не используется */
AG>}
Зависит от того, что назвать гонкой.
Если взять формальное определение POSIX (более одного потока одновременно обращаются к ячейке памяти, причём хотя бы один поток на запись), то гонка здесь, очевидно, есть. Со всеми вытекающими — UB.
Microsoft явно не документирует модель памяти Windows, хотя, видимо, подоплёка такая же как и в POSIX. Поэтому тоже формально гонка и UB.
C++09 — гонка и UB.
С++03/С99 — формально не документировано.
Java/C# — нет гонок (в том смысле, что считываемое значение детерминировано).
Если смотреть с практической стороны — т.е. гонка есть "нехорошее" поведение, то, в принципе, должно работать на всех распространенных аппаратных платформах и компиляторах. На всех распространенных аппаратных платформах сохранения/загрузки размером со слово по выровненным адресам будут вообще атомарны (на X86/Itanium, на которых работает Windows, даже и 16-ти байтные обращения будут атомарны). А что бы компилятор это попортил,.. ну он должен быть сильно невменяемым (хотя опять же — это не гарантируется).
Но самое интересное начинается дальше. Твой пример — это т.н. "синхронизация на флаге" (немного модифицированная). Она работает. Но если мы возьмём т.н. "синхронизацию на флаге с зависимым состоянием", то что ты приводил в одном из постов, то она уже не будет работать:
Если тебя интересует MSVC+Windows, то это аппаратные платформы IA-32, Intel 64 И IA-64. На IA-32/Intel 64 это не будет работать из-за возможных переупорядочиваний компилятором, т.е. компилятор волен сгенерировать, допустим, такой код:
На IA-64 это не будет работать из-за возможных переупорядочиваний компилятором + из-за возможных переупорядочиваний аппаратным обеспечением. Т.е. если даже компилятор сгенерирует "правильный" код, т.е. аппаратное обеспечение всё-равно вольно это выполнить так, как показано в примере с переупорядочиванием компилятором.
Для MSVC самый просто способ решить это — это добавить volatile к определению initialized. MSVC фактически расширяет значение volatile с примитива для работы с оборудованием до примитива синхронизации потоков. Т.о. volatile и подавит переупорядочивания компилятором, и заставит компилятор вставить необходимые инструкции (барьеры памяти) для подавления переупорядочивания аппаратным обеспечением (для IA-64). Однако это будет иметь некоторую ран-тайм стоимость на IA-64. Если барьер при сохранении в initialized не интересен, т.к. будет выполнен только один раз за время работы программы, то барьер при считывании может быть неприятен, т.к. будет добавлять порядка пары десятков тактов. Хорошие новости то, что его можно элиминировать следующим образом:
Ключевой момент здесь — это замена флага initialized на указатель p_data. Это — искуственная инджекция зависимости по данным между загрузкой флага и загрузкой самого объекта. Все современные процессоры автоматически соблюдают упорядочивание при зависимости по данным, в данном случае — между загрузкой указателя и загрузкой данных через этот указатель. Реализация сохранения во флаг тут реализуется так же — т.е. через volatile.
Хотя, в принципе, конечно, желательно кодировать не против конкретной платформы, а против портабельного стандарта; и спрашивать не "почему это может не работать?", а "кем гарантировано, что это должно работать?" (я имею в виду использование мьютексов и т.д). Однако курьёз в том, что Windows (я так понимаю речь идёт о нём) не предоставляет средств для корректной инициализации объектов (по крайней мере до Vista). В POSIX threads (pthread) всё проще — он из покон веков предоставляет макрос PTHREAD_MUTEX_INITIALIZER для *статической* инициализации глобальных мьютексов. boost.thread с (по-моему) 1.35 предоставляет аналогичную функциональность и под Windows (кстати, это была одна из основных причин для переписывания мьютекса с CRITICAL_SECTION на собственный велосипед на Interlocked операциях). А тащить в проект thread-win32 или boost только из-за этого видится не меньшим злом...
Есть ещё вариант с т.н. Lakos Singleton (nifty counting trick). Он инициализируется при инициализации глобальных объектов, однако старается это делать до любого его использования:
В принципе, его можно обмануть при желании, поэтому желательно этот заголовочный файл включить во все файлы проекта, stdafx.h подойдёт. Тогда объект будет создан во время инициализации глобальных объектов, однако гарантированно до любого его использования. Плюс не вносит никаких издержек во время выполнения, т.е. никаких дополнительных проверок и индирекций при доступе к объекту.
Здравствуйте, Кодт, Вы писали:
К>Сам мьютекс (и все остальные примитивы lock-based синхронизации) нужно инициализировать в рантайме. К>Следовательно, мы всё равно хоть что-то, да должны сделать на lock-free.
Это только в мире Windows, в мире POSIX (или pthread-win32) мы можем просто написать:
Здравствуйте, remark, Вы писали:
R>Здравствуйте, Кодт, Вы писали:
К>>Сам мьютекс (и все остальные примитивы lock-based синхронизации) нужно инициализировать в рантайме. К>>Следовательно, мы всё равно хоть что-то, да должны сделать на lock-free.
R>Это только в мире Windows, в мире POSIX (или pthread-win32) мы можем просто написать: R>
Это все работать будет, если только все эти красивые макросы разворачиваются в константы.
В MS Windows никаких констант нет, значит инициализация g_guard будет не на этапе загрузки секций кода из exe в память, а позже, когда возможно уже какие-то глобальные функции вызываются.
to Alexander G: вас спасут именованные мьютексы. Если из нескольких потоков запрашивать создать мьютекс с одним и тем же именем, то создан будет он только в одном потоке, а в остальные будет возвращен handle созданного мьютекса.
Здравствуйте, remark, Вы писали:
R>Microsoft явно не документирует модель памяти Windows, хотя, видимо, подоплёка такая же как и в POSIX. Поэтому тоже формально гонка и UB.
Simple reads and writes to properly-aligned 32-bit variables are atomic operations. In other words, you will not end up with only one portion of the variable updated; all bits are updated in an atomic fashion. However, access is not guaranteed to be synchronized. If two threads are reading and writing from the same variable, you cannot determine if one thread will perform its read operation before the other performs its write operation.
Simple reads and writes to properly aligned 64-bit variables are atomic on 64-bit Windows. Reads and writes to 64-bit values are not guaranteed to be atomic on 32-bit Windows. Reads and writes to variables of other sizes are not guaranteed to be atomic on any platform.
R>Ключевой момент здесь — это замена флага initialized на указатель p_data. Это — искуственная инджекция зависимости по данным между загрузкой флага и загрузкой самого объекта. Все современные процессоры автоматически соблюдают упорядочивание при зависимости по данным, в данном случае — между загрузкой указателя и загрузкой данных через этот указатель. Реализация сохранения во флаг тут реализуется так же — т.е. через volatile.
Супер. Только непонятно почему _ReadWriteBarrier();
Ещё, можно ли убрать volatile и заменить его на _WriteBarrier(); перед сохранением указателя ?
R>Хотя, в принципе, конечно, желательно кодировать не против конкретной платформы, а против портабельного стандарта; и спрашивать не "почему это может не работать?", а "кем гарантировано, что это должно работать?" (я имею в виду использование мьютексов и т.д).
Вообще да, но данный случай именно против платформы. И желательно без мьютексов. Выше я описал причину странных требований.
R>Есть ещё вариант с т.н. Lakos Singleton (nifty counting trick). Он инициализируется при инициализации глобальных объектов, однако старается это делать до любого его использования:
... R>В принципе, его можно обмануть при желании, поэтому желательно этот заголовочный файл включить во все файлы проекта, stdafx.h подойдёт. Тогда объект будет создан во время инициализации глобальных объектов, однако гарантированно до любого его использования. Плюс не вносит никаких издержек во время выполнения, т.е. никаких дополнительных проверок и индирекций при доступе к объекту.
Если файл включается во все единицы, то всё равно, почему при использование этого из другого глобального объекта (возможно, тоже с внутренним связыванием) не может произойти до создания этого объекта ?
R>
Здравствуйте, alsemm, Вы писали:
A>to Alexander G: вас спасут именованные мьютексы. Если из нескольких потоков запрашивать создать мьютекс с одним и тем же именем, то создан будет он только в одном потоке, а в остальные будет возвращен handle созданного мьютекса.
Интересно.
Для моего случая видимо стоит делать мьютекс так, чтобы он не был один на все копии процесса:
Но мне больше нравится ответ remark, что можно и без мьютекса, если обеспечить правильный запрет переупорядочивания.
A>т.к. если память выделенную для globals_ система может и почистит сама, после завершения приложения, то именованный мьютекс — вряд-ли.
Очистит, и, если им больше никто не владеет, удалит.
Можно попробовать в одном процессе создать, занять и не вернуть, а в другом создать и ждать, при завершении первого во втором д.б. WAIT_ABANDONED.
A>Это все работать будет, если только все эти красивые макросы разворачиваются в константы.
Именно. Они в константы и будут всегда разворачиваться.
A>В MS Windows никаких констант нет, значит инициализация g_guard будет не на этапе загрузки секций кода из exe в память, а позже, когда возможно уже какие-то глобальные функции вызываются.
В Windows можно использовать boost или pthread-win32.
А нужно именно Win32 API, то InitOnce. Если до Vista, то ничего хорошего для этой цели нет — либо самому морочиться с Interlocked операциями, либо premature pessimization в виде именованных объектов ядра для межпроцессного взаимодействия.
Здравствуйте, alsemm, Вы писали:
A>to Alexander G: вас спасут именованные мьютексы. Если из нескольких потоков запрашивать создать мьютекс с одним и тем же именем, то создан будет он только в одном потоке, а в остальные будет возвращен handle созданного мьютекса.
A>Как-то так: A>
A>class Globals
A>{
A> static Globals* instance_;
A>public:
A> Globals*
A> getInstance() // nothrow
A> {
A> HANDLE guard = CreateMutex(0, false, "unique-name-of-your-taste");
A> if (NULL == guard)
A> {
A> return 0;
A> }
A> if (WAIT_FAILED == ::WaitForSingleObject(impl, INFINITE))
A> {
A> return 0;
A> }
A> if (0 == globals_)
A> {
A> globals_ = new (std::nothrow) Globals();
A> }
A> ::ReleaseMutex(guard);
A> return globals_;
A> }
A>};
A>
A>По хорошему надо еще где-то A>
A>delete globals_;
A>::CloseHandle(<тот-самый мьютекс>);
A>
A>делать, т.к. если память выделенную для globals_ система может и почистит сама, после завершения приложения, то именованный мьютекс — вряд-ли.
Не считая того, что получаем 3 системных вызова вместо + блокировку вместо пары инструкций процессора...
Если при каждом получении синглтона создавать по хендлу, то через пару минут лимит хендлов будет достигнут и новые хендлы на event не будут создаваться — будем получать 0 вместо синглтона.
Плюс new (std::nothrow) не работает так, как ты думаешь, соотв. будем получать полное зависание процесса при исключении из new (std::nothrow).
Плюс CloseHandle() надо делать на каждый CreateMutex(), а не один раз. Один CloseHandle() ничего не закроет, т.к. хэндлов насоздавалась уже целая туча.
Плюс ОС сама разрушит event при завершении процесса.
Здравствуйте, remark, Вы писали:
A>>В MS Windows никаких констант нет, значит инициализация g_guard будет не на этапе загрузки секций кода из exe в память, а позже, когда возможно уже какие-то глобальные функции вызываются.
R>В Windows можно использовать boost или pthread-win32.
Там полностью свои сампописные муьютексы/крит. секции?
R>А нужно именно Win32 API, то InitOnce. Если до Vista, то ничего хорошего для этой цели нет — либо самому морочиться с Interlocked операциями,
А чего с ними мучаться — взять просто boost::detail::spinlock и все дела.
R>либо premature pessimization в виде именованных объектов ядра для межпроцессного взаимодействия.
Ну getInstance() — это не тот случай, тобы такты экономить, не часто она и вызываться-то будет.
Здравствуйте, remark, Вы писали:
R>Не считая того, что получаем 3 системных вызова вместо + блокировку вместо пары инструкций процессора... R>Если при каждом получении синглтона создавать по хендлу, то через пару минут лимит хендлов будет достигнут и новые хендлы на event не будут создаваться — будем получать 0 вместо синглтона.
Мой косяк, согласен. См. ниже.
R>Плюс new (std::nothrow) не работает так, как ты думаешь, соотв. будем получать полное зависание процесса при исключении из new (std::nothrow).
Замечание по существу, но ыглядит как придирка, т.к. ничего не мешает или 'new Globals();' в try/catch обернуть, или просто принять, что конструктор Globals не должен кидать исключений.
R>Плюс CloseHandle() надо делать на каждый CreateMutex(), а не один раз. Один CloseHandle() ничего не закроет, т.к. хэндлов насоздавалась уже целая туча.
Да нет проблем:
Здравствуйте, Alexander G, Вы писали:
AG>Здравствуйте, alsemm, Вы писали:
A>>to Alexander G: вас спасут именованные мьютексы. Если из нескольких потоков запрашивать создать мьютекс с одним и тем же именем, то создан будет он только в одном потоке, а в остальные будет возвращен handle созданного мьютекса.
AG>Интересно. AG>Для моего случая видимо стоит делать мьютекс так, чтобы он не был один на все копии процесса:
Ну не знаю, Globals::globals_-то у вас одна на все процессы будет, если она в dll. А если в exe, то да, нужно имя мьютекса уникальное для каждого процесса заводить.
AG>Но мне больше нравится ответ remark, что можно и без мьютекса, если обеспечить правильный запрет переупорядочивания.
"Скользкое решение" imho, хотя и довольно элегантное. С именованным мьтексом выглядит брутально, но зато все очевидно. Кстати в pthread-ах именованные мьютексы тоже есть, это к вопросу о переносимости.
AG>Можно попробовать в одном процессе создать, занять и не вернуть, а в другом создать и ждать, при завершении первого во втором д.б. WAIT_ABANDONED.
А это для чего?
Здравствуйте, alsemm, Вы писали:
AG>>Можно попробовать в одном процессе создать, занять и не вернуть, а в другом создать и ждать, при завершении первого во втором д.б. WAIT_ABANDONED. A>А это для чего?
Это просто к тому, что при аварийном завершении процесса ничего страшного не произойдёт.
Здравствуйте, alsemm, Вы писали:
AG>>Интересно. AG>>Для моего случая видимо стоит делать мьютекс так, чтобы он не был один на все копии процесса: A>Ну не знаю, Globals::globals_-то у вас одна на все процессы будет, если она в dll. А если в exe, то да, нужно имя мьютекса уникальное для каждого процесса заводить.
не уловил, какая разница dll/exe, если синглтон один штук на одно адресное пространство в обоих случаях.
В случае dll вообще прийдётся забыть про Win 2000, которая интересует многих
Здравствуйте, Alexander G, Вы писали:
AG>Здравствуйте, alsemm, Вы писали:
AG>>>Интересно. AG>>>Для моего случая видимо стоит делать мьютекс так, чтобы он не был один на все копии процесса: A>>Ну не знаю, Globals::globals_-то у вас одна на все процессы будет, если она в dll. А если в exe, то да, нужно имя мьютекса уникальное для каждого процесса заводить.
AG>не уловил, какая разница dll/exe, если синглтон один штук на одно адресное пространство в обоих случаях.
Разница такая, что если Globals::globals_ определена в dll, то она будет одинаковая во всех процессах, которые эту dll используют. Следовательно имя мьютекса должно быть одинаковым для все процессов из которых Globals::getInstance() может быть вызвано.
Если Globals::globals_ определена в exe, то на каждый экземпляр запущенного приложения будет своя копия Globals::globals_. Следовательно нужно имя мьютекса выбирать для каждого процесса свое, чтобы оно не конфликтовало с "соседями".
AG>http://msdn.microsoft.com/en-us/library/ms682583(VS.85).aspx AG>
Windows 2000: Do not create a named synchronization object in DllMain because the system will then load an additional DLL.
Досадно. Возможно решить проблему можно статически слинковав свою dll с той DLL, которую дополнительно грузит система. Надо только узнать, чего она там грузит.
Здравствуйте, alsemm, Вы писали:
A>Разница такая, что если Globals::globals_ определена в dll, то она будет одинаковая во всех процессах, которые эту dll используют.
С чего бы это ? она одна на каждый процесс.
Разве что если она в Shared section — так зачем её туда пихать?
A>Досадно. Возможно решить проблему можно статически слинковав свою dll с той DLL, которую дополнительно грузит система. Надо только узнать, чего она там грузит.
Нет, это не решит проблему.
Советую сходить по моей ссылке, найти процитированный текст, и почитать что там ещё рядом написано, ещё, если таки исользуешь инициализацию в dll, неплохо ознакомиться с документом здесь http://go.microsoft.com/FWLink/?LinkId=84138 .
Кстати, несколько месяцев назад вышла книга Рихтера "Windows via C/C++", новая версия этой
Здравствуйте, remark, Вы писали:
R>Есть ещё вариант с т.н. Lakos Singleton (nifty counting trick). Он инициализируется при инициализации глобальных объектов, однако старается это делать до любого его использования:
R>В принципе, его можно обмануть при желании, поэтому желательно этот заголовочный файл включить во все файлы проекта, stdafx.h подойдёт. Тогда объект будет создан во время инициализации глобальных объектов, однако гарантированно до любого его использования. Плюс не вносит никаких издержек во время выполнения, т.е. никаких дополнительных проверок и индирекций при доступе к объекту.
Чем это лучше/хуже синглтона Мейерса, форсированного через конструктор глобального объекта с внутренним связыванием ?
R>
Здравствуйте, Alexander G, Вы писали:
AG>Здравствуйте, alsemm, Вы писали:
A>>Разница такая, что если Globals::globals_ определена в dll, то она будет одинаковая во всех процессах, которые эту dll используют.
AG>С чего бы это ? она одна на каждый процесс. AG>Разве что если она в Shared section — так зачем её туда пихать?
Согласен, я ошибся.
A>>Досадно. Возможно решить проблему можно статически слинковав свою dll с той DLL, которую дополнительно грузит система. Надо только узнать, чего она там грузит.
AG>Нет, это не решит проблему.
Почему?
AG>Советую сходить по моей ссылке, найти процитированный текст, и почитать что там ещё рядом написано
Да вроде почитал.
С другой стороны, почему бы просто явно не инициализировать глобальные данные в DllMain?