Groups | Search | Server Info | Keyboard shortcuts | Login | Register [http] [https] [nntp] [nntps]


Groups > comp.lang.c++ > #81160 > unrolled thread

Does static initialization of a local variable always happens synchronized ?

Started byBonita Montero <Bonita.Montero@gmail.com>
First post2021-09-11 11:06 +0200
Last post2021-09-12 08:24 +0200
Articles 11 — 4 participants

Back to article view | Back to comp.lang.c++


Contents

  Does static initialization of a local variable always happens synchronized ? Bonita Montero <Bonita.Montero@gmail.com> - 2021-09-11 11:06 +0200
    Re: Does static initialization of a local variable always happens synchronized ? "Alf P. Steinbach" <alf.p.steinbach@gmail.com> - 2021-09-11 13:47 +0200
      Re: Does static initialization of a local variable always happens synchronized ? Bonita Montero <Bonita.Montero@gmail.com> - 2021-09-11 16:17 +0200
        Re: Does static initialization of a local variable always happens synchronized ? "Alf P. Steinbach" <alf.p.steinbach@gmail.com> - 2021-09-11 17:09 +0200
          Re: Does static initialization of a local variable always happens synchronized ? Bonita Montero <Bonita.Montero@gmail.com> - 2021-09-11 17:18 +0200
          Re: Does static initialization of a local variable always happens synchronized ? David Brown <david.brown@hesbynett.no> - 2021-09-11 20:03 +0200
    Re: Does static initialization of a local variable always happens synchronized ? Paavo Helde <myfirstname@osa.pri.ee> - 2021-09-11 15:13 +0300
    Re: Does static initialization of a local variable always happens synchronized ? Bonita Montero <Bonita.Montero@gmail.com> - 2021-09-11 16:02 +0200
    Re: Does static initialization of a local variable always happens synchronized ? Bonita Montero <Bonita.Montero@gmail.com> - 2021-09-11 21:19 +0200
      Re: Does static initialization of a local variable always happens synchronized ? "Alf P. Steinbach" <alf.p.steinbach@gmail.com> - 2021-09-11 22:05 +0200
        Re: Does static initialization of a local variable always happens synchronized ? Bonita Montero <Bonita.Montero@gmail.com> - 2021-09-12 08:24 +0200

#81160 — Does static initialization of a local variable always happens synchronized ?

FromBonita Montero <Bonita.Montero@gmail.com>
Date2021-09-11 11:06 +0200
SubjectDoes static initialization of a local variable always happens synchronized ?
Message-ID<shhrig$7fl$1@dont-email.me>
Look at this code:

#include <iostream>

using namespace std;

#if defined(_MSC_VER)
	#define NOINLINE __declspec(noinline)
#elif defined(__GNUC__) || defined(__clang__)
	#define NOINLINE __attribute((noinline))
#endif

struct S
{
	S();
};

NOINLINE
S::S()
{
	cout << "S::S()" << endl;
}

NOINLINE
S &fn()
{
	static
	S s;
	return s;
}

int main()
{
	fn();
	fn();
}


I call fn twice, but of course s is initialized only once at the first
call of fn. What I asked myself if this happens synchronized, i.e. if
multiple threads try to initialize s, it's only initiailized only once
in a locked context.
With MSVC this happens as expected: before the initialization of s, fn
calls _Init_thread_header and afterwards it calls _Init_thread_footer.
These functions each call EnterCriticalSection or LeaveCriticalSection
on a global mutex. But is this behaviour guaranteed for C++ in general ?

[toc] | [next] | [standalone]


#81162

From"Alf P. Steinbach" <alf.p.steinbach@gmail.com>
Date2021-09-11 13:47 +0200
Message-ID<shi50r$5vv$1@dont-email.me>
In reply to#81160
On 11 Sep 2021 11:06, Bonita Montero wrote:
> Look at this code:
> 
> #include <iostream>
> 
> using namespace std;
> 
> #if defined(_MSC_VER)
>      #define NOINLINE __declspec(noinline)
> #elif defined(__GNUC__) || defined(__clang__)
>      #define NOINLINE __attribute((noinline))
> #endif
> 
> struct S
> {
>      S();
> };
> 
> NOINLINE
> S::S()
> {
>      cout << "S::S()" << endl;
> }
> 
> NOINLINE
> S &fn()
> {
>      static
>      S s;
>      return s;
> }
> 
> int main()
> {
>      fn();
>      fn();
> }
> 
> 
> I call fn twice, but of course s is initialized only once at the first
> call of fn. What I asked myself if this happens synchronized, i.e. if
> multiple threads try to initialize s, it's only initiailized only once
> in a locked context.
> With MSVC this happens as expected: before the initialization of s, fn
> calls _Init_thread_header and afterwards it calls _Init_thread_footer.
> These functions each call EnterCriticalSection or LeaveCriticalSection
> on a global mutex. But is this behaviour guaranteed for C++ in general ?

Yes.

C++ 17 §9.7/4 (next to last sentence applies):
❝Dynamic initialization of a block-scope variable with static storage 
duration or thread storage duration is performed the first time control 
passes through its declaration; such a variable is considered 
initialized upon the completion of its initialization. If the 
initialization exits by throwing an exception, the initialization is not 
complete, so it will be tried again the next time control enters the 
declaration. If control enters the declaration concurrently while the 
variable is being initialized, the concurrent execution shall wait for 
completion of the initialization. If control re-enters the declaration 
recursively while the variable is being initialized, the behavior is 
undefined.❞

- Alf

[toc] | [prev] | [next] | [standalone]


#81173

FromBonita Montero <Bonita.Montero@gmail.com>
Date2021-09-11 16:17 +0200
Message-ID<shidqb$7vq$1@dont-email.me>
In reply to#81162
Am 11.09.2021 um 13:47 schrieb Alf P. Steinbach:
> On 11 Sep 2021 11:06, Bonita Montero wrote:
>> Look at this code:
>>
>> #include <iostream>
>>
>> using namespace std;
>>
>> #if defined(_MSC_VER)
>>      #define NOINLINE __declspec(noinline)
>> #elif defined(__GNUC__) || defined(__clang__)
>>      #define NOINLINE __attribute((noinline))
>> #endif
>>
>> struct S
>> {
>>      S();
>> };
>>
>> NOINLINE
>> S::S()
>> {
>>      cout << "S::S()" << endl;
>> }
>>
>> NOINLINE
>> S &fn()
>> {
>>      static
>>      S s;
>>      return s;
>> }
>>
>> int main()
>> {
>>      fn();
>>      fn();
>> }
>>
>>
>> I call fn twice, but of course s is initialized only once at the first
>> call of fn. What I asked myself if this happens synchronized, i.e. if
>> multiple threads try to initialize s, it's only initiailized only once
>> in a locked context.
>> With MSVC this happens as expected: before the initialization of s, fn
>> calls _Init_thread_header and afterwards it calls _Init_thread_footer.
>> These functions each call EnterCriticalSection or LeaveCriticalSection
>> on a global mutex. But is this behaviour guaranteed for C++ in general ?
> 
> Yes.
> 
> C++ 17 §9.7/4 (next to last sentence applies):
> ❝Dynamic initialization of a block-scope variable with static storage 
> duration or thread storage duration is performed the first time control 
> passes through its declaration; such a variable is considered 
> initialized upon the completion of its initialization. If the 
> initialization exits by throwing an exception, the initialization is not 
> complete, so it will be tried again the next time control enters the 
> declaration. If control enters the declaration concurrently while the 
> variable is being initialized, the concurrent execution shall wait for 
> completion of the initialization. If control re-enters the declaration 
> recursively while the variable is being initialized, the behavior is 
> undefined.❞

It couldn't be really undefined since the flag which remembers that
the object has been created successfully is set afterwards. Otherwise
it woud be impossible that the object will be tried to be re-created
if the constructor throws an exception and the function containing
the object will be called again. So an infinite recursion is the only
possible behaviour here.

[toc] | [prev] | [next] | [standalone]


#81175

From"Alf P. Steinbach" <alf.p.steinbach@gmail.com>
Date2021-09-11 17:09 +0200
Message-ID<shigrr$vah$1@dont-email.me>
In reply to#81173
On 11 Sep 2021 16:17, Bonita Montero wrote:
> Am 11.09.2021 um 13:47 schrieb Alf P. Steinbach:
>> On 11 Sep 2021 11:06, Bonita Montero wrote:
>>> Look at this code:
>>>
>>> #include <iostream>
>>>
>>> using namespace std;
>>>
>>> #if defined(_MSC_VER)
>>>      #define NOINLINE __declspec(noinline)
>>> #elif defined(__GNUC__) || defined(__clang__)
>>>      #define NOINLINE __attribute((noinline))
>>> #endif
>>>
>>> struct S
>>> {
>>>      S();
>>> };
>>>
>>> NOINLINE
>>> S::S()
>>> {
>>>      cout << "S::S()" << endl;
>>> }
>>>
>>> NOINLINE
>>> S &fn()
>>> {
>>>      static
>>>      S s;
>>>      return s;
>>> }
>>>
>>> int main()
>>> {
>>>      fn();
>>>      fn();
>>> }
>>>
>>>
>>> I call fn twice, but of course s is initialized only once at the first
>>> call of fn. What I asked myself if this happens synchronized, i.e. if
>>> multiple threads try to initialize s, it's only initiailized only once
>>> in a locked context.
>>> With MSVC this happens as expected: before the initialization of s, fn
>>> calls _Init_thread_header and afterwards it calls _Init_thread_footer.
>>> These functions each call EnterCriticalSection or LeaveCriticalSection
>>> on a global mutex. But is this behaviour guaranteed for C++ in general ?
>>
>> Yes.
>>
>> C++ 17 §9.7/4 (next to last sentence applies):
>> ❝Dynamic initialization of a block-scope variable with static storage 
>> duration or thread storage duration is performed the first time 
>> control passes through its declaration; such a variable is considered 
>> initialized upon the completion of its initialization. If the 
>> initialization exits by throwing an exception, the initialization is 
>> not complete, so it will be tried again the next time control enters 
>> the declaration. If control enters the declaration concurrently while 
>> the variable is being initialized, the concurrent execution shall wait 
>> for completion of the initialization. If control re-enters the 
>> declaration recursively while the variable is being initialized, the 
>> behavior is undefined.❞
> 
> It couldn't be really undefined since the flag which remembers that
> the object has been created successfully is set afterwards. Otherwise
> it woud be impossible that the object will be tried to be re-created
> if the constructor throws an exception and the function containing
> the object will be called again. So an infinite recursion is the only
> possible behaviour here.

The standard says it's undefined behavior, hence it's undefined 
behavior. Really. So I believe that you meant to say that there can't be 
any other actual effect of this UB than infinite recursion.

But of course there can be: formal UB means that the compiler is free to 
insert checking that does whatever the compiler writers want.

By the way I'm sorry for not making clear that the thread safey 
guarantee stems from C++11. Instead my quoting made it seem like C++17. 
Happily Paavo Helde pointed that out else-thread. :)


- Alf

[toc] | [prev] | [next] | [standalone]


#81176

FromBonita Montero <Bonita.Montero@gmail.com>
Date2021-09-11 17:18 +0200
Message-ID<shihc8$2ut$1@dont-email.me>
In reply to#81175
Am 11.09.2021 um 17:09 schrieb Alf P. Steinbach:
> On 11 Sep 2021 16:17, Bonita Montero wrote:
>> Am 11.09.2021 um 13:47 schrieb Alf P. Steinbach:
>>> On 11 Sep 2021 11:06, Bonita Montero wrote:
>>>> Look at this code:
>>>>
>>>> #include <iostream>
>>>>
>>>> using namespace std;
>>>>
>>>> #if defined(_MSC_VER)
>>>>      #define NOINLINE __declspec(noinline)
>>>> #elif defined(__GNUC__) || defined(__clang__)
>>>>      #define NOINLINE __attribute((noinline))
>>>> #endif
>>>>
>>>> struct S
>>>> {
>>>>      S();
>>>> };
>>>>
>>>> NOINLINE
>>>> S::S()
>>>> {
>>>>      cout << "S::S()" << endl;
>>>> }
>>>>
>>>> NOINLINE
>>>> S &fn()
>>>> {
>>>>      static
>>>>      S s;
>>>>      return s;
>>>> }
>>>>
>>>> int main()
>>>> {
>>>>      fn();
>>>>      fn();
>>>> }
>>>>
>>>>
>>>> I call fn twice, but of course s is initialized only once at the first
>>>> call of fn. What I asked myself if this happens synchronized, i.e. if
>>>> multiple threads try to initialize s, it's only initiailized only once
>>>> in a locked context.
>>>> With MSVC this happens as expected: before the initialization of s, fn
>>>> calls _Init_thread_header and afterwards it calls _Init_thread_footer.
>>>> These functions each call EnterCriticalSection or LeaveCriticalSection
>>>> on a global mutex. But is this behaviour guaranteed for C++ in 
>>>> general ?
>>>
>>> Yes.
>>>
>>> C++ 17 §9.7/4 (next to last sentence applies):
>>> ❝Dynamic initialization of a block-scope variable with static storage 
>>> duration or thread storage duration is performed the first time 
>>> control passes through its declaration; such a variable is considered 
>>> initialized upon the completion of its initialization. If the 
>>> initialization exits by throwing an exception, the initialization is 
>>> not complete, so it will be tried again the next time control enters 
>>> the declaration. If control enters the declaration concurrently while 
>>> the variable is being initialized, the concurrent execution shall 
>>> wait for completion of the initialization. If control re-enters the 
>>> declaration recursively while the variable is being initialized, the 
>>> behavior is undefined.❞
>>
>> It couldn't be really undefined since the flag which remembers that
>> the object has been created successfully is set afterwards. Otherwise
>> it woud be impossible that the object will be tried to be re-created
>> if the constructor throws an exception and the function containing
>> the object will be called again. So an infinite recursion is the only
>> possible behaviour here.
> 
> The standard says it's undefined behavior, hence it's undefined 
> behavior. Really. So I believe that you meant to say that there
> can't be any other actual effect of this UB than infinite recursion.

There isn't any other possibility.

> But of course there can be: formal UB means that the compiler is free
> to insert checking that does whatever the compiler writers want.

I didn't refer to the flag-checking but setting the flag. This can only
be done after creating the object since the constructor can throw an
exeption. But C++ shall try to re create the object at the next call;
so this flag can only be set afterwards.

[toc] | [prev] | [next] | [standalone]


#81181

FromDavid Brown <david.brown@hesbynett.no>
Date2021-09-11 20:03 +0200
Message-ID<shir2b$b66$1@dont-email.me>
In reply to#81175
On 11/09/2021 17:09, Alf P. Steinbach wrote:
> On 11 Sep 2021 16:17, Bonita Montero wrote:
>> Am 11.09.2021 um 13:47 schrieb Alf P. Steinbach:
>>> On 11 Sep 2021 11:06, Bonita Montero wrote:

>>> Yes.
>>>
>>> C++ 17 §9.7/4 (next to last sentence applies):
>>> ❝Dynamic initialization of a block-scope variable with static storage
>>> duration or thread storage duration is performed the first time
>>> control passes through its declaration; such a variable is considered
>>> initialized upon the completion of its initialization. If the
>>> initialization exits by throwing an exception, the initialization is
>>> not complete, so it will be tried again the next time control enters
>>> the declaration. If control enters the declaration concurrently while
>>> the variable is being initialized, the concurrent execution shall
>>> wait for completion of the initialization. If control re-enters the
>>> declaration recursively while the variable is being initialized, the
>>> behavior is undefined.❞
>>
>> It couldn't be really undefined since the flag which remembers that
>> the object has been created successfully is set afterwards. Otherwise
>> it woud be impossible that the object will be tried to be re-created
>> if the constructor throws an exception and the function containing
>> the object will be called again. So an infinite recursion is the only
>> possible behaviour here.
> 
> The standard says it's undefined behavior, hence it's undefined
> behavior. Really. So I believe that you meant to say that there can't be
> any other actual effect of this UB than infinite recursion.
> 
> But of course there can be: formal UB means that the compiler is free to
> insert checking that does whatever the compiler writers want.
> 

It is quite easy to see how you could get a deadlock from recursive use,
but not concurrent use.  A typical initialisation sequence could be
roughly :

	if (!initialised_flag) {	// atomic flag
		get_lock();
		if (!initialised_flag) {
			do_initialisation();
			initialised_flag = true;
		}
		release_lock();
	}

Calling this concurrently when the lock is taken, the second thread will
block on "get_lock()" until the first thread is finished, then see the
initialisation was done and go on its way.  But calling it recursively,
the block at "get_lock()" will never finish because it is the same
thread that was supposed to be doing the initialisation.

On the other hand, the sequence could be:

	if (!initialised_flag) {
		initialise_temporary_object();
		compare_and_swap(static_object, temporary_object, null);
		initialised_flag = true;
	}

That would be fine if there is no problem in accidentally creating extra
objects and discarding them.  The compare and swap would avoid
overriding a previous initialisation.  This would work fine with both
concurrent and recursive use.


So the standard says the behaviour is undefined on recursive use,
leaving the implementation freedom to pick either tactic (or other tactics).

[toc] | [prev] | [next] | [standalone]


#81167

FromPaavo Helde <myfirstname@osa.pri.ee>
Date2021-09-11 15:13 +0300
Message-ID<shi6i1$gna$2@dont-email.me>
In reply to#81160
11.09.2021 12:06 Bonita Montero kirjutas:
> I call fn twice, but of course s is initialized only once at the first
> call of fn. What I asked myself if this happens synchronized, i.e. if
> multiple threads try to initialize s, it's only initiailized only once
> in a locked context.

Thread-safe initialization of statics is guaranteed since C++11.

[toc] | [prev] | [next] | [standalone]


#81172

FromBonita Montero <Bonita.Montero@gmail.com>
Date2021-09-11 16:02 +0200
Message-ID<shictj$1p7$1@dont-email.me>
In reply to#81160
Am 11.09.2021 um 11:06 schrieb Bonita Montero:
> Look at this code:
> 
> #include <iostream>
> 
> using namespace std;
> 
> #if defined(_MSC_VER)
>      #define NOINLINE __declspec(noinline)
> #elif defined(__GNUC__) || defined(__clang__)
>      #define NOINLINE __attribute((noinline))
> #endif
> 
> struct S
> {
>      S();
> };
> 
> NOINLINE
> S::S()
> {
>      cout << "S::S()" << endl;
> }
> 
> NOINLINE
> S &fn()
> {
>      static
>      S s;
>      return s;
> }
> 
> int main()
> {
>      fn();
>      fn();
> }
> 
> 
> I call fn twice, but of course s is initialized only once at the first
> call of fn. What I asked myself if this happens synchronized, i.e. if
> multiple threads try to initialize s, it's only initiailized only once
> in a locked context.
> With MSVC this happens as expected: before the initialization of s, fn
> calls _Init_thread_header and afterwards it calls _Init_thread_footer.
> These functions each call EnterCriticalSection or LeaveCriticalSection
> on a global mutex. But is this behaviour guaranteed for C++ in general ?

And MSVC of course uses double-checked locking here !

[toc] | [prev] | [next] | [standalone]


#81183

FromBonita Montero <Bonita.Montero@gmail.com>
Date2021-09-11 21:19 +0200
Message-ID<shivfh$98b$1@dont-email.me>
In reply to#81160
Something I wondered about is whether it is possible to get a C function
pointer from a local static lambda _with_ captures like this:

#include <iostream>

int main()
{
	int i;
	static
	auto lambda = [&i]()
	{
		++i;
	};
	void (*pLambda)() = lambda;
}

In theory this should be possible since the lambda doesn't need
its own context-pointer (pseudo-"this") since the capture-context
is global storage. This seems something the designers of C++ didn't
consider, although it is of little use.

[toc] | [prev] | [next] | [standalone]


#81185

From"Alf P. Steinbach" <alf.p.steinbach@gmail.com>
Date2021-09-11 22:05 +0200
Message-ID<shj25j$j54$1@dont-email.me>
In reply to#81183
On 11 Sep 2021 21:19, Bonita Montero wrote:
> Something I wondered about is whether it is possible to get a C function
> pointer from a local static lambda _with_ captures like this:
> 
> #include <iostream>
> 
> int main()
> {
>      int i;
>      static
>      auto lambda = [&i]()
>      {
>          ++i;
>      };
>      void (*pLambda)() = lambda;
> }

This particular example — the lines of code up to the hypothetical 
declaration of `pLambda` — is flawed because if you put this code in any 
function other than `main` it will then use a dangling reference on 
second call of that function.

---

> In theory this should be possible since the lambda doesn't need
> its own context-pointer (pseudo-"this") since the capture-context
> is global storage.

If you make `i` static then you have something that a smart enough 
compiler can rewrite to a capture-less lambda. Don't know if they do 
though. And conversion to C function pointer would need to be a language 
extension.


---

> This seems something the designers of C++ didn't
> consider, although it is of little use.

You can get a freestanding function that references any C++ object 
simply by storing a reference or pointer to (a copy of) that object 
indirectly or directly in a static variable that the function uses.

For example, the type of the static variable can be `std::function<void()>`.

And that scheme can be elaborated to provide a function that you can use 
like `as_c_callback( [&]{ ++i; } )`. To make that safe against multiple 
overlapping uses of `as_c_callback` let the static variable be a 
collection, e.g. with the return type of `as_c_callback` as a RAII 
object with implicit conversion to C function pointer. To make it thread 
safe use thread local storage. Uhm, not sure how to make it fool proof. 
But if such elaborations are adopted it can probably be a good idea to 
separate the elaborations from the basic simple scheme.

- Alf

[toc] | [prev] | [next] | [standalone]


#81188

FromBonita Montero <Bonita.Montero@gmail.com>
Date2021-09-12 08:24 +0200
Message-ID<shk6eb$dm0$1@dont-email.me>
In reply to#81185
Am 11.09.2021 um 22:05 schrieb Alf P. Steinbach:
> On 11 Sep 2021 21:19, Bonita Montero wrote:
>> Something I wondered about is whether it is possible to get a C function
>> pointer from a local static lambda _with_ captures like this:
>>
>> #include <iostream>
>>
>> int main()
>> {
>>      int i;
>>      static
>>      auto lambda = [&i]()
>>      {
>>          ++i;
>>      };
>>      void (*pLambda)() = lambda;
>> }
> 
> This particular example — the lines of code up to the hypothetical 
> declaration of `pLambda` — is flawed because if you put this code in
> any function other than `main` it will then use a dangling reference
> on second call of that function.

Of course there could be a dangling reference, but that's not what I'm
discussing.


> ---
> 
>> In theory this should be possible since the lambda doesn't need
>> its own context-pointer (pseudo-"this") since the capture-context
>> is global storage.

> If you make `i` static then you have something that a smart enough 
> compiler can rewrite to a capture-less lambda. Don't know if they do 
> though. And conversion to C function pointer would need to be a language 
> extension.

Of course I could make i static, but that's not what I'm talking about.

[toc] | [prev] | [standalone]


Back to top | Article view | comp.lang.c++


csiph-web