0

(Assuming VC++ 2010: (1) can use /volatile:ms, (2) no std::atomic yet, (3) no thread-safe static variable initialization, (4) no std::call_once)

If I have a plain C pointer, I can impl the following double checked lock pattern to avoid the cost of lock every time:

static volatile void * ptr = nullptr;

//...
if ( ptr == nullptr)
{
   // Acquire Lock
   if (ptr == nullptr)
   {
      // some code
      // ptr = ...; // init ptr
   }
   // Release Lock
}
// ....

Since VC++ 2005, the volatile makes sure the above code is correct. Assume I'm OK with the code being not portable.

Now assume I need to replace the plain pointer with a std::shared_ptr or boost::shared_ptr, how would I do the same thing? How to make that shared_ptr volatile? Do I need another volatile flag?

  • 2
    No, it doesn't. Double-checked locking is bad. – Puppy Sep 22 '12 at 18:33
  • 3
    What makes you believe that `volatile` makes that code correct? – Daniel Fischer Sep 22 '12 at 18:37
  • If you change `static volatile void *ptr = nullptr;` to `std::atomic ptr = nullptr;` the double-checked locking will work. This assumes C++11. – Pete Becker Sep 22 '12 at 18:59
  • @Daniel : `volatile` guarantees a memory barrier in VC++ 2005+. Hence "*Since VC++ 2005, the volatile makes sure the above code is correct.*" ;-] – ildjarn Sep 22 '12 at 19:36
  • @ildjarn Ah, thanks. (Supposing that _is_ actually what made the OP believe the code is correct, since that makes sense.) – Daniel Fischer Sep 22 '12 at 19:45
  • 1
    yes, I do assume /volatile:ms with VC++. (Thus the code is not portable or even correct in standard C++). Sorry for not making it clear. My question should be how to do the same thing with the std::shared_ptr or boost::shared_ptr in VS2010 (where std::atomic is not yet available.) – Lost In Translation Sep 22 '12 at 21:20

3 Answers3

4

With C++11, there are atomic accessor functions for shared_ptr. To write a double-checked lock that uses shared_ptr, use those accessors:

static std::shared_ptr<MyType> ptr;
if (std::atomic_load(ptr) == 0) {
    // lock the lock
    if (std::atomic_load(ptr) == 0) {
        std::shared_ptr<MyType> local_ptr(new MyType);
        std::atomic_store(ptr, local_ptr);
    }
    // unlock the lock
}
return ptr;
Pete Becker
  • 74,985
  • 8
  • 76
  • 165
3

Since VC++ 2005, the volatile makes sure the above code is correct.

No, it does not. volatile has nothing to do with threading or atomicity.

Your current code is incorrect and is not guaranteed by any C++ standard to produce reasonable behavior.

Since your pretend-locking code doesn't work in general, it's certainly not going to work on shared_ptr or other smart pointers. If you want cheaper locking, look into lock-free coding patterns.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • 1
    In _Visual C++_, which the OP specifically mentions, `volatile` guarantees a full memory fence. If the question is about standard C++, you're entirely correct, but I don't get the impression that that's what the question is about. – ildjarn Sep 22 '12 at 19:38
  • Yes. I should be clear that it's about VC++ specific code (using /volatile:ms). Thus the above code is correct. – Lost In Translation Sep 22 '12 at 21:16
1

In C++ 2011 it isn't even necesary to use any explict synchronization. According to 6.7 [stmt.dcl] paragraph 4 the initialization is synchronized by the system:

If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.

This seems to imply that the std::shared_ptr<T> can be initialzed like this:

{
    static std::shared_ptr<MyType> ptr(new MyType(/*...*/));
    // ...
}
Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • Thanks. However, AFAIK, this feature is not even in VC2012 (please correct me if I'm wrong). Also, (1) does it use lazy initialization? (2) if I need to have some costly logic in the "//some code" part before calling the ctor, it's still not ideal. – Lost In Translation Sep 22 '12 at 21:24
  • It is lazy initialization and you'll need to do the costly initialization at some point anyway. You can call a function returning a `std::shared_ptr` if the initialization doesn't fit a constructor. The implementstion will apply something like `std::call_once()` to make sure threads are correctly blocked during initialization and minimally impeded afterwards. I don't know if any compiler implements this feature, yet. I still consider it useful to point out how it is correctly done. – Dietmar Kühl Sep 22 '12 at 21:35
  • @LostInTranslation : You're correct re: VC++ 2012 -- according to [this blog post](http://blogs.msdn.com/b/vcblog/archive/2011/09/12/10209291.aspx) N2660 is not implemented. – ildjarn Sep 23 '12 at 04:56