Patterns for using the InitOnce functions


Since writing lock-free code is is such a headache-inducer, you're probably best off making some other people suffer the headaches for you. And those other people are the kernel folks, who have developed quite a few lock-free building blocks so you don't have to. For example, there's a collection of functions for manipulating interlocked lists. But today we're going to look at the one-time initialization functions.

The simplest version of the one-time initialization functions isn't actually lock-free, but it does implement the double-checked-lock pattern for you so you don't have to worry about the details. The usage pattern for the Init­Once­Execute­Once function is pretty simple. Here it is in its simplest form:

int SomeGlobalInteger;

BOOL CALLBACK ThisRunsAtMostOnce(
    PINIT_ONCE initOnce,
    PVOID Parameter,
    PVOID *Context)
{
    calculate_an_integer(&SomeGlobalInteger);
    return TRUE;
}

void InitializeThatGlobalInteger()
{
    static INIT_ONCE initOnce = INIT_ONCE_STATIC_INIT;
    InitOnceExecuteOnce(&initOnce,
                        ThisRunsAtMostOnce,
                        nullptr, nullptr);
}

In the simplest form, you give Init­Once­Execute­Once an INIT_ONCE structure (where it records its state), and a callback. If this is the first time that Init­Once­Execute­Once is called for a particular INIT_ONCE structure, it calls the callback. The callback can do whatever it likes, but presumably it's doing some one-time initialization. If another thread calls Init­Once­Execute­Once on the same INIT_ONCE structure, that other thread will wait until the first thread is finished its one-time execution.

We can make this a tiny bit fancier by supposing that the calculation of the integer can fail.

BOOL CALLBACK ThisSucceedsAtMostOnce(
    PINIT_ONCE initOnce,
    PVOID Parameter,
    PVOID *Context)
{
    return SUCCEEDED(calculate_an_integer(&SomeGlobalInteger));
}

BOOL TryToInitializeThatGlobalInteger()
{
    static INIT_ONCE initOnce = INIT_ONCE_STATIC_INIT;
    return InitOnceExecuteOnce(&initOnce,
                               ThisSucceedsAtMostOnce,
                               nullptr, nullptr);
}

If your initialization function returns FALSE, then the initialization is considered to have failed, and the next time somebody calls Init­Once­Execute­Once, it will try to initialize again.

A slightly fancier use of the Init­Once­Execute­Once function takes advantage of the Context parameter. The kernel folks noticed that an INIT_ONCE structure in the "initialized" state has a lot of unused bits, and they've offered to let you use them. This is convenient if the thing you're initializing is a pointer to a C++ object, because it means that there's only one thing you need to worry about instead of two.

BOOL CALLBACK AllocateAndInitializeTheThing(
    PINIT_ONCE initOnce,
    PVOID Parameter,
    PVOID *Context)
{
    *Context = new(nothrow) Thing();
    return *Context != nullptr;
}

Thing *GetSingletonThing(int arg1, int arg2)
{
    static INIT_ONCE initOnce = INIT_ONCE_STATIC_INIT;
    void *Result;
    if (InitOnceExecuteOnce(&initOnce,
                            AllocateAndInitializeTheThing,
                            nullptr, &Result))
    {
        return static_cast<Thing*>(Result);
    }
    return nullptr;
}

The final parameter to Init­Once­Execute­Once function receives the magic almost-pointer-sized data that the function will remember for you. Your callback function passes this magic value back through the Context parameter, and then Init­Once­Execute­Once gives it back to you as the Result.

As before, if two threads call Init­Once­Execute­Once simultaneously on an uninitialized INIT_ONCE structure, one of them will call the initialization function and the other will wait.

Up until now, we've been looking at the synchronous initialization patterns. They aren't lock-free: If you call Init­Once­Execute­Once and initialization of the the INIT_ONCE structure is already in progress, your call will wait until that initialization attempt completes (either successfully or unsuccessfully).

More interesting is the asynchronous pattern. Here it is, as applied to our Singleton­Manager exercise:

 SingletonManager(const SINGLETONINFO *rgsi, UINT csi)
               : m_rgsi(rgsi), m_csi(csi),
                 m_rgio(new INITONCE[csi]) {
   for (UINT iio = 0; iio < csi; iio++) {
    InitOnceInitialize(&m_rgio[iio]);
   }
 }
 ...
 // Array that describes objects we've created
 // runs parallel to m_rgsi
 INIT_ONCE *m_rgio;
};

ITEMCONTROLLER *SingletonManager::Lookup(DWORD dwId)
{
 ... same as before until we reach the "singleton constructor pattern"

 void *pv = NULL;
 BOOL fPending;
 if (!InitOnceBeginInitialize(&m_rgio[i], INIT_ONCE_ASYNC,
                              &fPending, &pv)) return NULL;

 if (fPending) {
  ITEMCONTROLLER *pic = m_rgsi[i].pfnCreateController();
  DWORD dwResult = pic ? 0 : INIT_ONCE_INIT_FAILED;
  if (InitOnceComplete(&m_rgio[i],
                       INIT_ONCE_ASYNC | dwResult, pic)) {
   pv = pic;
  } else {
   // lost the race - discard ours and retrieve the winner
   delete pic;
   InitOnceBeginInitialize(&m_rgio[i], INIT_ONCE_CHECK_ONLY,
                           X&fPending, &pv);
  }
 }
 return static_cast<ITEMCONTROLLER *>(pv);
}

The pattern for asynchronous initialization is as follows:

  • Call Init­Once­Begin­Initialize in async mode.
  • If it returns fPending == FALSE, then initialization has already been performed and you can go ahead and use the result passed back in the final parameter.
  • Otherwise, initialization is pending. Do your initialization, but remember that since this is a lock-free algorithm, there can be many threads trying to initialize simultaneously, so you have to be careful how you manipulate global state. This pattern works best if initialization takes the form of creating a new object (because that means multiple threads performining initialization are each creating independent objects).
  • Call Init­Once­Complete with the result of your initialization.
  • If Init­Once­Complete succeeds, then you won the initialization race, and you're done.
  • If Init­Once­Complete fails, then you lost the initialization race and should clean up your failed initialization. In that case, you should call Init­Once­Begin­Initialize one last time to get the answer from the winner.

it's conceptually simple; it just takes a while to explain. but at least now it's in recipe form.

Exercise: Instead of calling Init­Once­Complete with INIT_ONCE_INIT_FAILED, what happens if the function simply returns without ever completing the init-once?

Exercise: What happens if two threads try to perform asynchronous initialization and the first one to complete fails?

Exercise: Combine the results of the first two exercises and draw a conclusion.

Comments (14)
  1. IC says:

    It's 12:46AM on Saturday here in Australia and I'm awake reading this series of articles. They are very good. Please keep posting in-depth explorations of challenging subject matter!

  2. acq says:

    > This pattern works best if initialization takes the form of creating a new object (because that means multiple threads performing initialization are each creating independent objects

    I'm having hard time figuring out the real life scenario where this knowledge can be used. Can anybody give some specific and real example where it's important (in the sense "it has visible performance impact") to have the lock free initialisation like here? I mean if it's just "allocate some object" (as "on the heap") I see no reason to do all this. like, do you know if your allocator uses global lock anyway, etc? And especially for the case when the synchronisation is to happen only once in the life of the program?

    [If your algorithm is lock-free, then it cannot cause a deadlock. Handy when your function can be called with an unknown lock state. I should write an article about this aspect since you can even combine it with a lock to get lock-free-like behavior with locks. -Raymond]
  3. Joshua says:

    And I would have just used the lock-free multi-constructed singleton published in the previous article rather than try to untangle this API in my head.

  4. Adam Rosenfield says:

    1. If the function fails to call InitOnceComplete, then every subsequent call to InitOnceBeginInitialize is going to return with fPending == TRUE, so it will try to recreate the singleton every time and return a different object on each call to SingletonManager::Lookup.  Result: not a singleton.

    2. The documentation on InitOnceComplete does not say what the effect of the INIT_ONCE_INIT_FAILED flag.  In a quick test I did, InitOnceComplete returned 0 with GetlastError() = ERROR_INVALID_PARAMETER [*].  Result: The first thread which failed at initialization returns NULL from Lookup, subsequent threads will try again to initialize the singleton, since the initialization is still considered to be in progress (because nobody properly finished it).

    3. Don't fail your initialization.

    [*] I steppted through the disassembly, and it looks like InitOnceComplete checks that at most 1 bit is set in dwFlags; if not, the internal helper function returns STATUS_INVALID_PARAMETER_2, resulting in the last error getting set as ERROR_INVALID_PARAMETER.  This suggests that (INIT_ONCE_ASYNC | INIT_ONCE_INIT_FAILED) is not a valid parameter.  I tried various flags combinations, along with passing NULL and non-NULL for lpContext, and they all resulted in ERROR_INVALID_PARAMETER.

    Thanks for this series on the Interlocked* and related functions this week, Raymond.  Keep up the good stuff.

  5. Christopher says:

    Hmmm… I'm struggling a bit with the line

    static INIT_ONCE initOnce = INIT_ONCE_STATIC_INIT; // INIT_ONCE_STATIC_INIT == RTL_RUN_ONCE_INIT == {0}

    Does this get placed in .data or something by the compiler in an initialized state? Is that guaranteed somewhere? If not, how is the coordination done so that only one thread will initialize the uninitialized initOnce?

    [See chapter 6.7 paragraph 4 of the C++ language specification. I can't explain everything; some things you will just have to go figure out yourself. -Raymond]
  6. Christopher says:

    Ah, thanks, Raymond. Searching for "static" resulted in way too many hits in the standard, so I was looking in the wrong places. Keyword overloading…

  7. Burak KALAYCI says:

    Minimum supported client for InitOnceExecuteOnce is Vista!?! Useless for those of us who target 2000 or even XP…

    Have we run out of old new things?

    [Apparently I would have been better off if I had never mentioned, "Oh, and if you're using Vista or higher, there's an easier way." I should just cover old stuff and not bother mentioning that newer versions of Windows make things easier. I'll remember that for next time. If you are targeting Windows NT 3.1, you can continue doing it the hard way. (And how dare you leave out people who are still targeting Windows NT 3.1?) -Raymond]
  8. 640k says:

    XP is still officially supported by MS.

  9. Burak KALAYCI says:

    Dear Raymond:

    My comment was the result of my disappointment. It seems I missed your comment about Vista, my bad, but it certainly is not to be found in this post. I would have really appreciated a warning at the start of this post.

    Actually I think if you update the post (even now), you will cause a lot of people save time.

    Best.

    [I'm not going to call out which version of Windows introduced each API I write about. If that's important to you, you can go look it up yourself. (That way you can't blame me if I looked it up wrong, which I often do.) -Raymond]
  10. S says:

    @640k – That doesn't mean it is officially supported by Raymond though. Remember Raymond != Microsoft.

  11. acq says:

     I should write an article about this aspect

    I'm enjoying your writing and I'm looking forward for such articles too.

  12. T.C. says:

    Neat! Any patterns for deleting the objects created by means of InitOnce? Is this something I'd have to do at DLL unloading?

    [Destroy them when you don't want them any more. (Waiting until DLL unloading is not a great idea due to constraints on what you can do in DllMain.) -Raymond]
  13. hex_omega says:

    "Instead of calling Init­Once­Complete with INIT_ONCE_INIT_FAILED, what happens if the function simply returns without ever completing the init-once?"

    Returning without ever calling InitOnceComplete is exactly what is done in the sample in MSDN.

    msdn.microsoft.com/…/ms686934(v=VS.85).aspx

    In either case, calling InitOnceComplete or not when initialization fails, would result in the Lookup method returning NULL. Unless I'm missing some subtlety (completely possible), I don't see why calling it is necessary in the case of initialization failure.  

  14. nksingh says:

    @hex_omega:

    It seems like this is a subtlety that's missing from the documentation.  It's legitimate to signal INIT_FAILED if you're using the synchronous functionality, but you cannot do the same thing when using async.

Comments are closed.

Skip to main content