Simplified One-Time Initialization in Windows Vista

Five years ago, when Microsoft introduced Windows XP, the computing landscape was very different from a threading and synchronization standpoint. The x86 family of chips represented a stable, homogeneous execution environment, and the only place where multi-threading was a real concern was in server applications, which often exhibited natural parallelism through the servicing of simultaneous requests from multiple clients. With the introduction of Windows Vista, processors supporting the Itanium (IA64) and AMD64 instruction sets are widely available and multi-core processors in multi-processor machines are a reality from home users upwards.

The loss of the ubiquitous x86 execution environment, coupled with real simultaneous execution, causes some interesting issues with multi-threaded code. For code that attempts to be clever and minimize the number of locks taken out to guard the consistency of data structures accessible from multiple threads, the x86 memory model provided a level of safety by guaranteeing that all write operations occurred in order. This guarantee does not hold true with the 64-bit processors, and code that reads data without the protections of locks can experience unexpected results caused by the processor reordering instructions. Nowhere is this truer than in variable initialization, where the well known double-check locking pattern is commonly used. Consider the following code:

class ExpensiveToCreateClass{
   private:
   //pretend this comes from a web service call
   int _i;

   public:
   ExpensiveToCreateClass(){
      //first write
      _i=1;
   }
   int get_I(){return _i;}
};

CRITICAL_SECTION G_CriticalSection;
ExpensiveToCreateClass* g_p = NULL;

ExpensiveToCreateClass* getETCCPointer(){
   if (g_p == NULL){
      EnterCriticalSection(&G_CriticalSection);
      if (g_p == NULL){
         //second write
         g_p = new ExpensiveToCreateClass();
      }
      LeaveCriticalSection(&G_CriticalSection);
   }
   return g_p;
}

int _tmain(int argc, _TCHAR* argv[])
{
   InitializeCriticalSection(&G_CriticalSection);
   ExpensiveToCreateClass* p = getETCCPointer();
   if (p)
      printf("%i", p->get_I());
   DeleteCriticalSection(&G_CriticalSection);
   return 0;
}

The code contains a fairly simple implementation of the double-check locking pattern. As far as most programmers (or at least those with familiarity of the x86 memory model) are concerned, it is rock solid in the face of concurrent execution. When weaker memory models that allow reordering of write operations are thrown into the mix, the potential for unexpected results during code execution arises.

The code is commented with the two write operations that are expected to occur in order—the first is the assignments that occur in the constructors, which in this case is the simple assignment of the value one to the variable _i. Once the constructor has fully completed, the value of the pointer to the object is then expected to be assigned to the variable p. If these write operations are reordered, the memory for the object may be allocated, the pointer value of p may be set, and the assignment to _i may happen last. This reordering can allow a thread to access the state of the object before the constructor has executed.

The simplest solution to any problem caused by weaker processor memory models is to avoid clever coding patterns and take locks when reading or writing data structures that are operated on by multiple threads. To avoid locking during the read operations post-initialization, the intrinsic _WriteBarrier function, which is available on x86, x64, and IX64 processors, can be used. The _WriteBarrier function is provided directly by the compiler. It prevents the compiler or the processor from reordering writes, and ensures the assumptions made in the code are correct.

Vista's One-Time Initialization Support

Given how common the requirement for lazily creating various objects is, Windows Vista includes built-in support for one-time initialization of data structures. The built-in initialization support is designed for the fastest possible initialization on the processor architecture that the application is compiled to execute on, and it avoids the need for the developer to have a detailed understanding of memory barriers and threading interactions. The InitOnceBeginInitialize function is the main API for Vista's one-time initialization support. The following shows the double-check code ported to the new functionality (the definition of ExpensiveToCreateClass is omitted for brevity):

INIT_ONCE g_init;

BOOL CALLBACK CreateETCCPointer(PINIT_ONCE InitOnce, PVOID Parameter,
                                PVOID *lpContext)
{
   *lpContext = new ExpensiveToCreateClass();
   return TRUE;
}

ExpensiveToCreateClass* getETCCPointer(){
   PVOID lpContext;
   if (InitOnceExecuteOnce(&g_init,
                           CreateETCCPointer,
                           NULL, &lpContext)){
      return (ExpensiveToCreateClass*)lpContext;
   }
   return NULL;
}

int _tmain(int argc, _TCHAR* argv[])
{
   ExpensiveToCreateClass* p = getETCCPointer();
   if (p)
      printf("%i", p->get_I());
   return 0;
}

Note that the getETCCPointer function, which previously had used double-checking initialization to create the object, now calls InitOnceExecuteOnce. Also, one of the parameters is a pointer to the function that is actually responsible for creating the object. The first time InitOnceExecuteOnce is called, the function pointer is used to call the creating function, which stores the results of the call in the lpContext parameter. The value stored in lpContext is cached, and subsequent calls to InitOnceExecuteOnce using the same INIT_ONCE parameter will result in the cached pointer being returned.

It is also possible to use the new InitOnceBeginInitialize and InitOnceComplete to allow asynchronous one-time initialization. In contrast to InitOnceExecuteOnce, InitOnceBeginInitialize allows multiple threads to attempt to initialize an object at the same time, with the result of the first successful initialization stored using a call to InitOnceComplete. Subsequent calls to InitOnceComplete will return a failure result. The successfully created pointer can be retrieved by calling InitOnceBeginInitialize again. Rarely will a scenario call for multiple threads attempting to simultaneously create an object for one-time use, however, and developers likely won't need to use the InitOnceBeginInitialize and InitOnceComplete APIs.

Safe Multi-Threaded Development Is in Demand

The need to take advantage of parallel computation to achieve better performance is becoming even more pronounced with the declining rate of clock-speed improvements. As such, the number of language and operating system features designed to make life simpler for safe multi-threaded development will increase. Although the one-time initialization support that has been added to Vista is far from revolutionary, Longhorn Server likely will offer a lot more support for thread programming.

About the Author

Nick Wienholt is an independent Windows and .NET consultant based in Sydney, Australia. He is the author of Maximizing .NET Performance from Apress, and specializes in system-level software architecture and development with a particular focus on performance, security, interoperability, and debugging. Nick can be reached at NickW@dotnetperformance.com.



Comments

  • Double Checked Locking

    Posted by grahamr (work) on 01/08/2007 04:57am

    Double Checked Locking has always been a bad idea and has been since long before Vista arrived. Also if you needed this type of support in your code your search would of revealed the Loki library and the Boost library already. Also Boost and Loki work on pre-Vista platforms.

    Reply
  • Not clear

    Posted by kirants on 01/08/2007 12:42am

    For some reason, I find the explanation a little hazu. Perhaps , it is just me. I , somehow, am not able to understand, why the problem of unordered writes is a problem only for initialization. Am sure, the article is making a very important point, but I am not able to see it. Perhaps, you could elaborate ?

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: November 20, 2014 @ 2:00 p.m. ET / 11:00 a.m. PT Are you wanting to target two or more platforms such as iOS, Android, and/or Windows? You are not alone. 90% of enterprises today are targeting two or more platforms. Attend this eSeminar to discover how mobile app developers can rely on one IDE to create applications across platforms and approaches (web, native, and/or hybrid), saving time, money, and effort and introducing apps to market faster. You'll learn the trade-offs for gaining long …

  • Live Event Date: October 29, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Are you interested in building a cognitive application using the power of IBM Watson? Need a platform that provides speed and ease for rapidly deploying this application? Join Chris Madison, Watson Solution Architect, as he walks through the process of building a Watson powered application on IBM Bluemix. Chris will talk about the new Watson Services just released on IBM bluemix, but more importantly he will do a step by step cognitive …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds