Thread-safe lazy creation with TBB?
-
27-10-2019 - |
Question
In my C++ code I'm keeping a pointer to an object which should be created lazily, i.e., created only upon request. I have following code, which is clearly not thread-safe.
LAZY* get_lazy()
{
if (0 == _lazy)
_lazy = create_lazy();
return _lazy;
}
I wonder what kind of synchronization should I use here? I know Boost.thread provides supports for one-time initialization. But I'm hoping that there is a simple solution using TBB + C++ only. I should also note that...
- I cannot create
_lazy
as a static object (I actually want to keep an unbounded array of such lazily created objects) - Such
LAZY
objects cannot be over-allocated (creation is very expensive)
Solution
You need a local mutex (tbb::mutex), to be sure you create your lazy object only once.
#include <tbb/mutex.h>
tbb::atomic<LAZY*> _lazy;
tbb::mutex myMutex;
LAZY* GetLazy()
{
if (0 == _lazy)
{
myMutex.lock();
if (0 == _lazy)
_lazy = create_lazy();
myMutex.unlock();
}
return _lazy;
}
OTHER TIPS
Is it okay to occasionally call create_lazy
more than once? If so this is a very lightweight, efficient solution using only TBB:
tbb::atomic<LAZY*> lazy;
if(!lazy)
{
LAZY *newlazy = create_lazy();
if(lazy.compare_and_swap(newlazy, 0))
{
// lazy was initialized elsewhere.
delete newlazy;
}
}
// use lazy.
This will have much less (zero!) overhead than Maciej's solution, but again will only work if it's okay to occasionally call create_lazy
more than once in the event that there is contention among threads on that specific variable.
One way to avoid both a mutex and calling create_lazy
more than once is to use a spin loop. This will use more CPU than a mutex if there is contention, but will still be low overhead:
tbb::atomic<LAZY*> lazy;
static int sentry;
if(!lazy && !lazy.compare_exchange((LAZY*)&sentry, 0))
{
// lazy is set to a sentry value while being allocated.
try{ lazy = create_lazy(); }
catch(...) { lazy = 0; throw; }
}
else
{
// yield the thread while lazy is still set to the sentry.
while(lazy == (LAZY*)&sentry)
{
tbb::this_tbb_thread::yield();
}
}
// use lazy.
You might also look at how this problem is solved internally in TBB. The name to search for in the code is atomic_do_once
; it's an internal (at the moment of writing) TBB function for lazy initialization. The definition of this function and auxiliary stuff is in src/tbb_misc.h, and there are a few places in other files where it is used.
The basic idea is the same as in @CoryNelson's answer, but generalized with the help of a tri-state flag (see enum do_once_state
). One needs to create a static variable of type tbb::atomic<do_once_state>
, and pass it, together with a function/functor that should be run once, into a call to atomic_do_once
. For example:
void initialize_once();
static tbb::atomic<tbb::internal::do_once_state> init_state;
/*...*/
// Safe to execute concurrently
tbb::internal::atomic_do_once( &initialize_once, init_state );
For long-running initialization, using tbb::mutex
like recommended by @MaciejDopieralski is preferred, as it avoids excessive CPU usage by putting waiting thread(s) to sleep. Note that most other mutex flavors in TBB also spin, not sleep.