4

In a code review today, I stumbled across the following bit of code (slightly modified for posting):

while (!initialized)
{
  // The thread can start before the constructor has finished initializing the object.
  // Can lead to strange behavior. 
  continue;
}

This is the first few lines of code that runs in a new thread. In another thread, once initialization is complete, it sets initialized to true.

I know that the optimizer could turn this into an infinite loop, but what's the best way to avoid that?

  • volatile - considered harmful
  • calling an isInitialized() function instead of using the variable directly - would this guarantee a memory barrier? What if the function was declared inline?

Are there other options?

Edit:

Should have mentioned this sooner, but this is portable code that needs to run on Windows, Linux, Solaris, etc. We use mostly use Boost.Thread for our portable threading library.

leedm777
  • 21,698
  • 10
  • 53
  • 84
  • 5
    Why not just... not start the thread until initialization is complete? I.e., move the code that starts the new thread to the end of the initialization code of the other thread. – Karl Knechtel Dec 30 '10 at 17:25
  • 1
    @Karl yeah, the best solution is to avoid the situation altogether. I'm still interested in the answer to the general question, though. – leedm777 Dec 30 '10 at 17:32
  • Oops, this isn't an *exact* duplicate. This isn't an `exit_now` flag, so you'd want `std::atomic` with at least `memory_order_acquire`. `mo_relaxed` would *not* be sufficient here, the way it is for an `exit_now` flag. Only noticed the difference after I'd closed it. From the title, people probably aren't going to be looking at this for other ways to solve the initialization problem, though (other than spin-wait). – Peter Cordes May 13 '21 at 14:14

5 Answers5

6

Calling a function won't help at all; even if a function is not declared inline, its body can still be inlined (barring something extreme, like putting your isInitialized() function in another library and dynamically linking against it).

Two options that come to mind:

  • Declare initialized as an atomic flag (in C++0x, you can use std::atomic_flag; otherwise, you'll want to consult the documentation for your threading library for how to do this)

  • Use a semaphore; acquire it in the other thread and wait for it in this thread.

James McNellis
  • 327,682
  • 71
  • 882
  • 954
  • if the function body is in another compilation unit (.cpp), not necessarily in another library, I don't think it can be inlined (don't know if this is defined in the standard, but having a dependency between two .cpp files would be weird) – Roman L Dec 30 '10 at 19:35
  • 1
    @7vies: Compilers can delay some optimization and code generation until link time. Visual C++ does this, for example (it's called Link-Time Code Generation, or LTCG). – James McNellis Dec 30 '10 at 19:37
  • For the infinite loop part: [Multithreading program stuck in optimized mode but runs normally in -O0](https://stackoverflow.com/q/58516052) is a modern version of this. `std::atomic` with `memory_order_relaxed` is the lightest-weight way. But in that case it's an `exit_now` flag. This use-case is different; we need an acquire load since we're presumably about to read data another thread initialized. Anyway, the available operations on `std::atomic_flag` are / were very limited, like only test-and-set, not just pure write, so it's less efficient than `atomic` – Peter Cordes May 13 '21 at 14:13
4

@Karl's comment is the answer. Don't start processing in thread A until thread B has finished initialization. They key to doing this is sending a signal from thread B to thread A that it is up & running.

You mentioned no OS, so I will give you some Windows-ish psudocode. Transcode to the OS/library of your choice.

First create a Windows Event object. This will be used as the signal:

Thread A:

HANDLE running = CreateEvent(0, TRUE, FALSE, 0);

Then have Thread A start Thread B, passing the event along to it:

Thread A:

DWORD thread_b_id = 0;
HANDLE thread_b = CreateThread(0, 0, ThreadBMain, (void*)handle, 0, &thread_b_id);

Now in Thread A, wait until the event is signaled:

Thread A:

DWORD rc = WaitForSingleObject(running, INFINITE);
if( rc == WAIT_OBJECT_0 )
{
  // thread B is up & running now...
  // MAGIC HAPPENS
}

Thread B's startup routine does its initialization, and then signals the event:

Thread B:

DWORD WINAPI ThreadBMain(void* param)
{
  HANDLE running = (HANDLE)param;
  do_expensive_initialization();
  SetEvent(running); // this will tell Thread A that we're good to go
}
John Dibling
  • 94,084
  • 27
  • 171
  • 303
3

Synchronization primitives are the solution to this problem, not spinning in a loop... But if you must spin in a loop and can't use a semaphore, event, etc, you can safely use volatile. It's considered harmful because it hurts the optimizer. In this case that's exactly what you want to do, no?

martona
  • 4,990
  • 1
  • 15
  • 18
  • 1
    You still need a memory barrier even with volatile, else the effects of `initialized = true` can become visible before the effects of the object initialization (because the CPU can reorder the memory accesses). Atomic variables and other synchronization primitives usually have implicit memory barriers for that reason. – CesarB May 07 '11 at 15:51
0

There is a boost equivalent of atomic_flag which is called once_flag in boost::once. It may well be what you want here.

Effectively if you want something to be constructed the first time it is called, eg lazy loading, and happens in multiple threads, you get boost::once to call your function the first time it is reached. The post-condition is that it has been initialized so there is no need for any kind of looping or locking.

What you do need to ensure is that your initialization logic does not throw exceptions.

CashCow
  • 29,087
  • 4
  • 53
  • 86
0

This is a well known problem when working with threads. Creation/Initialization of objects takes relatively little time. When the thread actually starts running though... That can take quite a long time in terms of executed code.

Everyone keeps mentioning semaphores...

You may want to look at POSIX 1003.1b semaphores. Under Linux, try man sem_init. E.g.:

These semaphores have the advantage that, once Created/Initialized, one thread can block indefinitely until signaled by another thread. More critically, that signal can occur BEFORE the waiting thread starts waiting. (A significant difference between Semaphores and Condition Variables.) Also, they can handle the situation where you receive multiple signals before waking up.

alanc
  • 3,921
  • 18
  • 24
Mr.Ree
  • 7,990
  • 25
  • 28