[Development] Q_GLOBAL_STATIC_WITH_INITIALIZER is harmful: please stop using it! (long)
Thiago Macieira
thiago.macieira at intel.com
Fri Mar 16 12:38:57 CET 2012
Ok, some people requested an explanation. I will provide one, but I don't
expect many will understand it or even read until the end of this email, so if
you don't get it, you'll have to trust me (which is the exact same result as
not giving an explanation).
First, two public announcements:
1) The Q_GLOBAL_STATIC macros are *not* public API. Do not use them outside Qt
modules. I *will* change its semantics soon and I will not hesitate to break
your code if you're using it. I will fix all of Qt when making my changes, but
I will not fix your non-Qt code.
For Qt 5.1, my rewritten Q_GLOBAL_STATIC can be made public API.
2) < bhughes> i'd agree with the blanket statement "global static initializers
are evil"
Explanation (short):
Q_GLOBAL_STATIC_WITH_INITIALIZER is not thread-safe. Therefore, you must not
use it: if you want thread-safety, use one of the other two macros. If you
don't care about thread-safety, you can drop the macro altogether. For
example:
Q_GLOBAL_STATIC_WITH_INITIALIZER(Type, object, {initializer})
can be "safely" replaced with:
Type *object()
{
static Type *pointer;
if (!pointer) {
pointer = new Type;
initializer
}
}
that is equally as thread-safe as the macro. And it's a lot more readable.
Explanation (long):
Q_GLOBAL_STATIC_WITH_INIITIALIZER can be used thread-safely, just as the other
macros can be used unsafely. The question we must judge is how often a given
API is used incorrectly. As it happens, this particular macro, especially with
the name that it has, is most often used incorrectly.
Q_GLOBAL_STATIC macros are lock-free. They inspect an atomic pointer[1] to see
if it has been initialised or not. If it has not yet been initialised, a new
object is created and then an atomic compare-and-swap (CAS) operation is
performed. If two or more threads are concurrently trying to initialise the
object, only one of them will succeed in the CAS operation. The other N-1
threads will delete the object they had created and will use the object
created by the thread that succeeded in setting.
It should be clear that an object can be created N times and deleted N-1 times
through that process. That's how it must be if Q_GLOBAL_STATIC is to remain
lock-free[2]. Unfortunately, that means the code run by the object's
constructor and by the evaluation of the arguments in the case of
Q_GLOBAL_STATIC_WITH_ARGS must be thread-safe too. Expensive operations,
thread-safe or not, should be avoided in the initialisation of the global
static. But if they are thread-safe, an expensive operation is acceptable.
Q_GLOBAL_STATIC_WITH_INITIALIZER modifies the macro slightly by adding a
section of code that is run only in the thread that succeeded in the CAS
operation. So, unlike the constructor code, this extra code is run exactly
once. This is tempting.
However, note that the macros remain lock-free. That means this initialisation
code is run completely unsynchronised with other threads. The absence of the
lock means the N-1 threads that failed the CAS will simply loadAcquire the
pointer again and proceed normally, possibly before the initialisation code
gets run. They simply expect that the object is already fully initialised by
then. Moreover, a proper Acquire/Release pair is missing. That means any
further operations executed on the object may be visible to other threads in
any order, including impossible conditions.
Therefore, we conclude this code MUST NOT touch the global static object! The
*only* acceptable extra code is to run extra operations, like registering a
cleanup function:
Q_GLOBAL_STATIC_WITH_INITIALIZER(QtCursorDatabase, cursorDatabase,
qAddPostRoutine(clearCursorDatabase))
(found in qttools/src/shared/qtpropertybrowser/qtpropertymanager.cpp)
Unfortunately, that's not what this macro is being used for. It has a bad name
and causes people to use the extra argument not for post-initialisation, but
for actual initialisation of the object. More often than not, you're going to
find something like:
Q_GLOBAL_STATIC_WITH_INITIALIZER(itemVectorType, staticItemVector, {
*x = itemVector();
})
(found in Qt Creator c0e5060:src/plugins/qt4projectmanager/qtmodulesinfo.cpp)
The magic "x" variable is the object created by the current thread. The code
above exhibits both failures I mentioned: it's not thread-safe and it lacks a
proper Release operation. The type in question (a QVector<const item*>) is
*not* designed for concurrent mutating operations.
The code above can be replaced by the more correct:
Q_GLOBAL_STATIC_WITH_ARGS(itemVectorType, staticItemVector, (itemVector()))
The above is thread-safe and has proper memory ordering semantics. However,
due to its lock-free nature, itemVector() may be called N times and its result
discarded N-1 times. Therefore, the itemVector() function must be thread-safe
too.
More complex code like this one found right below the above:
Q_GLOBAL_STATIC_WITH_INITIALIZER(QStringList, staticModulesList, {
const itemVectorType * const itemVector = staticItemVector();
for (int i = 0; i < itemVector->count(); i++)
x->append(QString::fromLatin1(itemVector->at(i)->config));
})
Can be replaced by the following:
namespace {
struct ModuleList: public QStringList
{
ModuleList()
{
const itemVectorType * const itemVector = staticItemVector();
for (int i = 0; i < itemVector->count(); i++)
append(QString::fromLatin1(itemVector->at(i)->config));
}
};
}
Q_GLOBAL_STATIC(ModuleList, staticModulesList)
[1] as per the C++11 standard, it's neither active nor radioactive
[2] my changes will make it use a lock, which means the initialisation will be
performed exactly once too, obviating the need for the initialisation function
to be thread-safe. However, my current solution is not deadlock-free.
--
Thiago Macieira - thiago.macieira (AT) intel.com
Software Architect - Intel Open Source Technology Center
Intel Sweden AB - Registration Number: 556189-6027
Knarrarnäsgatan 15, 164 40 Kista, Stockholm, Sweden
-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 190 bytes
Desc: This is a digitally signed message part.
URL: <http://lists.qt-project.org/pipermail/development/attachments/20120316/59b8aba4/attachment.sig>
More information about the Development
mailing list