EEVblog Electronics Community Forum

Products => Computers => Programming => Topic started by: peter-h on August 04, 2023, 01:15:51 pm

Title: What is thread-safe initialization of local static objects
Post by: peter-h on August 04, 2023, 01:15:51 pm
With Cube IDE v 1.13.1 I get this in the errata

With STM32CubeIDE v1.5.0, the option “Disable thread-safe initialization of local static objects (-fnothreadsafe-
statics)” has changed the default value from “true” to “false”. This means that both flash
memory and RAM usages are slightly increased with respect to previous versions of STM32CubeIDE, with the
benefit of removing a potential race condition in the embedded code. To preserve the old behavior, make sure
that the checkbox for the option is checked under [Project properties]>[C/C++ Build]>[Settings]>[Tool
Settings]>[MCU G++ Compiler]>[Optimization].


I did a google but can't find an explanation I can understand.

Obviously statics are not thread safe. Only variables on the stack are, plus variables stored in some structure which has the RTOS task number as the 2nd dimension (a popular method with old multiuser OSs).

Title: Re: What is thread-safe initialization of local static objects
Post by: ejeffrey on August 04, 2023, 02:38:11 pm
Static variables are neither thread safe nor not thread safe, it depends on how you use them i.e., protected by a mutex.  Technically that's true of automatic variables but *most* automatic variables are not shared across threads.

Thread safe initialization means that the variables are protected from being initialized twice in two different threads.  Since initialization happens automatically you don't have the option to make them thread safe yourself.
Title: Re: What is thread-safe initialization of local static objects
Post by: ejeffrey on August 04, 2023, 02:58:23 pm
Note that this probably only affects static variables with non trivial initialization.  If the initialized is a constant it will just be initialized at program load time with the rest of the data section.  However if your initialized requires code execution I believe that happens the first time the function is called.  There is a flag indicating that the variable is initialized and the first caller will do the initialization and set the flag.  This is the part that the "thread safe initialization" affects, presumably creating a mutex to guard the initialization flag.
Title: Re: What is thread-safe initialization of local static objects
Post by: peter-h on August 04, 2023, 08:49:09 pm
Quote
Static variables are neither thread safe nor not thread safe, it depends on how you use them i.e., protected by a mutex.

Surely a function like

int fred (int x)
{
   int y=3;
   int dd=0;
   int z=(x*y)+dd;
   return (z);
}

is re-entrant and thus thread-safe.

The y=3 cannot be done at program init (i.e. it is not done with DATA, and DD=0 is also not done by placing dd into BSS; the compiler creates some code which lives at the start of that function, and it gets run every time that function is called, IME.

How would you create non thread safe initialisation?
Title: Re: What is thread-safe initialization of local static objects
Post by: SiliconWizard on August 04, 2023, 08:59:03 pm
It's a GCC option and "documented" (if we can call it that) here: https://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Dialect-Options.html

-fno-threadsafe-statics

Note that it's purely a C++ option, so if you don't use C++, it should have no effect.

My understanding is that, contrary to C, for which initializers of static variables must be constants, in C++, local static variables can be initialized at run-time and thus there may be problems of thread-safety *for the initialization*. It's only about initialization. Obviously as you mentioned, using static variables may not be thread-safe per se otherwise. It all depends on how they are used.

As a simple illustration, the following piece of code is not correct C and won't compile:
Code: [Select]
#include <stdlib.h>

int Foo(int m)
{
    static int n = rand();

    return n + m;
}

But in C++, it is correct code and will compile.

Example of GCC (C++ mode) output for ARM:

with '-Os':
Code: [Select]
Foo(int):
 push {r3, r4, r5, lr}
 mov r5, r0
 ldr r4, [pc, #36] ; (2c <Foo(int)+0x2c>)
 ldr r3, [r4, #0]
 dmb ish
 lsls r3, r3, #31
 bmi.n 24 <Foo(int)+0x24>
 mov r0, r4
 bl 0 <__cxa_guard_acquire>
    R_ARM_THM_CALL __cxa_guard_acquire
 cbz r0, 24 <Foo(int)+0x24>
 bl 0 <rand>
    R_ARM_THM_CALL rand
 str r0, [r4, #4]
 mov r0, r4
 bl 0 <__cxa_guard_release>
    R_ARM_THM_CALL __cxa_guard_release
 ldr r0, [r4, #4]
 add r0, r5
 pop {r3, r4, r5, pc}
 nop
 .word 0x00000000
    R_ARM_ABS32 .bss
It guards the initialization with calls to  __cxa_guard_acquire and __cxa_guard_release.

With '-Os -fno-threadsafe-statics':
Code: [Select]
Foo(int):
 push {r3, r4, r5, lr}
 mov r5, r0
 ldr r4, [pc, #20] ; (1c <Foo(int)+0x1c>)
 ldr r3, [r4, #0]
 lsls r3, r3, #31
 bmi.n 16 <Foo(int)+0x16>
 bl 0 <rand>
    R_ARM_THM_CALL rand
 movs r3, #1
 str r0, [r4, #4]
 str r3, [r4, #0]
 ldr r0, [r4, #4]
 add r0, r5
 pop {r3, r4, r5, pc}
 .word 0x00000000
    R_ARM_ABS32 .bss
It doesn't guard initialization.

So this may look a bit pointless in practice. But it's actually mandated by C++11 as far as I've seen.
GCC just gives an option to disable it.

If you don't use C++ or use C++ but don't use local static objects, there is absolutely nothing to be concerned about.
Title: Re: What is thread-safe initialization of local static objects
Post by: ejeffrey on August 04, 2023, 10:34:20 pm
Quote
Static variables are neither thread safe nor not thread safe, it depends on how you use them i.e., protected by a mutex.

Surely a function like

int fred (int x)
{
   int y=3;
   int dd=0;
   int z=(x*y)+dd;
   return (z);
}

is re-entrant and thus thread-safe.

The y=3 cannot be done at program init (i.e. it is not done with DATA, and DD=0 is also not done by placing dd into BSS; the compiler creates some code which lives at the start of that function, and it gets run every time that function is called, IME.

How would you create non thread safe initialisation?

That option is only about static variables not automatic/stack allocated variables.  Stack allocated variables indeed always have thread-safe initialization, although you can then do non-thread-safe activities with them, for instance if you store a pointer to a local variable somewhere another thread can access it.
Title: Re: What is thread-safe initialization of local static objects
Post by: peter-h on August 05, 2023, 06:23:22 am
Quote
static int n = rand();

The "static int n" is itself non thread safe; no need for the non-constant initialisation. The variable n will be placed in main RAM, not on the stack, and the only "benefit" of n being defined inside a function is that its name is private.

I have lots of functions like that, sometimes manipulating explicitly global variables, but such functions can be called from only one RTOS task.

Also calling rand() is probably not re-entrant, especially if the randomisation comes from a bit of hardware (usually the best way to do "truly random" numbers).

So, I must be stupid (I certainly am compared to all you guys) because I don't get the deal here. The examples given are obviously not going to be re-entrant.

Does this option mean that any statement which is initially loading a local variable gets mutexed?
Title: Re: What is thread-safe initialization of local static objects
Post by: gf on August 05, 2023, 12:22:15 pm
Does this option mean that any statement which is initially loading a local variable gets mutexed?

It is double-checked locking (https://en.wikipedia.org/wiki/Double-checked_locking), i.e. the test of the "is initialized" flag is primarily lock-free. The lock is only acquired if the flag indicates that the variable is not yet initialized. Once the variable has been initialized, the lock is not acquired any more.
Title: Re: What is thread-safe initialization of local static objects
Post by: peter-h on August 05, 2023, 01:12:42 pm
This is well above my pay grade, so I am happy to see it is for C++ only which I have studiously avoided :)

I pay people on freelancer.com to knock up little win32 utilities, in MS VC++...

I use mutexes extensively and had a nasty experience with the Newlib printf / heap family where they a) don't supply sources b) implement them as stubs c) fail to compile them as "weak" (I posted all the details) so I won't ask how GCC implements them ;) Maybe it puts them on the heap? I am not sure one can make totally target-independent mutexes, which can be hidden wholly inside the language. You have to either use TEST/SET or call a stub which the dev then implements. In my target I use locks provided by FreeRTOS.
Title: Re: What is thread-safe initialization of local static objects
Post by: ejeffrey on August 05, 2023, 05:19:15 pm
Quote
static int n = rand();

The "static int n" is itself non thread safe; no need for the non-constant initialisation. The variable n will be placed in main RAM, not on the stack, and the only "benefit" of n being defined inside a function is that its name is private.

No this is a fundamental misunderstanding of what it means to be thread safe.

Thread safety can roughly be thought of as a property of code, not data.  It is a promise that the code will behave correctly when called from multiple threads.  It is 100% possible to do this with static variables, and the listed code is one way to do it.  n is never modified after initialization (and could/should be declared const in C++). The compiler is required to ensure thread safe initialization so the whole construct is thread safe.  Other ways to do this involve using mutexes or atomic operations to update the static variable.

Generally you are responsible for making your code thread safe if needed, but when the compiler generates code for you, such as static initialization the standard specified if and how the generated must be thread safe.

Thread local is  mostly what you are talking about and is really a property of the data not the code.  It means code than can or should be only accessed from a single thread.  It's certainly one tool for creating thread safe code but not the only one.
Title: Re: What is thread-safe initialization of local static objects
Post by: ejeffrey on August 05, 2023, 05:57:33 pm
Also calling rand() is probably not re-entrant, especially if the randomisation comes from a bit of hardware (usually the best way to do "truly random" numbers).

This is mostly irrelevent to the issue at hand, but on linux at least rand() is thread safe and rand_r is deprecated.  I believe rand() is explicitly required to be psuedo-random, not a true random number generator.  In any case, given only the code shown here, rand() is only called once, so it's thread safety is not important, the only point of that example was to initialize to a value that couldn't be a compile time constant.

Quote
The examples given are obviously not going to be re-entrant.

The code siliconwizard posted is for sure silly, but is legal multi-thread safe C++ code with well defined behavior.

Quote
Does this option mean that any statement which is initially loading a local variable gets mutexed?

Specifically for static local variables that are initialized with run-time executed expressions, they will have a mutex and a flag guarding their initialization.  It will generally use some form of double checked locking optimization to avoid acquiring the lock after initialization is complete and globally visible.

static local variables that are initialized with constant values will simply be included in .bss or .data, no special synchronization is required.  Automatic local variables are thread local and create a new copy for each invocation so they don't need any special handling.