Thread-safe any_ptr: Is this code actually thread-safe?

Started by
6 comments, last by Shinkage 13 years, 4 months ago
It's been quite some time since this article has been posted on GameDev, and recently I got to thinking if a thread-safe version was possible without having to use any (platform specific, or library dependent) synchronization mechanism.

I happened to come up with the following:

Replace the following lines in any_ptr.h in the existing article code:
 93    static const int GenerateId() 94    { 95        static int id = 0; 96        return ++id; 97    } 98 99    template <class T>100    static const int TypeId()101    {102        static const int typeId = GenerateId();103        return typeId;104    }

with the following lines instead:
 93    template <class T> 94    static const int TypeId() 95    { 96        static const int typeId( 0 ); 97        return reinterpret_cast<int>( &typeId ); 98    }

The idea behind the new code is that memory will be allocated at program start for each (static integral constant) typeId, belonging to each instantiation of the templated function TypeId. The amount of memory required to be allocated would be deducible at compile-time. Whether the initialization of typeId to zero is thread-safe or not doesn't matter because only its address is used, which serves as a unique identifier for a type.

Can anyone else clarify if this is a working solution?

Thanks in advance!
Francis Xavier
Advertisement
It's not reliably unique. It's perfectly possible for typeid<Foo>() != typeid<Foo>().

It's the same issue as with allocators across DLLs or member function pointer signatures across compilation units and a bunch of other edge cases.

But it can work perfectly well, even for large projects.
Neat idea.
You could do a simmilar thing with the RTTI structures, though it might suffer the same issue that Antheus noted:
    template <class T>    static const int TypeId()    {        static const type_info& typeId = typeid(T);        return reinterpret_cast<int>( &typeId );    }
Or you could replace int with a new type based on the RTTI structures, though type_info::operator== is quite slow on MSVC:
    struct TypeIdentifier    {        TypeIdentifier(const type_info& t) : m(&t) {}        TypeIdentifier(const TypeIdentifier& t) : m(t.m) {}        TypeIdentifier& operator=(const TypeIdentifier& t) { m==t.m; }        bool operator==(const TypeIdentifier& t) const { return m==t.m || (m && t.m && *m==*t.m); }    private:        const type_info* m;    };...    template <class T>    static TypeIdentifier TypeId()    {        return TypeIdentifier( typeid(T) );    }
Quote:Original post by Antheus
It's not reliably unique. It's perfectly possible for typeid<Foo>() != typeid<Foo>().

It's the same issue as with allocators across DLLs or member function pointer signatures across compilation units and a bunch of other edge cases.
As you've mentioned, I now see how any_ptrs passed across DLLs would not work as expected.

However, I've tested the code with gcc (4.4.1) and MSVC (2005/2010) and found that the code works correctly across different translation units (the integral constant returned from the function TypeId() is the same for a particular type). So within the same executable, an any_ptr based on this implementation should work as expected. (Please correct me if I'm wrong here).

Now the question remaining is, within the scope of where it's supposed to work (an executable), will it work in a thread-safe manner?

Thanks in advance!
Francis Xavier
Quote:Original post by Hodgman
Neat idea.
You could do a simmilar thing with the RTTI structures, though it might suffer the same issue that Antheus noted:
*** Source Snippet Removed ***

Or you could replace int with a new type based on the RTTI structures, though type_info::operator== is quite slow on MSVC:
*** Source Snippet Removed ***
By policy, any_ptr does not rely on (and hence does not require) RTTI or exceptions to be enabled in the environment where it's used. That was the motivation behind creating the TypeId() function discussed above.
Quote:Original post by FrancisXavier
Now the question remaining is, within the scope of where it's supposed to work (an executable), will it work in a thread-safe manner?


It's not universally thread-safe, no. In fact, it won't work as expected at all in the (probably unlikely) event that two threads call TypeId() for the first time at the same time (that is, two threads call it before the local typeId has been initialized). When a C++ compiler sees something like this:
static const int typeId = GenerateId();

It usually turns it into something like this behind the scenes:
static bool typeIdReady = false;static const int typeId;if(!typeIdReady) {   typeIdReady = true;   typeId = GenerateId();}

So what happens if the first thread to execute gets preempted between the last two lines of code? Bad things. Bad things happen. Local initialization of static variables is, as a rule, never thread-safe.

On the other hand, in perhaps the majority of use cases you can count on one thread always calling the suspect function well before any others, so the issue usually won't pop up, but it's definitely there.
Quote:Original post by Shinkage
Quote:Original post by FrancisXavier
Now the question remaining is, within the scope of where it's supposed to work (an executable), will it work in a thread-safe manner?
It's not universally thread-safe, no. In fact, it won't work as expected at all in the (probably unlikely) event that two threads call TypeId() for the first time at the same time (that is, two threads call it before the local typeId has been initialized). When a C++ compiler sees something like this:
static const int typeId = GenerateId();
It usually turns it into something like this behind the scenes:
static bool typeIdReady = false;static const int typeId;if(!typeIdReady) {   typeIdReady = true;   typeId = GenerateId();}
So what happens if the first thread to execute gets preempted between the last two lines of code? Bad things. Bad things happen. Local initialization of static variables is, as a rule, never thread-safe.

On the other hand, in perhaps the majority of use cases you can count on one thread always calling the suspect function well before any others, so the issue usually won't pop up, but it's definitely there.
The code you have commented on, is the old code. The new code does not require typeId to be initialized to any particular value. (See the first post in this thread for details).
Ah, sorry, my mistake, misread what was going on there. As for the new code, I'm not sure whether the standard says anything about that one way or the other--my best guess is that it would be implementation specific. Probably the best thing to do would be to compile it and check out the assembly output to see what ends up happening, and you could probably figure out whether it's thread safe from there.

This topic is closed to new replies.

Advertisement