Automatic locking for C++ local static initialization

Christophe de Dinechin ddd at cup.hp.com
Wed Aug 2 22:55:27 UTC 2000


"Boehm, Hans" wrote:
> 
> Maybe I'm the only one who's confused, but it seems to me we have at least 3
> possible extreme positions here, and some intermediate ones:
> 
> 1) Local static constructors are just like other code, except that once it
> has been completed it will not be reentered.  Synchronization is the
> programmer's responsibility.  Having the runtime diagnose concurrent
> constructor calls is bad, since sometimes those are benign.

I happen to agree with a position where "synchronization is the programmer's
responsibility". On the other hand, "sometimes those are benign" seems quite
wrong to me. Many constructors do this:

	X::X(): field(0) {
		field = new Data[LARGE_CONSTANT];
	}
	X::~X() {
		delete[] field;
	}

In the multithreaded case, now, this constructor can allocate twenty times the
memory, and leak, _even if protected by the lock mechanism you submitted_ (look
how you reenter the f function in your example.) Worse, consider the following:

	Thread 1		Thread 2
	--------		--------

	X::X()
	field = 0
	field = malloc() = f1				// #0
	start ctors
	f1[0] init
	f1[1] init
	f1[2] init - throws
				X::X
				field = 0
				field = malloc() = f2
				start ctors
	f2[2]->Data::~Data				// #1
				f2[0] init		// #2
	f2[1]->Data::~Data				
				f2[1] init		// #3
	f2[0]->Data::~Data				// #4
	delete f2					// #5
				f2[2] init		// #6

				Proceeds		// #7

If you don't get a nice core dump by doing one of the following, I consider you
lucky:

- calling a dtor on uninitialized data (#1)
- Exiting a constructor (#7) with just destroyed objects (#4), residing in
deleted memory (#5)
- Having a constructor and a destructor executing at the same time on the same
object (#3)
- Having a constructor work on free-d memory (#6)

And, of course, there is still the following:
- Having allocated and initialized memory that will never been freed (#0)
- Having a partially allocated array somewhere in memory (the 'f1' value has
been known to other functions, such as operator new[] and Data::Data.)

So I believe it is fair to say that executing the same constructor twice at the
same time is bad.


The second question is: can we make it so that synchronization is the
programmer's responsibility. I had a discussion a long time back with people who
implemented our scheme, and what I remember from the discussion is that if your
constructor can throw (which is true for 99% of constructors which allocate
memory), you can't do it right manually, except with a convoluted scheme such
as:

	int f() {
		X *ptr = NULL;
		AcquireLock();
		try {
			static X x = initX();
			ptr = &x;
		} catch (...) {
			// init of X failed
			ReleaseLock();
		}
		ReleaseLock();
		// use ptr here.
	}

This is ugly, and roughly equivalent to what we generate anyway. The only case
where static init needs not be protected is if you can ensure for other reasons
that no more than one thread will enter a given function at the same time. But
this doesn't work for the most frequent schemes, such as the singleton pattern.



> 
> 2) It should be illegal to enter a constructor for the same static twice
> concurrently, no matter what the cause.  Having the run-time diagnose this
> error is good.  (It can't easily be diagnosed reliably, since the problem
> may not occur with a different thread schedule.)
> 
> 3) The runtime should try to add locking to prevent reentry, eventhough this
> may add deadlocks to code that assumed (1).
> 
> I read the official HP position as being between 2 and 3, i.e. either try to
> delay a thread to avoid the simultaneous constructor calls, or if that's
> impossible, report an error.

This is no more an "official HP position" than yours ;-). This is what our
compiler implements.

>  I would personally prefer either 1 or a pure
> 2, since I think it makes the programming model easier to state.  If either
> the official HP position or (3) is chosen, it needs to be explicitly
> documented that programming to model (1) is incorrect.

The C++ Standard says so: you are not allowed to enter the same constructor
twice. To me, this is one of the rare statements in the Standard that has an
impact on a threading implementation.

On the other hand, while trying to be compliant, we can also try to accomodate
code written for less compliant platforms, hence the delay. Practically, our
current delay (100s) means that most code that runs on another platform will run
on ours, just more reliably. Adopting (3) would make so much code fail at
runtime that we would get severe complaints. Note that we used to abort, and we
now proceed after emitting an error message.


> If you take position 2, and just want to detect the error, do you really
> need ABI support?  You can just set a volatile flag to one of three states:
> not initialized, in progress, initialized.  If you find the in progress
> state, you report an error.  You will of course not detect all programs with
> races of this kind, but no proposed scheme does that.

This would have been much simpler, but again we also have to take into
consideration satisfaction of customers. On the other hand, I agree that your
model is cleaner, so if everybody implements it, we could go for that and give
our customers a good hint that something goes wrong in their application.


Christophe




More information about the cxx-abi-dev mailing list